- Fix TOCTOU race conditions by wrapping read-modify-write cycles under single-file locks in execution_state, portfolio_service, precheck_state, state_manager, and precheck_service. - Add missing test coverage (96 tests total): - test_review_service.py (15 tests) - test_check_api.py (6 tests) - test_external_gate.py main branches (+10 tests) - test_trade_execution.py new commands (+8 tests) - Unify all agent-consumed JSON messages to English. - Config-ize hardcoded values (volume filter, schema_version) via get_user_config with sensible defaults. - Add 1-hour TTL to exchange cache with force_new override. - Add ruff and mypy to dev dependencies; fix all type errors. - Add __all__ declarations to 11 service modules. - Sync README with new commands, config tuning docs, and PyPI badge. - Publish package as coinhunter==1.0.0 on PyPI with MIT license. - Deprecate coinhunter-cli==1.0.1 with runtime warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""Tests for state_manager precheck utilities."""
|
|
|
|
from datetime import timedelta, timezone
|
|
|
|
from coinhunter.services.state_manager import (
|
|
clear_run_request_fields,
|
|
sanitize_state_for_stale_triggers,
|
|
update_state_after_observation,
|
|
)
|
|
from coinhunter.services.time_utils import utc_iso, utc_now
|
|
|
|
|
|
class TestClearRunRequestFields:
|
|
def test_removes_run_fields(self):
|
|
state = {"run_requested_at": utc_iso(), "run_request_note": "test"}
|
|
clear_run_request_fields(state)
|
|
assert "run_requested_at" not in state
|
|
assert "run_request_note" not in state
|
|
|
|
|
|
class TestSanitizeStateForStaleTriggers:
|
|
def test_no_changes_when_clean(self):
|
|
state = {"pending_trigger": False}
|
|
result = sanitize_state_for_stale_triggers(state)
|
|
assert result["pending_trigger"] is False
|
|
assert result["_stale_recovery_notes"] == []
|
|
|
|
def test_clears_completed_run_request(self):
|
|
state = {
|
|
"pending_trigger": True,
|
|
"run_requested_at": utc_iso(),
|
|
"last_deep_analysis_at": utc_iso(),
|
|
}
|
|
result = sanitize_state_for_stale_triggers(state)
|
|
assert result["pending_trigger"] is False
|
|
assert "run_requested_at" not in result
|
|
assert any("completed run_requested" in note for note in result["_stale_recovery_notes"])
|
|
|
|
def test_clears_stale_run_request(self):
|
|
old = (utc_now() - timedelta(minutes=60)).replace(tzinfo=timezone.utc).isoformat()
|
|
state = {
|
|
"pending_trigger": False,
|
|
"run_requested_at": old,
|
|
}
|
|
result = sanitize_state_for_stale_triggers(state)
|
|
assert "run_requested_at" not in result
|
|
assert any("stale run_requested" in note for note in result["_stale_recovery_notes"])
|
|
|
|
def test_recovers_stale_pending_trigger(self):
|
|
old = (utc_now() - timedelta(minutes=60)).replace(tzinfo=timezone.utc).isoformat()
|
|
state = {
|
|
"pending_trigger": True,
|
|
"last_triggered_at": old,
|
|
}
|
|
result = sanitize_state_for_stale_triggers(state)
|
|
assert result["pending_trigger"] is False
|
|
assert any("stale pending_trigger" in note for note in result["_stale_recovery_notes"])
|
|
|
|
|
|
class TestUpdateStateAfterObservation:
|
|
def test_updates_last_observed_fields(self):
|
|
state = {}
|
|
snapshot = {
|
|
"generated_at": "2024-01-01T00:00:00Z",
|
|
"snapshot_hash": "abc",
|
|
"positions_hash": "pos123",
|
|
"candidates_hash": "can456",
|
|
"portfolio_value_usdt": 100.0,
|
|
"market_regime": "neutral",
|
|
"positions": [],
|
|
"top_candidates": [],
|
|
}
|
|
analysis = {"should_analyze": False, "details": [], "adaptive_profile": {}}
|
|
result = update_state_after_observation(state, snapshot, analysis)
|
|
assert result["last_observed_at"] == snapshot["generated_at"]
|
|
assert result["last_snapshot_hash"] == "abc"
|
|
|
|
def test_sets_pending_trigger_when_should_analyze(self):
|
|
state = {}
|
|
snapshot = {
|
|
"generated_at": "2024-01-01T00:00:00Z",
|
|
"snapshot_hash": "abc",
|
|
"positions_hash": "pos123",
|
|
"candidates_hash": "can456",
|
|
"portfolio_value_usdt": 100.0,
|
|
"market_regime": "neutral",
|
|
"positions": [],
|
|
"top_candidates": [],
|
|
}
|
|
analysis = {
|
|
"should_analyze": True,
|
|
"details": ["price move"],
|
|
"adaptive_profile": {},
|
|
"hard_reasons": ["moon"],
|
|
"signal_delta": 1.5,
|
|
}
|
|
result = update_state_after_observation(state, snapshot, analysis)
|
|
assert result["pending_trigger"] is True
|
|
assert result["pending_reasons"] == ["price move"]
|