//! End-to-end verification of the C10 derived position-event table (#114) at the //! public crate boundary: the World consumes `PositionAction` / `PositionEvent` //! via the `aura_engine` re-exports and persists the audit table the same way //! the run registry persists a `RunReport` — through serde_json. These tests pin //! the *consumer-visible wire shape* (the persisted column form), distinct from //! the crate-internal type-level unit tests in `report.rs`. use aura_engine::{PositionAction, PositionEvent, Timestamp}; /// A minimal stop-and-reverse audit table as a broker would derive it: open /// long, then at one later instant Close-before-open into a short (a reversal, /// two events sharing `event_ts`). The smallest table that exercises every /// action variant and the same-ts reversal shape. fn reversal_table() -> Vec { let open = Timestamp(1_000); let flip = Timestamp(2_000); vec![ PositionEvent { event_ts: open, action: PositionAction::Buy, position_id: 1, instrument_id: 3, volume: 0.10, }, // reversal at one instant: close the long first, then open the short. PositionEvent { event_ts: flip, action: PositionAction::Close, position_id: 1, instrument_id: 3, volume: 0.10, }, PositionEvent { event_ts: flip, action: PositionAction::Sell, position_id: 2, instrument_id: 3, volume: 0.10, }, ] } /// Property: a derived position-event table survives a full serde_json /// round-trip through the public re-exports byte-for-byte equal, AND the /// persisted `action` column is a **bare integer** (C7-scalar / ledger column /// shape), never a tagged-enum object. The World persists this audit table with /// the same encoder it uses for `RunReport`; if `action` serialised as /// `{"Sell":null}` it would break the flat columnar contract and any tool /// reading the table as `action: i64`. Pins the consumer-visible on-disk shape. #[test] fn position_event_table_round_trips_with_bare_int_action_column() { let table = reversal_table(); let json = serde_json::to_string(&table).expect("serialize audit table"); // action persists as a bare i64 (Buy=0, Sell=1, Close=2), not a tagged enum. assert!(json.contains("\"action\":0"), "Buy must encode as action:0 — {json}"); assert!(json.contains("\"action\":1"), "Sell must encode as action:1 — {json}"); assert!(json.contains("\"action\":2"), "Close must encode as action:2 — {json}"); assert!(!json.contains("\"Buy\""), "action must not be a tagged enum — {json}"); let back: Vec = serde_json::from_str(&json).expect("deserialize audit table"); assert_eq!(back, table, "the audit table must round-trip byte-for-byte (C18 reproducibility)"); } /// Property: the persisted `PositionAction` integer mapping is the **stable /// wire encoding** `Buy=0, Sell=1, Close=2`, and any out-of-range integer is /// **rejected** on read rather than silently coerced. This is the contract every /// persisted audit table depends on: renumbering the variants (or accepting a /// stray 3 as some action) would silently mis-read every historical table. /// Exercised through the public re-export as a downstream reader would. #[test] fn position_action_wire_encoding_is_stable_and_validated() { // forward: the literal i64 each variant persists as. assert_eq!(serde_json::to_string(&PositionAction::Buy).unwrap(), "0"); assert_eq!(serde_json::to_string(&PositionAction::Sell).unwrap(), "1"); assert_eq!(serde_json::to_string(&PositionAction::Close).unwrap(), "2"); // backward: each valid code reads back to its variant. assert_eq!(serde_json::from_str::("0").unwrap(), PositionAction::Buy); assert_eq!(serde_json::from_str::("1").unwrap(), PositionAction::Sell); assert_eq!(serde_json::from_str::("2").unwrap(), PositionAction::Close); // an out-of-range code is refused on read, not coerced (the validation arm). assert!(serde_json::from_str::("3").is_err()); assert!(serde_json::from_str::("-1").is_err()); }