test(reconciliation): verify repository lookup parity
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
| --- | --- | --- |
|
||||
| REQ-119 — fetched provider parity, no silent fallback, break-glass/env-only compatibility, safe source identity | Focused provider/CLI regression coverage passed under the in-process `pika` stub; changed-file lint/diff and secret scan passed. | Independent QC must rerun the PR #42 evidence and assess the blocked subprocess case in an environment with `pika` available. No live broker evidence exists. |
|
||||
| REQ-120 — fail-loud balance failures, nonzero/no-write barrier, true-empty separation, lifecycle/redaction | Focused adapter and CLI failure-path regression coverage passed under the stub; secret scan passed. | Independent QC must rerun failure/no-write and true-empty evidence. The unavailable subprocess prevents a complete isolated CLI-process confirmation. |
|
||||
| REQ-121 — four-case divergence classification, instance isolation, no auto-remediation, repository parity | Focused reconciliation and InMemory-oriented regression coverage passed under the stub. | **Environmental coverage gap:** no live/local PostgreSQL fixture was executed, so PostgreSQL/InMemory parity is not claimed. Independent QC must retain this gap or obtain an approved non-production PostgreSQL fixture. No live broker or database was used. |
|
||||
| REQ-121 — four-case divergence classification, instance isolation, no auto-remediation, repository parity | Focused reconciliation/InMemory regression coverage plus a fake-driver parity test passed: it executes the PostgreSQL adapter's managed-slot SQL/params and row mapping against the same fixture. | **Environmental coverage gap:** no live/local PostgreSQL fixture was executed. The fake-driver unit contract establishes adapter-query parity, not real database integration behavior. Independent QC must retain this integration gap or obtain an approved non-production PostgreSQL fixture. No live broker or database was used. |
|
||||
| REQ-122 — runtime/interface/operations alignment, safe examples, release-gated remediation | Baseline documentation changes at the PR #42 tip were included in changed-file diff checks; documentation secret scan passed. | Independent QC must cross-check the rendered baseline text, actual CLI/help and decision-code behavior against the committed PR diff. This artifact does not authorize a production command or deployment. |
|
||||
|
||||
## Draft Validation Results
|
||||
@@ -31,7 +31,7 @@
|
||||
| `git diff --check` | PASS | Recorded implementation-lane validation. |
|
||||
| Documentation secret scan | PASS | No credentials, tokens, raw account numbers, or raw credential payloads in scanned documentation. |
|
||||
| `test_config_required_cli_exits_nonzero` subprocess | BLOCKED (environment) | Isolated worktree lacks `pika`; subprocess cannot inherit the in-process stub. |
|
||||
| PostgreSQL repository parity/live database coverage | NOT EXECUTED (environmental coverage gap) | Do not infer PostgreSQL parity from InMemory-focused evidence. |
|
||||
| PostgreSQL repository parity/live database coverage | FAKE-DRIVER PARITY PASS; live database NOT EXECUTED | The injected fake psycopg driver executed the actual adapter query/params and row mapping against the InMemory fixture. No local/live PostgreSQL integration was run, so database-engine behavior remains an environmental coverage gap. |
|
||||
|
||||
## Result and Next Gate
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
"""REQ-121 AC-5 fake-driver parity coverage for managed-slot lookup.
|
||||
|
||||
This is a unit contract test: the injected psycopg fake exercises the actual
|
||||
PostgreSQL adapter query and row mapping without requiring a PostgreSQL server.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
# This worktree lacks pika, but importing the PostgreSQL adapter traverses the
|
||||
# application alarm-emitter import path. Install only the import-time minimum;
|
||||
# this test exercises neither RabbitMQ nor a real PostgreSQL connection.
|
||||
_pika = types.ModuleType("pika")
|
||||
_pika.__path__ = []
|
||||
_pika.URLParameters = lambda *args, **kwargs: None
|
||||
_pika.BlockingConnection = object
|
||||
_pika.BasicProperties = object
|
||||
_pika_exceptions = types.ModuleType("pika.exceptions")
|
||||
_pika_exceptions.AMQPError = Exception
|
||||
_pika.exceptions = _pika_exceptions
|
||||
sys.modules.setdefault("pika", _pika)
|
||||
sys.modules.setdefault("pika.exceptions", _pika_exceptions)
|
||||
|
||||
from trex_core.adapters.persistence.postgresql import PostgreSQLSlotRepository # noqa: E402
|
||||
from trex_core.domain.slot import Slot, SlotState # noqa: E402
|
||||
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository # noqa: E402
|
||||
|
||||
|
||||
_SLOT_COLUMNS = (
|
||||
"slot_id, status, symbol, qty, avg_price, limit_price, "
|
||||
"order_id, strategy_id, broker_order_id, portfolio_id, "
|
||||
"sell_strategy_id, forced_close_at, stop_loss_price, take_profit_price, "
|
||||
"created_at, updated_at, instance_key, retired_at, "
|
||||
"timeout_sec, slippage_pct, occupancy_seq"
|
||||
)
|
||||
_MANAGED_SLOT_SQL = (
|
||||
f"SELECT {_SLOT_COLUMNS} FROM daily_slot "
|
||||
"WHERE instance_key = %s AND symbol = %s "
|
||||
"ORDER BY updated_at DESC NULLS LAST, slot_id ASC LIMIT 1"
|
||||
)
|
||||
|
||||
|
||||
class _FakeCursor:
|
||||
def __init__(self, rows: list[tuple], executions: list[tuple[str, tuple[str, str]]]) -> None:
|
||||
self._rows = rows
|
||||
self._executions = executions
|
||||
self._row: tuple | None = None
|
||||
|
||||
def __enter__(self) -> _FakeCursor:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
def execute(self, query: str, params: tuple[str, str]) -> None:
|
||||
self._executions.append((query, params))
|
||||
instance_key, symbol = params
|
||||
candidates = [
|
||||
row for row in self._rows if row[16] == instance_key and row[2] == symbol
|
||||
]
|
||||
candidates.sort(key=lambda row: (
|
||||
row[15] is None,
|
||||
-row[15].timestamp() if row[15] is not None else 0,
|
||||
str(UUID(str(row[0]))),
|
||||
))
|
||||
self._row = candidates[0] if candidates else None
|
||||
|
||||
def fetchone(self) -> tuple | None:
|
||||
return self._row
|
||||
|
||||
|
||||
class _FakeConnection:
|
||||
def __init__(self, rows: list[tuple], executions: list[tuple[str, tuple[str, str]]]) -> None:
|
||||
self._rows = rows
|
||||
self._executions = executions
|
||||
|
||||
def __enter__(self) -> _FakeConnection:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
def cursor(self) -> _FakeCursor:
|
||||
return _FakeCursor(self._rows, self._executions)
|
||||
|
||||
|
||||
class _FakePsycopg:
|
||||
def __init__(self, rows: list[tuple]) -> None:
|
||||
self._rows = rows
|
||||
self.connect_calls: list[tuple[str, dict[str, object]]] = []
|
||||
self.executions: list[tuple[str, tuple[str, str]]] = []
|
||||
|
||||
def connect(self, dsn: str, **kwargs: object) -> _FakeConnection:
|
||||
self.connect_calls.append((dsn, kwargs))
|
||||
return _FakeConnection(self._rows, self.executions)
|
||||
|
||||
|
||||
def _slot(slot_id: str, instance_key: str, symbol: str, updated_at: datetime | None) -> Slot:
|
||||
return Slot(
|
||||
slot_id=UUID(slot_id),
|
||||
status=SlotState.HOLDING,
|
||||
symbol=symbol,
|
||||
qty=10.5,
|
||||
avg_price=101.25,
|
||||
limit_price=100.0,
|
||||
order_id="order-1",
|
||||
strategy_id="buy-v1",
|
||||
broker_order_id="broker-1",
|
||||
portfolio_id="portfolio-1",
|
||||
sell_strategy_id="sell-v1",
|
||||
created_at=datetime(2026, 7, 14, tzinfo=timezone.utc),
|
||||
updated_at=updated_at,
|
||||
instance_key=instance_key,
|
||||
timeout_sec=30,
|
||||
slippage_pct=1.5,
|
||||
occupancy_seq=3,
|
||||
)
|
||||
|
||||
|
||||
def _row(slot: Slot) -> tuple:
|
||||
"""Return a driver-shaped row, including values converted by _row_to_slot."""
|
||||
return (
|
||||
str(slot.slot_id),
|
||||
slot.status.value,
|
||||
slot.symbol,
|
||||
Decimal(str(slot.qty)),
|
||||
Decimal(str(slot.avg_price)),
|
||||
Decimal(str(slot.limit_price)),
|
||||
slot.order_id,
|
||||
slot.strategy_id,
|
||||
slot.broker_order_id,
|
||||
slot.portfolio_id,
|
||||
slot.sell_strategy_id,
|
||||
slot.forced_close_at,
|
||||
None,
|
||||
None,
|
||||
slot.created_at,
|
||||
slot.updated_at,
|
||||
slot.instance_key,
|
||||
slot.retired_at,
|
||||
slot.timeout_sec,
|
||||
Decimal(str(slot.slippage_pct)),
|
||||
slot.occupancy_seq,
|
||||
)
|
||||
|
||||
|
||||
def test_find_managed_slot_by_symbol_postgresql_inmemory_parity_with_fake_driver():
|
||||
newest = datetime(2026, 7, 15, 12, tzinfo=timezone.utc)
|
||||
older = datetime(2026, 7, 15, 11, tzinfo=timezone.utc)
|
||||
slots = [
|
||||
# Same newest timestamp: UUID ASC must pick the first fixture slot.
|
||||
_slot("00000000-0000-0000-0000-000000000001", "instance-a", "AAA", newest),
|
||||
_slot("00000000-0000-0000-0000-000000000002", "instance-a", "AAA", newest),
|
||||
_slot("00000000-0000-0000-0000-000000000003", "instance-a", "AAA", older),
|
||||
_slot("00000000-0000-0000-0000-000000000004", "instance-a", "AAA", None),
|
||||
# Same symbol in a different instance must never leak into instance-a.
|
||||
_slot("00000000-0000-0000-0000-000000000005", "instance-b", "AAA", newest),
|
||||
# Same instance with another symbol must not satisfy an AAA query.
|
||||
_slot("00000000-0000-0000-0000-000000000006", "instance-a", "BBB", newest),
|
||||
]
|
||||
in_memory = InMemorySlotRepository()
|
||||
for slot in slots:
|
||||
in_memory.add_slot(slot)
|
||||
|
||||
fake_psycopg = _FakePsycopg([_row(slot) for slot in slots])
|
||||
postgresql = PostgreSQLSlotRepository("postgresql://fake", psycopg_module=fake_psycopg)
|
||||
lookups = [
|
||||
("instance-a", "AAA"),
|
||||
("instance-b", "AAA"),
|
||||
("instance-a", "BBB"),
|
||||
("instance-a", "missing"),
|
||||
("missing-instance", "AAA"),
|
||||
]
|
||||
|
||||
for instance_key, symbol in lookups:
|
||||
postgres_slot = postgresql.find_managed_slot_by_symbol(instance_key, symbol)
|
||||
in_memory_slot = in_memory.find_managed_slot_by_symbol(instance_key, symbol)
|
||||
assert (postgres_slot.slot_id if postgres_slot else None) == (
|
||||
in_memory_slot.slot_id if in_memory_slot else None
|
||||
)
|
||||
|
||||
selected = postgresql.find_managed_slot_by_symbol("instance-a", "AAA")
|
||||
assert selected is not None
|
||||
assert selected.slot_id == UUID("00000000-0000-0000-0000-000000000001")
|
||||
assert selected.qty == 10.5
|
||||
assert selected.avg_price == 101.25
|
||||
assert selected.slippage_pct == 1.5
|
||||
assert selected.occupancy_seq == 3
|
||||
|
||||
expected_executions = [
|
||||
(_MANAGED_SLOT_SQL, lookup) for lookup in [*lookups, ("instance-a", "AAA")]
|
||||
]
|
||||
assert fake_psycopg.executions == expected_executions
|
||||
assert fake_psycopg.connect_calls == [("postgresql://fake", {})] * len(expected_executions)
|
||||
Reference in New Issue
Block a user