fix strategy state persistence corrections

This commit is contained in:
Hermes Agent
2026-08-29 16:11:02 +09:00
parent 4699de61cc
commit 3854aabd0d
11 changed files with 190 additions and 85 deletions
@@ -28,6 +28,7 @@ _EXPECTED_TABLES = {
"broker_account_snapshot", # dcy-041 / GitHub #45
"ledger_skip", # GitHub #68 skipped-ledger durable audit
"reconcile_notification_state", # req-0007 / ac-0024
"slot_strategy_state", # req-0009 / ac-0027
}
@@ -59,6 +60,25 @@ def test_daily_slot_columns_match_expected_contract(pg_clean):
assert actual == set(EXPECTED_SLOT_FIELDS)
def test_strategy_state_schema_matches_instance_map_contract(pg_clean):
with psycopg.connect(pg_clean) as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT column_name, data_type, udt_name "
"FROM information_schema.columns "
"WHERE table_schema = 'public' AND table_name = 'slot_strategy_state'"
)
columns = {row[0]: (row[1], row[2]) for row in cur.fetchall()}
cur.execute(
"SELECT a.attname FROM pg_index i "
"JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) "
"WHERE i.indrelid = 'slot_strategy_state'::regclass AND i.indisprimary"
)
primary_key = {row[0] for row in cur.fetchall()}
assert columns == {"slot_id": ("uuid", "uuid"), "data": ("jsonb", "jsonb")}
assert primary_key == {"slot_id"}
def test_drift_detected_when_contract_extended(pg_clean):
"""Negative case: a contract field absent from the table is caught."""
actual = _columns(pg_clean, "daily_slot")
@@ -164,7 +164,7 @@ def test_strategy_state_store_pg(pg_clean):
repo = PostgreSQLSlotRepository(pg_clean)
store = PgStrategyStateStore(pg_clean)
svc = SlotService(repo, instance_key=INST, strategy_state_store=store)
svc = SlotService(repo, instance_key=INST)
repo.reconcile_universe(INST, {"pf-a": "005930"})
slot = repo.find_slot_by_portfolio(INST, "pf-a")
svc.transit_slot(slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
@@ -189,12 +189,58 @@ def test_strategy_state_store_pg(pg_clean):
))
assert all(write.ok for write in writes)
assert {"instance-c", "instance-d"} <= set(store.get_for_slots([slot.slot_id])[slot.slot_id])
assert store.get_for_slots([slot.slot_id])[slot.slot_id]["instance-a"].version == 1
# Separate connections race on the same entry/version: only one CAS wins.
with ThreadPoolExecutor(max_workers=2) as executor:
competing = list(executor.map(
lambda data: store.set(
slot.slot_id, "instance-a", expected_version=1, data=data, slot_active=True,
),
("winner-one", "winner-two"),
))
assert sum(result.ok for result in competing) == 1, competing
assert sum(result.reason == ss.REASON_VERSION_CONFLICT for result in competing) == 1
winner = next(data for data, result in zip(("winner-one", "winner-two"), competing) if result.ok)
assert store.get_for_slots([slot.slot_id])[slot.slot_id]["instance-a"].data == winner
assert store.get_for_slots([slot.slot_id])[slot.slot_id]["instance-a"].version == 2
# A fresh adapter sees the byte-identical persisted entry and can continue CAS.
before_restart = store.get_for_slots([slot.slot_id])[slot.slot_id]["instance-a"]
after_restart = PgStrategyStateStore(pg_clean).get_for_slots([slot.slot_id])[slot.slot_id]["instance-a"]
assert after_restart == before_restart
svc.transit_slot(slot.slot_id, SlotState.HOLDING, SlotState.PENDING_SELL,
reason="sell", actor="t")
svc.transit_slot(slot.slot_id, SlotState.PENDING_SELL, SlotState.INACTIVE,
reason="sold", actor="t")
assert store.get_for_slots([slot.slot_id])[slot.slot_id]["instance-a"].version == 1
assert store.get_for_slots([slot.slot_id])[slot.slot_id]["instance-a"].version == 2
reused = svc.transit_slot(slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="buy", actor="t")
reason="buy", actor="t")
assert reused.occupancy_seq == 2
rehydrated = PgStrategyStateStore(pg_clean)
persisted = rehydrated.get_for_slots([slot.slot_id])[slot.slot_id]["instance-a"]
assert persisted == before_restart
assert rehydrated.set(
slot.slot_id, "instance-a", expected_version=2, data="continued", slot_active=True,
).version == 3
def test_strategy_state_write_cannot_follow_close(pg_clean):
"""The store's locked lifecycle read rejects a write after close wins."""
from trex_core.adapters.persistence.postgresql import PgStrategyStateStore
repo = PostgreSQLSlotRepository(pg_clean)
svc = SlotService(repo, instance_key=INST)
store = PgStrategyStateStore(pg_clean)
repo.reconcile_universe(INST, {"pf-close": "005930"})
slot = repo.find_slot_by_portfolio(INST, "pf-close")
svc.transit_slot(slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY, reason="buy", actor="t")
svc.transit_slot(slot.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING, reason="fill", actor="t", qty=1)
assert store.set(slot.slot_id, "instance", expected_version=0, data="before", slot_active=True).ok
svc.transit_slot(slot.slot_id, SlotState.HOLDING, SlotState.PENDING_SELL, reason="sell", actor="t")
svc.transit_slot(slot.slot_id, SlotState.PENDING_SELL, SlotState.INACTIVE, reason="sold", actor="t")
result = store.set(slot.slot_id, "instance", expected_version=1, data="after", slot_active=True)
assert not result.ok and result.reason == "slot_inactive"
assert store.get_for_slots([slot.slot_id])[slot.slot_id]["instance"].data == "before"