SimpleChatbot / hermes_overlay /tests /test_phase4_execution_state_machine.py
Amin
Deploy: HermesFace finalized project to HF Space
2e658e7
Raw
History Blame Contribute Delete
6.89 kB
from __future__ import annotations
import ast
from pathlib import Path
import pytest
from trading.domain.execution_history import (
begin_execution,
execution_events,
finish_execution,
transition_execution,
)
from trading.domain.execution_state import (
CLOSE_UNKNOWN,
CLOSED,
CLOSING,
CONFIGURING_ACCOUNT,
CREATED,
ENTRY_FILLED,
ENTRY_OPEN,
ENTRY_PARTIALLY_FILLED,
ENTRY_UNKNOWN,
FAILED_SAFE,
FAILED_UNPROTECTED,
InvalidExecutionTransition,
LOCKED,
MANUAL_INTERVENTION_REQUIRED,
PARTIALLY_CLOSED,
PLACING_PROTECTION,
PROTECTED,
PROTECTION_PARTIAL,
SUBMITTING_ENTRY,
TRANSITIONS,
VALIDATING,
transition,
)
from trading.domain.schema import apply_migrations, connect
@pytest.fixture(autouse=True)
def _db(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setenv("TRADING_MODE", "paper")
monkeypatch.setenv("HERMES_EXECUTION_ENABLED", "true")
ok, messages = apply_migrations()
assert ok, messages
def _begin() -> str:
return begin_execution(
owner_id="state-owner",
symbol="BTC/USDT:USDT",
action="entry",
mode="paper",
correlation_id="corr-state",
payload={"safe": True},
actor="state-owner",
)
def _history(execution_id: str):
conn = connect()
try:
return conn.execute(
"SELECT state,version,completed_at FROM execution_history WHERE execution_id=?",
(execution_id,),
).fetchone()
finally:
conn.close()
def test_complete_state_graph_and_service_transition_targets_are_declared():
required = {
CREATED,
VALIDATING,
LOCKED,
CONFIGURING_ACCOUNT,
SUBMITTING_ENTRY,
ENTRY_UNKNOWN,
ENTRY_OPEN,
ENTRY_PARTIALLY_FILLED,
ENTRY_FILLED,
PLACING_PROTECTION,
PROTECTION_PARTIAL,
PROTECTED,
CLOSING,
CLOSE_UNKNOWN,
PARTIALLY_CLOSED,
CLOSED,
FAILED_SAFE,
FAILED_UNPROTECTED,
MANUAL_INTERVENTION_REQUIRED,
}
assert set(TRANSITIONS) == required
assert set().union(*TRANSITIONS.values()) <= required
service_path = Path(__file__).parents[1] / "trading" / "domain" / "execution_service.py"
tree = ast.parse(service_path.read_text())
transition_names = {
call.args[1].id
for call in ast.walk(tree)
if isinstance(call, ast.Call)
and isinstance(call.func, ast.Name)
and call.func.id == "transition_execution"
and len(call.args) >= 2
and isinstance(call.args[1], ast.Name)
}
assert transition_names <= required
assert {
VALIDATING,
LOCKED,
CONFIGURING_ACCOUNT,
SUBMITTING_ENTRY,
ENTRY_UNKNOWN,
ENTRY_OPEN,
ENTRY_PARTIALLY_FILLED,
ENTRY_FILLED,
PLACING_PROTECTION,
PROTECTION_PARTIAL,
PROTECTED,
CLOSING,
CLOSE_UNKNOWN,
PARTIALLY_CLOSED,
CLOSED,
FAILED_SAFE,
FAILED_UNPROTECTED,
} <= transition_names
def test_transition_is_atomic_and_records_actor_reason_and_payload():
execution_id = _begin()
assert transition_execution(
execution_id,
VALIDATING,
actor="operator-1",
reason="validated_snapshot",
payload={"risk": "accepted"},
) == VALIDATING
row = _history(execution_id)
assert row[0] == VALIDATING
assert int(row[1]) == 2
events = execution_events(execution_id)
assert [(event["from_state"], event["to_state"]) for event in events] == [
(None, CREATED),
(CREATED, VALIDATING),
]
assert events[-1]["actor"] == "operator-1"
assert events[-1]["reason"] == "validated_snapshot"
assert '"risk": "accepted"' in events[-1]["payload"]
def test_illegal_transition_rejection_keeps_prior_durable_state():
execution_id = _begin()
with pytest.raises(InvalidExecutionTransition, match="not allowed"):
transition_execution(
execution_id,
PROTECTED,
actor="operator-1",
reason="illegal_skip",
)
row = _history(execution_id)
assert row[0] == CREATED
assert int(row[1]) == 1
assert [event["to_state"] for event in execution_events(execution_id)] == [CREATED]
def test_crash_at_event_boundary_rolls_back_state_and_event_together():
execution_id = _begin()
conn = connect()
try:
conn.execute(
"CREATE TRIGGER abort_crash_event BEFORE INSERT ON execution_events "
"WHEN NEW.reason='crash_boundary' BEGIN "
"SELECT RAISE(ABORT, 'simulated transition crash'); END"
)
conn.commit()
finally:
conn.close()
with pytest.raises(Exception, match="simulated transition crash"):
transition_execution(
execution_id,
VALIDATING,
actor="operator-1",
reason="crash_boundary",
)
row = _history(execution_id)
assert row[0] == CREATED
assert int(row[1]) == 1
assert [event["to_state"] for event in execution_events(execution_id)] == [CREATED]
def test_finish_execution_cannot_bypass_a_missing_transition_event():
execution_id = _begin()
with pytest.raises(InvalidExecutionTransition, match="cannot bypass"):
finish_execution(execution_id, state=PROTECTED, result_ref="order-1")
row = _history(execution_id)
assert row[0] == CREATED
assert row[2] is None
transition_execution(
execution_id,
FAILED_SAFE,
actor="operator-1",
reason="safe_terminal_failure",
)
finish_execution(execution_id, state=FAILED_SAFE, error=RuntimeError("safe failure"))
row = _history(execution_id)
assert row[0] == FAILED_SAFE
assert row[2] is not None
def test_representative_recovery_paths_follow_declared_edges():
entry_path = [
CREATED,
VALIDATING,
LOCKED,
CONFIGURING_ACCOUNT,
SUBMITTING_ENTRY,
ENTRY_FILLED,
PLACING_PROTECTION,
PROTECTED,
CLOSING,
CLOSED,
]
for current, following in zip(entry_path, entry_path[1:]):
assert transition(current, following) == following
recovery_path = [
ENTRY_UNKNOWN,
MANUAL_INTERVENTION_REQUIRED,
CLOSING,
CLOSE_UNKNOWN,
PARTIALLY_CLOSED,
CLOSING,
CLOSED,
]
for current, following in zip(recovery_path, recovery_path[1:]):
assert transition(current, following) == following
assert transition(ENTRY_PARTIALLY_FILLED, FAILED_UNPROTECTED) == FAILED_UNPROTECTED
assert transition(FAILED_UNPROTECTED, PROTECTED) == PROTECTED
assert transition(PLACING_PROTECTION, PROTECTION_PARTIAL) == PROTECTION_PARTIAL