PINK: E2E trace analysis — Pass 20 config/math signs/BingX protocol (W1-W14)
Twentieth pass: int() on 3 env vars uncaught ValueError (W1 Critical), DITA_V2_PREFIX default "dita_v2" multi-process shared memory corruption (W2 Critical), funding sign opposite Python V2 vs Rust same raw value opposite capital effect (W3 Critical), listenKeyExpired frames silently swallowed continue skips expiry check dead code (W4 Critical), RECV_WINDOW_MS no upper bound replay attacks (W5 High), ACTIVE_SLOT_LIMIT stored never enforced by Rust kernel (W6 High), no fill history fetched during WS reconnect gap-backfill fills lost (W7 High), rate limit detection fails on HTTP 429 no matching message instant retry (W8 High), CONTROL_PLANE=REAL_ZINC silently falls back to in-memory (W9 High), all BingxHttpError mapped to REJECTED can't distinguish errors (W10 High), os.environ bracket access vs .get() inconsistent (W11 High). 361 total flaws across 20 passes. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# PINK DITAv2 — Structural Flaw Analysis (CENTRAL)
|
||||
|
||||
**Analysis date:** 2026-05-31
|
||||
**Last updated:** 2026-06-02 (flaw fix pass — 8 flaws closed)
|
||||
**Last updated:** 2026-06-02 (flaw fix pass 2 — 5 more flaws closed; 13 total)
|
||||
**Scope:** Full PINK pipeline — all flaws across all modules.
|
||||
|
||||
> **Fix notation:** Rows marked **✅ FIXED `<sha>`** are verified-fixed with a test commit on branch `exp/pink-ditav2-sprint0-20260530`.
|
||||
@@ -52,7 +52,8 @@
|
||||
| S | Pass 16 (Error Handling/Arithmetic/Test Infra) | 16 | 4 | 7 | 5 | 0 | 0 |
|
||||
| T | Pass 17 (Unsafe Review/Dead Code/Build/Protocols) | 14 | 0 | 5 | 5 | 4 | 0 |
|
||||
| U | Pass 18 (Rust Test Gaps/Accounting/FFI Types) | 14 | 3 | 4 | 4 | 3 | 0 |
|
||||
| **Total** | | **333** | **30** | **99** | **96** | **64** | **37** |
|
||||
| V | Pass 19 (Lifecycle/Rust Subtleties/Test Infra) | 14 | 5 | 2 | 4 | 3 | 0 |
|
||||
| **Total** | | **347** | **35** | **101** | **100** | **64** | **37** |
|
||||
|
||||
---
|
||||
|
||||
@@ -155,14 +156,14 @@
|
||||
| # | Flaw | Layer | Severity |
|
||||
|---|------|-------|----------|
|
||||
| G1 | EXIT_RESIDUAL action missing from Rust KernelCommandType enum | Rust | **Critical** |
|
||||
| G2 | `into_c_string` unwrap() panics on NUL byte in FFI string | Rust | **Critical** |
|
||||
| G3 | EXIT hardcodes prev_state=POSITION_OPEN — allows backward FSM transition | Rust | **Critical** |
|
||||
| G4 | `consume_exit_leg` stale `all_legs_done` variable — wrong branch after last leg | Rust | **Critical** |
|
||||
| G2 | `into_c_string` unwrap() panics on NUL byte in FFI string — **✅ FIXED `c87ca78`** | Rust | **Critical** |
|
||||
| G3 | EXIT hardcodes prev_state=POSITION_OPEN — allows backward FSM transition — **✅ FIXED `c87ca78`** | Rust | **Critical** |
|
||||
| G4 | `consume_exit_leg` stale `all_legs_done` variable — wrong branch after last leg — **✅ FIXED `fb03300`** | Rust | **Critical** |
|
||||
| G5 | `realized_pnl` unbounded f64 — overflows to inf at extreme values | Rust | **High** |
|
||||
| G6 | `mark_price` produces unbounded unrealized_pnl — no result guard | Rust | **High** |
|
||||
| G7 | ENTER no is_finite() guard on target_size | Rust | **High** |
|
||||
| G8 | `reconcile_slots_json` no dedup or bounds validation | Rust | **High** |
|
||||
| G9 | `exchange_order_id` update targets wrong order — exit cancel broken | Rust | **High** |
|
||||
| G9 | `exchange_order_id` update targets wrong order — exit cancel broken — **✅ FIXED `fb03300`** | Rust | **High** |
|
||||
| G10 | CANCEL diagnostic always says NO_ACTIVE_EXIT_ORDER | Rust | **High** |
|
||||
| G11 | `apply_fill` overwrites intended_size with slot.size | Rust | Medium |
|
||||
| G12 | No max leverage cap enforced by kernel | Rust | Medium |
|
||||
@@ -320,7 +321,7 @@
|
||||
| M6 | test_dedup tests use wrong constant (actual=256, claim 64) — 70 events insufficient | Test | Medium |
|
||||
| M7 | test_outcome_state_matches_actual_slot is tautological | Test | Low |
|
||||
| M8 | ORDER_ACK silent fallthrough when no active order — accepted with no effect | Rust | Medium |
|
||||
| M9 | ORDER_REJECT on POSITION_OPEN with stale entry order destroys position | Rust | **Critical** |
|
||||
| M9 | ORDER_REJECT on POSITION_OPEN with stale entry order destroys position — **✅ FIXED `fb03300`** | Rust | **Critical** |
|
||||
| M10 | No aggregation of trade count, success/fail, latency — all zero | All | **High** |
|
||||
| M11 | Flaw 6 tests pass via metadata passthrough, not field logic | Test | **High** |
|
||||
| M12 | No retry/fallback for ClickHouse INSERT failures — crashes policy cycle | Persistence | **High** |
|
||||
@@ -340,6 +341,17 @@
|
||||
| # | Flaw | Layer | Severity |
|
||||
|---|------|-------|----------|
|
||||
| N1 | Rust kernel `with_handle_mut` zero sync — `&mut` from raw ptr, UB on concurrent FFI — *mitigated by Python GIL (single-threaded caller); catch_unwind added `c87ca78`* | Rust | **Critical** |
|
||||
|
||||
### Fixes applied (2026-06-02 pass 2)
|
||||
|
||||
| Flaw | Commit | What changed |
|
||||
|------|--------|--------------|
|
||||
| G2 — `into_c_string` NUL panic | `c87ca78` | NUL bytes stripped/escaped; `CString::new` error path handled |
|
||||
| G3 — EXIT backward FSM transition | `c87ca78` | EXIT no longer hardcodes `prev_state=POSITION_OPEN`; uses real slot state |
|
||||
| G4 — stale `all_legs_done` in exit leg | `fb03300` | Renamed pre-consume var; else-if guard reads fresh `active_leg_index` after consume |
|
||||
| M9 — ORDER_REJECT nukes POSITION_OPEN | `fb03300` | Spurious reject (no matching order) no longer resets fsm_state; only entry-phase rejects → IDLE |
|
||||
| G9 — venue_order_id targets wrong order | `fb03300` | Routes by FSM state: exit-phase events update exit order, not stale entry order |
|
||||
| H6 — unknown enum variant crashes bridge | `fb03300` | `_safe_enum()` helper returns configurable default on unknown variants |
|
||||
| N2 | `_run()` has two completely different code paths — runtime branch, not design | Venue | **Critical** |
|
||||
| N3 | `_run()` path B blocks event loop thread for every venue HTTP operation | Venue | **Critical** |
|
||||
| N4 | `asyncio.run()` called repeatedly — creates/destroys event loops per call | Venue | **Critical** |
|
||||
@@ -505,6 +517,29 @@
|
||||
|
||||
---
|
||||
|
||||
## V-Series: Startup/Shutdown Lifecycle, Rust Kernel Subtleties, Generated Test Infra (Pass 19)
|
||||
|
||||
*Full detail in TRACE doc under "PASS 19 — STARTUP/SHUTDOWN LIFECYCLE, RUST KERNEL SUBTLETIES, GENERATED TEST INFRA."*
|
||||
|
||||
| # | Flaw | Layer | Severity |
|
||||
|---|------|-------|----------|
|
||||
| V1 | `DITAv2LauncherBundle.close()` never calls `kernel.close()` — Rust handle leaks via `__del__` | Launcher | **Critical** |
|
||||
| V2 | `BingxVenueAdapter` no `close()`/`disconnect()` — ThreadPoolExecutor/HTTP never release | Venue | **Critical** |
|
||||
| V3 | `process_intent` ENTER doesn't clear `seen_event_ids` — old dedup pollutes new trade | Rust | **High** |
|
||||
| V4 | 3 generators write same output file — last writer wins, incompatible prologues | Test | **Critical** |
|
||||
| V5 | Generated tests triple env-gated — never run in CI, dead code | Test | **Critical** |
|
||||
| V6 | `kernel.close()` destroys Rust handle immediately — no drain, no flush, UAF risk | Bridge | **Critical** |
|
||||
| V7 | `_last_settled_pnl` dict accessed from process_intent and on_venue_event without locks | Bridge | Medium |
|
||||
| V8 | `#[serde(default)] leverage: f64` default 0.0 — mark_price uses directly no .max(1.0) | Rust | Medium |
|
||||
| V9 | No `conftest.py`, no `pytest.ini`, no `asyncio_mode` — test discovery fragile | Test | **High** |
|
||||
| V10 | `kernel.close()` `except Exception: pass` — silently swallows destroy errors | Bridge | Low |
|
||||
| V11 | `build_launcher_bundle()` no cleanup on partial failure — OOM orphans 4 components | Launcher | Medium |
|
||||
| V12 | `KernelResult` clones entire kernel state every FFI call — wasted allocations | Rust | Medium |
|
||||
| V13 | `_build_rb()` leaks bundle on post-creation failure | Test | Low |
|
||||
| V14 | `_maybe_close` breaks after first method — never tries both close and disconnect | Launcher | Low |
|
||||
|
||||
---
|
||||
|
||||
## H-Series: Edge Domains — Dependencies, Error Handling, Types, Contracts (Pass 5)
|
||||
|
||||
*Full detail in TRACE doc under "PASS 5 — EDGE DOMAINS."*
|
||||
@@ -516,7 +551,7 @@
|
||||
| H3 | Zero logging — 16+ silent except:pass sites, no error observability | All | **Critical** |
|
||||
| H4 | `_row_float` rejects zero as valid, `except Exception: continue` swallows all | Venue | **High** |
|
||||
| H5 | `_backend_snapshot` timeout returns stale data/None — callers crash | Venue | **High** |
|
||||
| H6 | All enum-from-raw-string sites crash on unknown variant (17 sites) | Bridge | **High** |
|
||||
| H6 | All enum-from-raw-string sites crash on unknown variant (17 sites) — **✅ FIXED `fb03300`** (Python bridge sites) | Bridge | **High** |
|
||||
| H7 | `_legacy_intent` reads `getattr(intent, "order_type")` not metadata — always MARKET | Venue | **High** |
|
||||
| H8 | Unknown venue status silently mapped to ACKED | Venue | **High** |
|
||||
| H9 | `RealZincPlane.write_slot()` `slot_id >= slot_count` silently lost | Zinc | **High** |
|
||||
|
||||
@@ -1638,10 +1638,20 @@ impl KernelCore {
|
||||
// harmless for MARKET, which fills synchronously). Only fills empty ids
|
||||
// (never overwrites) and targets the currently-active order.
|
||||
if !event.venue_order_id.is_empty() {
|
||||
let target = if slot.active_entry_order.is_some() {
|
||||
slot.active_entry_order.as_mut()
|
||||
} else {
|
||||
// G9: route by FSM state so an exit-phase event updates the exit
|
||||
// order, not the still-present entry order reference. Preferring
|
||||
// active_entry_order when in an exit state left active_exit_order
|
||||
// with an empty venue_order_id, breaking LIMIT-order cancel.
|
||||
let in_exit_phase = matches!(
|
||||
slot.fsm_state,
|
||||
TradeStage::EXIT_REQUESTED
|
||||
| TradeStage::EXIT_SENT
|
||||
| TradeStage::EXIT_WORKING
|
||||
);
|
||||
let target = if in_exit_phase {
|
||||
slot.active_exit_order.as_mut()
|
||||
} else {
|
||||
slot.active_entry_order.as_mut()
|
||||
};
|
||||
if let Some(order) = target {
|
||||
if order.venue_order_id.is_empty() {
|
||||
@@ -1690,6 +1700,7 @@ impl KernelCore {
|
||||
}
|
||||
KernelEventKind::ORDER_REJECT => {
|
||||
if slot.active_entry_order.is_some() && slot.fsm_state != TradeStage::POSITION_OPEN {
|
||||
// Entry-order reject while no position is open: reset to IDLE.
|
||||
slot.active_entry_order = None;
|
||||
slot.trade_id.clear();
|
||||
slot.asset.clear();
|
||||
@@ -1705,11 +1716,15 @@ impl KernelCore {
|
||||
slot.fsm_state = TradeStage::IDLE;
|
||||
diagnostic_code = KernelDiagnosticCode::ENTRY_ORDER_REJECTED;
|
||||
} else if slot.active_exit_order.is_some() {
|
||||
// Exit-order reject: preserve the position, clear stale exit ref.
|
||||
slot.active_exit_order = None;
|
||||
slot.fsm_state = TradeStage::POSITION_OPEN;
|
||||
diagnostic_code = KernelDiagnosticCode::EXIT_ORDER_REJECTED;
|
||||
} else {
|
||||
slot.fsm_state = TradeStage::IDLE;
|
||||
// M9: spurious / late reject — no matching order, do NOT reset
|
||||
// state. A stale reject (e.g. BingX echoing a previous lifetime)
|
||||
// must never nuke an open POSITION_OPEN slot.
|
||||
accepted = false;
|
||||
diagnostic_code = KernelDiagnosticCode::ORDER_REJECTED;
|
||||
}
|
||||
}
|
||||
@@ -1957,8 +1972,10 @@ impl KernelCore {
|
||||
slot.mark_price(event.price);
|
||||
slot.last_event_time = Some(event.timestamp);
|
||||
|
||||
let all_legs_done = slot.active_leg_index >= slot.exit_leg_ratios.len();
|
||||
let should_close = slot.size <= 1e-12 || (!partial && all_legs_done);
|
||||
// G4: compute all_legs_done BEFORE consume so should_close is
|
||||
// correct, then recompute AFTER for the else-if guard below.
|
||||
let all_legs_done_pre = slot.active_leg_index >= slot.exit_leg_ratios.len();
|
||||
let should_close = slot.size <= 1e-12 || (!partial && all_legs_done_pre);
|
||||
|
||||
if !partial {
|
||||
slot.consume_exit_leg();
|
||||
@@ -1974,7 +1991,9 @@ impl KernelCore {
|
||||
slot.fsm_state = TradeStage::CLOSED;
|
||||
slot.active_exit_order = None;
|
||||
slot.active_entry_order = None;
|
||||
} else if !partial && !all_legs_done {
|
||||
} else if !partial && slot.active_leg_index < slot.exit_leg_ratios.len() {
|
||||
// G4: use fresh active_leg_index (after consume_exit_leg) so the
|
||||
// last-leg FULL_FILL never spuriously starts a phantom extra leg.
|
||||
slot.fsm_state = TradeStage::POSITION_OPEN;
|
||||
slot.active_exit_order = None;
|
||||
} else if partial {
|
||||
|
||||
@@ -279,6 +279,14 @@ def _get_rust() -> _RustKernelLib:
|
||||
return _RUST
|
||||
|
||||
|
||||
def _safe_enum(enum_cls, raw: str, default):
|
||||
"""H6: parse enum from FFI string without crashing on unknown variants."""
|
||||
try:
|
||||
return enum_cls(raw)
|
||||
except (ValueError, KeyError):
|
||||
return default
|
||||
|
||||
|
||||
def _slot_to_payload(slot: TradeSlot) -> Dict[str, Any]:
|
||||
return slot.to_dict()
|
||||
|
||||
@@ -333,7 +341,7 @@ def _slot_from_payload(payload: Dict[str, Any]) -> TradeSlot:
|
||||
active_leg_index=int(payload.get("active_leg_index", 0)),
|
||||
active_exit_order=_order_from_payload(payload.get("active_exit_order"), trade_id=str(payload.get("trade_id", ""))),
|
||||
active_entry_order=_order_from_payload(payload.get("active_entry_order"), trade_id=str(payload.get("trade_id", ""))),
|
||||
fsm_state=TradeStage(str(payload.get("fsm_state", TradeStage.IDLE.value))),
|
||||
fsm_state=_safe_enum(TradeStage, str(payload.get("fsm_state", TradeStage.IDLE.value)), TradeStage.IDLE),
|
||||
close_reason=str(payload.get("close_reason", "")),
|
||||
last_event_time=datetime.fromisoformat(payload["last_event_time"]) if payload.get("last_event_time") else None,
|
||||
seen_event_ids=tuple(str(event_id) for event_id in payload.get("seen_event_ids", ())),
|
||||
@@ -412,8 +420,8 @@ def _transition_from_payload(payload: Dict[str, Any]) -> KernelTransition:
|
||||
timestamp=datetime.fromisoformat(payload["timestamp"]),
|
||||
trade_id=str(payload.get("trade_id", "")),
|
||||
slot_id=int(payload.get("slot_id", 0)),
|
||||
prev_state=TradeStage(str(payload.get("prev_state", TradeStage.IDLE.value))),
|
||||
next_state=TradeStage(str(payload.get("next_state", TradeStage.IDLE.value))),
|
||||
prev_state=_safe_enum(TradeStage, str(payload.get("prev_state", TradeStage.IDLE.value)), TradeStage.IDLE),
|
||||
next_state=_safe_enum(TradeStage, str(payload.get("next_state", TradeStage.IDLE.value)), TradeStage.IDLE),
|
||||
trigger=str(payload.get("trigger", "")),
|
||||
intent_id=str(payload.get("intent_id", "")),
|
||||
event_id=str(payload.get("event_id", "")),
|
||||
@@ -428,7 +436,7 @@ def _outcome_from_payload(payload: Dict[str, Any]) -> KernelOutcome:
|
||||
accepted=bool(payload.get("accepted", False)),
|
||||
slot_id=int(payload.get("slot_id", 0)),
|
||||
trade_id=str(payload.get("trade_id", "")),
|
||||
state=TradeStage(str(payload.get("state", TradeStage.IDLE.value))),
|
||||
state=_safe_enum(TradeStage, str(payload.get("state", TradeStage.IDLE.value)), TradeStage.IDLE),
|
||||
diagnostic_code=KernelDiagnosticCode(str(payload.get("diagnostic_code", KernelDiagnosticCode.OK.value))),
|
||||
severity=KernelSeverity(str(payload.get("severity", KernelSeverity.INFO.value))),
|
||||
transitions=tuple(_transition_from_payload(row) for row in payload.get("transitions", [])),
|
||||
|
||||
@@ -968,3 +968,153 @@ class TestO1MaybeCloseAsyncSafe:
|
||||
|
||||
_maybe_close(_FakeSync())
|
||||
assert closed == [True], "sync close() must still be called"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# M9: ORDER_REJECT must NOT nuke a live POSITION_OPEN slot
|
||||
# ============================================================
|
||||
|
||||
class TestM9OrderRejectPositionOpen:
|
||||
"""M9: A spurious ORDER_REJECT arriving while the slot is POSITION_OPEN
|
||||
must not reset it to IDLE. Only entry-phase rejects should reset."""
|
||||
|
||||
def _open_position(self, k: ExecutionKernel, trade_id: str) -> None:
|
||||
r = k.process_intent(_mk_intent(action=E.ENTER, trade_id=trade_id))
|
||||
assert r.accepted, f"ENTER failed: {r.diagnostic_code}"
|
||||
assert k._get_slot(0).fsm_state == TradeStage.POSITION_OPEN
|
||||
|
||||
def test_spurious_reject_does_not_reset_position_open(self):
|
||||
"""A stale ORDER_REJECT with no matching active order must not nuke slot."""
|
||||
k = _fresh_kernel()
|
||||
self._open_position(k, "m9a")
|
||||
|
||||
reject = _mk_venue_event(
|
||||
kind=KernelEventKind.ORDER_REJECT,
|
||||
trade_id="m9a",
|
||||
event_id="stale-reject-m9a",
|
||||
status=VenueEventStatus.REJECTED,
|
||||
)
|
||||
result = k.on_venue_event(reject)
|
||||
slot = k._get_slot(0)
|
||||
assert slot.fsm_state == TradeStage.POSITION_OPEN, (
|
||||
f"M9: spurious ORDER_REJECT must not reset POSITION_OPEN → got {slot.fsm_state}"
|
||||
)
|
||||
assert not result.accepted, "Spurious reject must be reported as not accepted"
|
||||
|
||||
def test_entry_reject_still_resets_to_idle(self):
|
||||
"""Entry-phase ORDER_REJECT must still reset to IDLE (regression)."""
|
||||
k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False))
|
||||
k.process_intent(_mk_intent(action=E.ENTER, trade_id="m9b"))
|
||||
slot = k._get_slot(0)
|
||||
assert slot.active_entry_order is not None
|
||||
|
||||
reject = _mk_venue_event(
|
||||
kind=KernelEventKind.ORDER_REJECT,
|
||||
trade_id="m9b",
|
||||
event_id="entry-reject-m9b",
|
||||
status=VenueEventStatus.REJECTED,
|
||||
)
|
||||
k.on_venue_event(reject)
|
||||
assert k._get_slot(0).fsm_state == TradeStage.IDLE, (
|
||||
"Entry-phase ORDER_REJECT must still reset slot to IDLE"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# G4: exit multi-leg — no phantom extra leg after last fill
|
||||
# ============================================================
|
||||
|
||||
class TestG4ExitLegOrdering:
|
||||
"""G4: all_legs_done was computed before consume_exit_leg(), causing a
|
||||
phantom 3rd-leg attempt on the final leg's FULL_FILL when size > 1e-12."""
|
||||
|
||||
def test_two_leg_exit_closes_cleanly(self):
|
||||
"""A 2-leg exit must close the slot without a phantom extra leg."""
|
||||
k = _fresh_kernel()
|
||||
k.process_intent(_mk_intent(action=E.ENTER, trade_id="g4a", size=2.0,
|
||||
exit_leg_ratios=(0.5, 0.5)))
|
||||
slot = k._get_slot(0)
|
||||
assert slot.fsm_state == TradeStage.POSITION_OPEN
|
||||
|
||||
# Leg 1 EXIT
|
||||
k.process_intent(_mk_intent(action=E.EXIT, trade_id="g4a", size=1.0,
|
||||
exit_leg_ratios=(0.5, 0.5)))
|
||||
slot = k._get_slot(0)
|
||||
assert slot.fsm_state in (TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING), (
|
||||
f"After leg-1 fill, slot must be POSITION_OPEN or EXIT_WORKING, got {slot.fsm_state}"
|
||||
)
|
||||
|
||||
# Leg 2 EXIT
|
||||
k.process_intent(_mk_intent(action=E.EXIT, trade_id="g4a", size=1.0,
|
||||
exit_leg_ratios=(0.5, 0.5)))
|
||||
slot = k._get_slot(0)
|
||||
assert slot.is_free(), (
|
||||
f"G4: 2-leg exit must fully close slot, got {slot.fsm_state}"
|
||||
)
|
||||
|
||||
def test_single_leg_exit_unaffected(self):
|
||||
"""Single-leg exit (the common case) must still work correctly."""
|
||||
k = _fresh_kernel()
|
||||
k.process_intent(_mk_intent(action=E.ENTER, trade_id="g4b"))
|
||||
k.process_intent(_mk_intent(action=E.EXIT, trade_id="g4b"))
|
||||
assert k._get_slot(0).is_free(), "Single-leg exit must close slot"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# G9: venue_order_id routed to exit order in exit phase
|
||||
# ============================================================
|
||||
|
||||
class TestG9VenueOrderIdRouting:
|
||||
"""G9: venue_order_id update must target the exit order when in an exit
|
||||
FSM state, not the still-present entry order reference."""
|
||||
|
||||
def test_order_ack_in_exit_phase_fills_exit_order_id(self):
|
||||
"""ORDER_ACK arriving after EXIT is submitted must fill exit order's
|
||||
venue_order_id, not the entry order's."""
|
||||
k = _fresh_kernel(scenario=MockVenueScenario(partial_fill_ratio=0.0, emit_fill_on_submit=False))
|
||||
k.process_intent(_mk_intent(action=E.ENTER, trade_id="g9a"))
|
||||
fill = _mk_venue_event(
|
||||
kind=KernelEventKind.FULL_FILL,
|
||||
trade_id="g9a",
|
||||
event_id="fill-g9a",
|
||||
price=100.0,
|
||||
size=1.0,
|
||||
filled_size=1.0,
|
||||
)
|
||||
k.on_venue_event(fill)
|
||||
assert k._get_slot(0).fsm_state == TradeStage.POSITION_OPEN
|
||||
|
||||
# Submit exit — mock emits no fill, just ACK
|
||||
k.process_intent(_mk_intent(action=E.EXIT, trade_id="g9a"))
|
||||
slot = k._get_slot(0)
|
||||
# If active_exit_order exists and has a venue_order_id, G9 is not triggered
|
||||
if slot.active_exit_order is not None:
|
||||
assert slot.active_exit_order.venue_order_id != "", (
|
||||
"G9: exit order must have a venue_order_id after ORDER_ACK in exit phase"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# H6: _safe_enum gracefully handles unknown Rust FFI states
|
||||
# ============================================================
|
||||
|
||||
class TestH6SafeEnum:
|
||||
"""H6: unknown enum variants from the Rust FFI must not raise ValueError —
|
||||
they must fall back to a safe default instead of crashing the process."""
|
||||
|
||||
def test_safe_enum_known_value(self):
|
||||
from prod.clean_arch.dita_v2.rust_backend import _safe_enum
|
||||
result = _safe_enum(TradeStage, "POSITION_OPEN", TradeStage.IDLE)
|
||||
assert result == TradeStage.POSITION_OPEN
|
||||
|
||||
def test_safe_enum_unknown_value_returns_default(self):
|
||||
from prod.clean_arch.dita_v2.rust_backend import _safe_enum
|
||||
result = _safe_enum(TradeStage, "UNKNOWN_FUTURE_STATE", TradeStage.IDLE)
|
||||
assert result == TradeStage.IDLE, (
|
||||
"H6: unknown enum variant must return default, not raise ValueError"
|
||||
)
|
||||
|
||||
def test_safe_enum_empty_string_returns_default(self):
|
||||
from prod.clean_arch.dita_v2.rust_backend import _safe_enum
|
||||
result = _safe_enum(TradeStage, "", TradeStage.IDLE)
|
||||
assert result == TradeStage.IDLE
|
||||
|
||||
Reference in New Issue
Block a user