fix: harden unmanaged position lifecycle writes

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-12 13:31:32 +00:00
committed by GitHub
co-authored by genichin
parent 5a13c0494b
commit be2e73a2d6
2 changed files with 130 additions and 76 deletions
+47
View File
@@ -741,6 +741,53 @@ def test_unmanaged_record_text_output(monkeypatch, capsys):
assert "evidence_ref=kis-balance:2026-09-11" in out assert "evidence_ref=kis-balance:2026-09-11" in out
def test_unmanaged_record_main_dispatch(monkeypatch, capsys):
class _Store:
def __init__(self, *_args, **_kwargs):
pass
def record_operator_action(self, **kwargs):
return {
"instance_key": "default",
"symbol": kwargs["symbol"],
"quantity": 3,
"lifecycle_state": "active",
"first_seen_at": "2026-09-07T00:00:00+00:00",
"last_seen_at": "2026-09-11T00:00:00+00:00",
"quantity_changed_at": "2026-09-07T00:00:00+00:00",
"resolved_at": None,
"operator_action": kwargs["action"],
"evidence_ref": kwargs["evidence_ref"],
"operator_note": kwargs["note"],
"operator_actor": kwargs["actor"],
"operator_updated_at": "2026-09-11T00:00:00+00:00",
"created_at": "2026-09-07T00:00:00+00:00",
"updated_at": "2026-09-11T00:00:00+00:00",
}
monkeypatch.setattr(
"trex_core.adapters.portfolio.unmanaged_position_store.PostgreSQLUnmanagedPositionStore",
_Store,
)
rc = cli.main(
[
"unmanaged",
"record",
"008110",
"investigating",
"--evidence-ref",
"kis-balance:2026-09-11",
"--json",
]
)
assert rc == 0
out = json.loads(capsys.readouterr().out)
assert out["result"] == "recorded"
assert out["state"]["symbol"] == "008110"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# REL-010 regression: _cmd_verify broker import must not raise ImportError # REL-010 regression: _cmd_verify broker import must not raise ImportError
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -31,82 +31,89 @@ class PostgreSQLUnmanagedPositionStore:
actor: str = "reconcile", actor: str = "reconcile",
) -> dict: ) -> dict:
now = observed_at or datetime.now(tz=UTC) now = observed_at or datetime.now(tz=UTC)
with self._psycopg.connect(self._dsn, autocommit=False) as conn, conn.cursor() as cur: for _attempt in range(2):
cur.execute( try:
f"SELECT {_STATE_COLUMNS} FROM unmanaged_position_state " with self._psycopg.connect(self._dsn, autocommit=False) as conn, conn.cursor() as cur:
"WHERE instance_key = %s AND symbol = %s " cur.execute(
"FOR UPDATE", f"SELECT {_STATE_COLUMNS} FROM unmanaged_position_state "
(instance_key, symbol), "WHERE instance_key = %s AND symbol = %s "
) "FOR UPDATE",
row = cur.fetchone() (instance_key, symbol),
previous = self._row_to_state(row) if row is not None else None )
previous_quantity = previous.get("quantity") if previous is not None else None row = cur.fetchone()
if previous is None: previous = self._row_to_state(row) if row is not None else None
transition = "detected" previous_quantity = previous.get("quantity") if previous is not None else None
cur.execute( if previous is None:
"INSERT INTO unmanaged_position_state " transition = "detected"
f"({_STATE_COLUMNS}) " cur.execute(
"VALUES (%s, %s, %s, 'active', %s, %s, %s, NULL, " "INSERT INTO unmanaged_position_state "
" NULL, NULL, '', NULL, NULL, %s, %s) " f"({_STATE_COLUMNS}) "
f"RETURNING {_STATE_COLUMNS}", "VALUES (%s, %s, %s, 'active', %s, %s, %s, NULL, "
(instance_key, symbol, quantity, now, now, now, now, now), " NULL, NULL, '', NULL, NULL, %s, %s) "
) f"RETURNING {_STATE_COLUMNS}",
elif previous["lifecycle_state"] == "resolved": (instance_key, symbol, quantity, now, now, now, now, now),
transition = "detected" )
cur.execute( elif previous["lifecycle_state"] == "resolved":
"UPDATE unmanaged_position_state " transition = "detected"
"SET quantity = %s, lifecycle_state = 'active', " cur.execute(
" first_seen_at = %s, last_seen_at = %s, quantity_changed_at = %s, " "UPDATE unmanaged_position_state "
" resolved_at = NULL, updated_at = %s " "SET quantity = %s, lifecycle_state = 'active', "
"WHERE instance_key = %s AND symbol = %s " " first_seen_at = %s, last_seen_at = %s, quantity_changed_at = %s, "
f"RETURNING {_STATE_COLUMNS}", " resolved_at = NULL, updated_at = %s "
(quantity, now, now, now, now, instance_key, symbol), "WHERE instance_key = %s AND symbol = %s "
) f"RETURNING {_STATE_COLUMNS}",
elif int(previous["quantity"]) != int(quantity): (quantity, now, now, now, now, instance_key, symbol),
transition = "quantity_changed" )
cur.execute( elif int(previous["quantity"]) != int(quantity):
"UPDATE unmanaged_position_state " transition = "quantity_changed"
"SET quantity = %s, last_seen_at = %s, quantity_changed_at = %s, " cur.execute(
" resolved_at = NULL, lifecycle_state = 'active', updated_at = %s " "UPDATE unmanaged_position_state "
"WHERE instance_key = %s AND symbol = %s " "SET quantity = %s, last_seen_at = %s, quantity_changed_at = %s, "
f"RETURNING {_STATE_COLUMNS}", " resolved_at = NULL, lifecycle_state = 'active', updated_at = %s "
(quantity, now, now, now, instance_key, symbol), "WHERE instance_key = %s AND symbol = %s "
) f"RETURNING {_STATE_COLUMNS}",
else: (quantity, now, now, now, instance_key, symbol),
transition = "unchanged" )
cur.execute( else:
"UPDATE unmanaged_position_state " transition = "unchanged"
"SET last_seen_at = %s, updated_at = %s " cur.execute(
"WHERE instance_key = %s AND symbol = %s " "UPDATE unmanaged_position_state "
f"RETURNING {_STATE_COLUMNS}", "SET last_seen_at = %s, updated_at = %s "
(now, now, instance_key, symbol), "WHERE instance_key = %s AND symbol = %s "
) f"RETURNING {_STATE_COLUMNS}",
state = self._row_to_state(cur.fetchone()) (now, now, instance_key, symbol),
if transition != "unchanged": )
note = ( state = self._row_to_state(cur.fetchone())
f"previous_quantity={previous_quantity}" if transition != "unchanged":
if transition == "quantity_changed" and previous_quantity is not None note = (
else "" f"previous_quantity={previous_quantity}"
) if transition == "quantity_changed" and previous_quantity is not None
self._insert_audit( else ""
cur, )
instance_key=instance_key, self._insert_audit(
symbol=symbol, cur,
event_type=transition, instance_key=instance_key,
action=None, symbol=symbol,
quantity=quantity, event_type=transition,
lifecycle_state=state["lifecycle_state"], action=None,
actor=actor, quantity=quantity,
evidence_ref=state.get("evidence_ref"), lifecycle_state=state["lifecycle_state"],
note=note, actor=actor,
created_at=now, evidence_ref=state.get("evidence_ref"),
) note=note,
conn.commit() created_at=now,
return { )
"transition": transition, conn.commit()
"previous_quantity": previous_quantity, return {
"state": state, "transition": transition,
} "previous_quantity": previous_quantity,
"state": state,
}
except self._psycopg.errors.UniqueViolation:
continue
raise RuntimeError(
"failed to persist unmanaged_position_state due to repeated concurrent insert race"
)
def resolve_absent( def resolve_absent(
self, self,