Files
coinhunter-cli/tests/test_state_manager.py
Tacit Lab 62c40a9776 refactor: address high-priority debt and publish to PyPI
- 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>
2026-04-16 01:21:27 +08:00

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"]