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>
This commit is contained in:
99
tests/test_state_manager.py
Normal file
99
tests/test_state_manager.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user