use super::*; use serde_json::{Value, json}; fn sample() -> ManualExecutionReplay { let fill = json!({"tradeId":"trade-1","observationEventId":"received-1","observationSequence":1,"tradeDate":"2026-09-14","executedAt":"2026-09-14T01:30:00Z", "feeObservationEventId":"received-1","feeObservationSequence":1,"feeObservedAt":"2026-09-14T01:30:01Z", "observedAt":"2026-09-14T01:30:01Z","timestampPrecision":"second","quantity":100, "price":"10.1234567891","commission":"0.1000001","stampTax":"0","transferFee":"0.02","totalFee":"0.1200001"}); let mut input:ManualExecutionReplay=serde_json::from_value(json!({ "schema":MANUAL_REPLAY_SCHEMA,"runtimeId":"runtime-1","accountId":"account-1", "sourceContractSha256":"a".repeat(64),"contentSha256":"", "observationCutoff":"2026-09-14T08:00:00Z", "actions":[{"actionId":"action-1","source":"manual_security_trade","auditEventIds":["audit-1"], "confirmedAt":"2026-09-14T01:30:00.500Z","confirmationObservedAt":"2026-09-14T01:30:00.550Z","outcome":"orders_terminal","orders":[{ "orderId":"order-1","brokerOrderId":"broker-1","sourceAdapter":"gt-api","symbol":"000001.SZ","side":"Buy","quantity":100, "orderCreatedAt":"2026-09-14T01:30:00.600Z","terminalObservedAt":"2026-09-14T01:30:00.900Z","terminalStatus":"filled", "fills":[fill] }]}] })).unwrap(); reseal(&mut input); input } fn reseal(input: &mut ManualExecutionReplay) { input.content_sha256 = input.content_digest().unwrap(); } fn delayed_buy_replay() -> ManualExecutionReplay { let mut input = sample(); let template = input.actions[0].clone(); input.actions.clear(); for (index, side, executed, observed, price, fee) in [ ( 0, OrderSide::Buy, "2026-09-14T01:30:00Z", "2026-09-14T01:30:01Z", "20", "0.25", ), ( 1, OrderSide::Buy, "2026-09-11T06:00:00Z", "2026-09-14T01:30:02Z", "10", "0.75", ), ( 2, OrderSide::Sell, "2026-09-14T01:31:00Z", "2026-09-14T01:31:01Z", "10", "0.5", ), ( 3, OrderSide::Sell, "2026-09-14T01:32:00Z", "2026-09-14T01:32:01Z", "10", "0.5", ), ] { let executed: DateTime = executed.parse().unwrap(); let observed: DateTime = observed.parse().unwrap(); let mut action = template.clone(); action.action_id = format!("action-{index}"); action.audit_event_ids = vec![format!("audit-{index}")]; action.confirmed_at = executed - chrono::Duration::seconds(2); action.confirmation_observed_at = action.confirmed_at; let order = &mut action.orders[0]; order.order_id = format!("order-{index}"); order.broker_order_id = Some(format!("broker-{index}")); order.side = side; order.order_created_at = executed - chrono::Duration::seconds(1); order.terminal_observed_at = observed; let fill = &mut order.fills[0]; fill.trade_id = format!("trade-{index}"); fill.observation_event_id = format!("receipt-{index}"); fill.observation_sequence = index + 1; fill.fee_observation_event_id = fill.observation_event_id.clone(); fill.fee_observation_sequence = fill.observation_sequence; fill.trade_date = executed .with_timezone(&FixedOffset::east_opt(8 * 3600).unwrap()) .date_naive(); fill.executed_at = executed; fill.observed_at = observed; fill.fee_observed_at = observed; fill.price = price.parse().unwrap(); fill.commission = None; fill.stamp_tax = None; fill.transfer_fee = None; fill.total_fee = fee.parse().unwrap(); input.actions.push(action); } reseal(&mut input); input.validate().unwrap(); input } #[test] fn late_buy_retains_the_earliest_opening_and_latest_buy_dates() { let mut cursor = ManualReplayCursor::new(delayed_buy_replay()).unwrap(); let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); let mut portfolio = PortfolioState::new(10000.); let applications = cursor .advance( "2026-09-14T01:30:02Z".parse().unwrap(), &mut portfolio, &data, false, ) .unwrap(); assert_eq!( applications .iter() .map(|row| row.trade_id.as_str()) .collect::>(), ["trade-0", "trade-1"] ); let position = portfolio.position("000001.SZ").unwrap(); assert_eq!(position.opened_date(), NaiveDate::from_ymd_opt(2026, 9, 11)); assert_eq!( position.last_buy_date(), NaiveDate::from_ymd_opt(2026, 9, 14) ); assert_eq!(position.quantity, 200); let calendar = crate::TradingCalendar::new( [11, 14, 15, 16, 17, 18] .map(|day| NaiveDate::from_ymd_opt(2026, 9, day).unwrap()) .into(), ); let evidence = crate::holding_policy::HoldingLifecycleEvidence { has_position: true, opened_date: position.opened_date(), last_buy_date: position.last_buy_date(), last_sell_date: None, }; let mut policy = crate::holding_policy::AutomaticTradeProtection { max_holding_days: 1, ..Default::default() }; assert!( policy .evaluate( "000001.SZ", NaiveDate::from_ymd_opt(2026, 9, 14).unwrap(), &evidence, &calendar ) .unwrap() .max_holding_exit ); policy.buy_protection_days = 3; for day in [14, 15, 16, 17] { let permission = policy .evaluate( "000001.SZ", NaiveDate::from_ymd_opt(2026, 9, day).unwrap(), &evidence, &calendar, ) .unwrap(); assert_eq!(permission.sell_denial, Some("buy_fill_protection")); assert!(!permission.max_holding_exit); } assert!( policy .evaluate( "000001.SZ", NaiveDate::from_ymd_opt(2026, 9, 18).unwrap(), &evidence, &calendar ) .unwrap() .max_holding_exit ); } #[test] fn late_buy_fifo_depletion_preserves_costs_and_cannot_unlock_today_lots() { let mut cursor = ManualReplayCursor::new(delayed_buy_replay()).unwrap(); let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); let mut portfolio = PortfolioState::new(10000.); let applications = cursor .advance( "2026-09-14T01:31:01Z".parse().unwrap(), &mut portfolio, &data, false, ) .unwrap(); assert_eq!(applications.len(), 3); let position = portfolio.position("000001.SZ").unwrap(); assert_eq!(position.quantity, 100); assert_eq!(position.unrealized_pnl(), -1000.25); assert_eq!( position.sellable_qty(NaiveDate::from_ymd_opt(2026, 9, 14).unwrap()), 0 ); assert_eq!(position.realized_pnl(), -0.75); assert_eq!(portfolio.cash(), 7998.5); assert_eq!(portfolio.external_cash_flow_total(), 0.); assert!( cursor .advance( "2026-09-14T01:32:01Z".parse().unwrap(), &mut portfolio, &data, false ) .unwrap_err() .contains("T+1") ); assert_eq!(cursor.applied_count(), 3); assert_eq!(portfolio.cash(), 7998.5); assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 100); } fn semantic_result(input: &ManualExecutionReplay) -> Result<(), String> { let mut input = input.clone(); reseal(&mut input); input.validate() } #[test] fn complete_exact_decimal_evidence_allows_later_observation_and_retains_source_digits() { let input = sample(); input.validate().unwrap(); let fill = &input.actions[0].orders[0].fills[0]; assert_eq!(fill.gross_amount().unwrap().to_string(), "1012.3456789100"); assert_eq!(fill.total_fees().unwrap().to_string(), "0.1200001"); assert_eq!( serde_json::to_value(&input).unwrap()["actions"][0]["orders"][0]["fills"][0]["price"], "10.1234567891" ); } #[test] fn data_scope_only_contains_actual_filled_securities_and_validates_the_source() { let mut input = sample(); let mut rejected = input.actions[0].orders[0].clone(); rejected.order_id = "rejected-order".into(); rejected.broker_order_id = None; rejected.source_adapter = None; rejected.symbol = "510300.SH".into(); rejected.terminal_status = ManualOrderTerminalStatus::Rejected; rejected.fills.clear(); input.actions[0].orders.push(rejected); reseal(&mut input); assert_eq!( input.required_data_symbols().unwrap(), BTreeSet::from(["000001.SZ".into()]) ); input.actions[0].orders[0].symbol = "600000.SH".into(); assert!(input.required_data_symbols().is_err()); } #[test] fn v2_facts_keep_their_encoding_but_cannot_silently_carry_new_runtime_settings() { let mut input = sample(); input.schema = "fidc.observed-manual-executions/v2".into(); reseal(&mut input); input.validate().unwrap(); let old = serde_json::to_value(&input).unwrap(); assert!(old.get("positionExposureEvents").is_none()); assert!(old.get("legacyPositionExposureBps").is_none()); input .legacy_position_exposure_bps .insert(NaiveDate::from_ymd_opt(2026, 9, 14).unwrap(), 5000); reseal(&mut input); assert!(input.validate().is_err()); input.schema = MANUAL_REPLAY_SCHEMA.into(); reseal(&mut input); input.validate().unwrap(); } #[test] fn runtime_position_events_cannot_claim_observations_after_the_source_cutoff() { let mut input = sample(); input.position_exposure_events.push(serde_json::from_value(json!({ "eventId": "position-event", "sequence": 1, "effectiveAt": input.observation_cutoff, "action": "scale", "requestedBps": 5000 })).unwrap()); semantic_result(&input).unwrap(); input.position_exposure_events[0].effective_at += chrono::Duration::nanoseconds(1); assert!(semantic_result(&input).unwrap_err().contains("after the evidence cutoff")); } #[test] fn all_required_money_and_binding_fields_reject_missing_or_wrong_values() { let original = serde_json::to_value(sample()).unwrap(); for field in ["price", "totalFee"] { let mut missing = original.clone(); missing["actions"][0]["orders"][0]["fills"][0] .as_object_mut() .unwrap() .remove(field); assert!( serde_json::from_value::(missing).is_err(), "{field}" ); let mut numeric = original.clone(); numeric["actions"][0]["orders"][0]["fills"][0][field] = json!(1.1); assert!( serde_json::from_value::(numeric).is_err(), "numeric {field}" ); } for mutate in [ ("schema", json!("unknown")), ("sourceContractSha256", json!("broken")), ("accountId", json!(" ")), ] { let mut value = original.clone(); value[mutate.0] = mutate.1; assert!( semantic_result(&serde_json::from_value::(value).unwrap()) .is_err() ); } } #[test] fn inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected() { let original = sample(); let mut invalid = original.clone(); invalid.actions[0].orders[0].quantity = 200; assert!(semantic_result(&invalid).is_err()); let mut invalid = original.clone(); invalid.actions[0].orders[0].terminal_status = ManualOrderTerminalStatus::Rejected; assert!(semantic_result(&invalid).is_err()); let mut invalid = original.clone(); invalid.actions[0].audit_event_ids.clear(); assert!(semantic_result(&invalid).is_err()); let mut invalid = original.clone(); invalid.actions.push(invalid.actions[0].clone()); assert!(semantic_result(&invalid).is_err()); let mut invalid = original.clone(); let duplicate = invalid.actions[0].orders[0].fills[0].clone(); invalid.actions[0].orders[0].fills.push(duplicate); assert!(semantic_result(&invalid).is_err()); let mut invalid = original.clone(); invalid.actions[0].orders[0].broker_order_id = None; assert!(semantic_result(&invalid).is_err()); invalid.actions[0].orders[0].source_adapter = Some("paper".into()); reseal(&mut invalid); invalid.validate().unwrap(); } #[test] fn source_time_precision_is_not_invented_and_submitted_time_must_fit_the_interval() { let mut input = sample(); input.actions[0].orders[0].order_created_at = "2026-09-14T01:30:00.999999Z".parse().unwrap(); input.actions[0].orders[0].terminal_observed_at = "2026-09-14T01:30:01.500Z".parse().unwrap(); input.actions[0].orders[0].fills[0].observed_at = "2026-09-14T01:30:02Z".parse().unwrap(); input.actions[0].orders[0].fills[0].fee_observed_at = input.actions[0].orders[0].fills[0].observed_at; reseal(&mut input); input.validate().unwrap(); input.actions[0].orders[0].order_created_at = "2026-09-14T01:30:01Z".parse().unwrap(); assert!(semantic_result(&input).is_err()); let mut input = sample(); input.actions[0].orders[0].fills[0].executed_at = "2026-09-14T01:30:00.800Z".parse().unwrap(); assert!(semantic_result(&input).is_err()); input.actions[0].orders[0].fills[0].timestamp_precision = ManualTimestampPrecision::Millisecond; reseal(&mut input); input.validate().unwrap(); input.actions[0].orders[0].fills[0].executed_at = "2026-09-14T01:30:00.800001Z".parse().unwrap(); assert!(semantic_result(&input).is_err()); } #[test] fn confirmed_no_order_outcome_is_distinct_from_unconfirmed_or_unknown_work() { let mut input = sample(); input.actions[0].orders.clear(); assert!(semantic_result(&input).is_err()); input.actions[0].outcome = ManualActionOutcome::NoOrdersNeeded; reseal(&mut input); input.validate().unwrap(); input.actions[0].outcome = ManualActionOutcome::NotExecuted; reseal(&mut input); input.validate().unwrap(); let mut value = serde_json::to_value(input).unwrap(); value["actions"][0]["outcome"] = json!("result_unknown"); assert!(serde_json::from_value::(value).is_err()); } #[test] fn raw_timezone_and_cutoff_are_required() { let mut value = serde_json::to_value(sample()).unwrap(); value["actions"][0]["orders"][0]["fills"][0]["executedAt"] = json!("2026-09-14T09:30:00"); assert!(serde_json::from_value::(value).is_err()); let mut input = sample(); input.observation_cutoff = "2026-09-14T01:30:00.700Z".parse().unwrap(); assert!(semantic_result(&input).is_err()); let mut value = serde_json::to_value(sample()).unwrap(); value["actions"][0]["orders"][0]["fills"][0]["totalFee"] = Value::Null; assert!(serde_json::from_value::(value).is_err()); } #[test] fn authoritative_total_fee_does_not_require_inventing_unknown_components() { let mut input = sample(); let fill = &mut input.actions[0].orders[0].fills[0]; fill.commission = None; fill.stamp_tax = None; fill.transfer_fee = None; assert_eq!( fill.total_fees().unwrap(), "0.1200001".parse::().unwrap() ); assert!(semantic_result(&input).is_ok()); let value = serde_json::to_value(&input).unwrap(); assert!(value["actions"][0]["orders"][0]["fills"][0]["commission"].is_null()); assert_eq!( value["actions"][0]["orders"][0]["fills"][0]["totalFee"], "0.1200001" ); for field in ["commission", "stampTax", "transferFee"] { let mut numeric = value.clone(); numeric["actions"][0]["orders"][0]["fills"][0][field] = json!(0.1); assert!(serde_json::from_value::(numeric).is_err()); } } #[test] fn manual_fee_total_includes_extra_charges_and_rejects_inconsistent_components() { let mut input = sample(); input.actions[0].orders[0].fills[0].total_fee = "0.15".parse().unwrap(); assert!(semantic_result(&input).is_ok()); assert_eq!( input.actions[0].orders[0].fills[0] .total_fees() .unwrap() .to_string(), "0.15" ); input.actions[0].orders[0].fills[0].total_fee = "0.1".parse().unwrap(); assert!(semantic_result(&input).is_err()); input.actions[0].orders[0].fills[0].total_fee = "0.15".parse().unwrap(); input.actions[0].orders[0].fills[0].commission = Some(Decimal::NEGATIVE_ONE); assert!(semantic_result(&input).is_err()); } #[test] fn late_fee_evidence_keeps_the_original_fill_observation_clock() { let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); let mut input = sample(); let fill = &mut input.actions[0].orders[0].fills[0]; let original = fill.observed_at; fill.fee_observation_event_id = "fee-receipt-1".into(); fill.fee_observation_sequence = 2; fill.fee_observed_at = original + chrono::Duration::hours(1); let fee_time = fill.fee_observed_at; reseal(&mut input); let mut cursor = ManualReplayCursor::new(input).unwrap(); assert_eq!(cursor.next_observation_at(), Some(original)); let mut portfolio = PortfolioState::new(10_000.); let result = cursor .advance(original, &mut portfolio, &data, false) .unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].observed_at, original); assert_eq!(result[0].fee_observed_at, fee_time); assert_eq!(result[0].source_total_fee, "0.1200001"); assert!( cursor .advance(fee_time, &mut portfolio, &data, false) .unwrap() .is_empty() ); } #[test] fn changing_any_external_price_or_identity_invalidates_the_frozen_trace() { let input = sample(); let original = input.content_sha256.clone(); let mut changed = input.clone(); changed.actions[0].orders[0].fills[0].price += Decimal::ONE; assert_ne!(changed.content_digest().unwrap(), original); assert_eq!( changed.validate().unwrap_err(), "manual replay content digest mismatch" ); let mut changed = input; changed.account_id = "another-account".into(); assert_ne!(changed.content_digest().unwrap(), original); assert!(changed.validate().is_err()); } fn identity_data(listed: NaiveDate) -> DataSet { DataSet::from_components( vec![crate::Instrument { symbol: "000001.SZ".into(), name: "test".into(), board: "SZ".into(), round_lot: 100, listed_at: Some(listed), delisted_at: None, status: "active".into(), }], vec![], vec![], vec![], vec![crate::BenchmarkSnapshot { date: listed, benchmark: "000300.SH".into(), open: 100., close: 100., prev_close: 100., volume: 0, }], ) .unwrap() } #[test] fn confirmed_manual_fill_changes_cash_and_lots_but_not_external_cash_flow_units() { let input = sample(); let observations = input.observations().unwrap(); let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); let mut account = PortfolioState::new(10_000.); let applied = observations[0].apply(&mut account, &data, false).unwrap(); assert_eq!( applied.gross, FixedMoney::from_decimal_str("1012.345679").unwrap() ); assert_eq!(applied.fees, FixedMoney::from_decimal_str("0.12").unwrap()); assert_eq!(account.cash(), 8987.534321); assert_eq!(account.position("000001.SZ").unwrap().quantity, 100); assert_eq!( account .position("000001.SZ") .unwrap() .sellable_qty(input.actions[0].orders[0].fills[0].trade_date), 0 ); assert_eq!(account.external_cash_flow_total(), 0.); assert_eq!(account.starting_cash(), 10_000.); } #[test] fn manual_mismatches_are_atomic_and_do_not_borrow_shares_cash_or_override_pending_orders() { let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); let input = sample(); let observations = input.observations().unwrap(); let mut poor = PortfolioState::new(10.); assert!(observations[0].apply(&mut poor, &data, false).is_err()); assert_eq!(poor.cash(), 10.); assert!(poor.positions().is_empty()); let mut account = PortfolioState::new(10_000.); assert!(observations[0].apply(&mut account, &data, true).is_err()); assert_eq!(account.cash(), 10_000.); assert!(account.positions().is_empty()); observations[0].apply(&mut account, &data, false).unwrap(); let before = account.cash(); let mut sell = input.clone(); sell.actions[0].orders[0].side = OrderSide::Sell; reseal(&mut sell); assert!( sell.observations().unwrap()[0] .apply(&mut account, &data, false) .unwrap_err() .contains("T+1") ); assert_eq!(account.cash(), before); assert_eq!(account.position("000001.SZ").unwrap().quantity, 100); let unlisted = identity_data(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap()); assert!( observations[0] .apply(&mut account, &unlisted, false) .unwrap_err() .contains("lifecycle") ); assert_eq!(account.cash(), before); } #[test] fn the_next_day_manual_sale_keeps_the_actual_quantity_and_fee_contract() { let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); let input = sample(); let mut account = PortfolioState::new(10_000.); input.observations().unwrap()[0] .apply(&mut account, &data, false) .unwrap(); let mut sell = input.clone(); let order = &mut sell.actions[0].orders[0]; order.side = OrderSide::Sell; order.order_created_at += chrono::Duration::days(1); order.terminal_observed_at += chrono::Duration::days(1); order.fills[0].trade_date = order.fills[0].trade_date.succ_opt().unwrap(); order.fills[0].executed_at += chrono::Duration::days(1); order.fills[0].observed_at += chrono::Duration::days(1); order.fills[0].fee_observed_at += chrono::Duration::days(1); sell.observation_cutoff += chrono::Duration::days(1); reseal(&mut sell); let applied = sell.observations().unwrap()[0] .apply(&mut account, &data, false) .unwrap(); assert_eq!(applied.quantity_after, 0); assert_eq!(account.cash(), 9999.76); assert_eq!(account.external_cash_flow_total(), 0.); } #[test] fn observations_follow_durable_receipt_order_and_not_input_array_order() { let mut input = sample(); let mut second = input.actions[0].orders[0].fills[0].clone(); second.trade_id = "trade-2".into(); second.observation_event_id = "received-2".into(); second.observation_sequence = 2; second.fee_observation_event_id = "received-2".into(); second.fee_observation_sequence = 2; input.actions[0].orders[0].quantity = 200; input.actions[0].orders[0].fills.insert(0, second); reseal(&mut input); assert_eq!( input .observations() .unwrap() .iter() .map(|row| row.fill.observation_sequence) .collect::>(), vec![1, 2] ); let mut invalid = input.clone(); invalid.actions[0].orders[0].fills[0].observation_sequence = 1; assert!( semantic_result(&invalid) .unwrap_err() .contains("observation") ); let mut invalid = input; invalid.actions[0].orders[0].fills[0].observation_event_id = "received-1".into(); assert!( semantic_result(&invalid) .unwrap_err() .contains("observation") ); } #[test] fn partial_cancel_is_valid_but_full_fill_cannot_be_reported_as_cancelled() { let mut input = sample(); input.actions[0].orders[0].quantity = 200; input.actions[0].orders[0].terminal_status = ManualOrderTerminalStatus::Cancelled; semantic_result(&input).unwrap(); input.actions[0].orders[0].quantity = 100; assert!( semantic_result(&input) .unwrap_err() .contains("terminal status") ); } #[test] fn cursor_waits_for_observation_and_never_reapplies_or_rewinds() { let input = sample(); let at = input.actions[0].orders[0].fills[0].observed_at; let mut replay = ManualReplayCursor::new(input).unwrap(); let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); let mut account = PortfolioState::new(10_000.); assert_eq!(replay.next_observation_at(), Some(at)); assert!( replay .advance( at - chrono::Duration::milliseconds(1), &mut account, &data, false ) .unwrap() .is_empty() ); assert_eq!(account.cash(), 10_000.); let records = replay.advance(at, &mut account, &data, false).unwrap(); assert_eq!(records.len(), 1); assert_eq!(records[0].cash_delta, "-1012.465679"); assert_eq!(replay.applied_count(), 1); assert_eq!(replay.next_observation_at(), None); let cash = account.cash(); assert!( replay .advance(at, &mut account, &data, false) .unwrap() .is_empty() ); assert_eq!(account.cash(), cash); assert!( replay .advance( at - chrono::Duration::seconds(1), &mut account, &data, false ) .unwrap_err() .contains("backwards") ); } #[test] fn failed_multi_receipt_advance_keeps_both_progress_and_portfolio_unchanged() { let mut input = sample(); let mut next = input.actions[0].orders[0].fills[0].clone(); next.trade_id = "trade-2".into(); next.observation_event_id = "received-2".into(); next.observation_sequence = 2; next.fee_observation_event_id = "received-2".into(); next.fee_observation_sequence = 2; input.actions[0].orders[0].quantity = 200; input.actions[0].orders[0].fills.push(next); reseal(&mut input); let at = input.actions[0].orders[0].fills[0].observed_at; let mut replay = ManualReplayCursor::new(input).unwrap(); let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); let mut account = PortfolioState::new(1_500.); assert!(replay.advance(at, &mut account, &data, false).is_err()); assert_eq!(account.cash(), 1_500.); assert!(account.positions().is_empty()); assert_eq!(replay.applied_count(), 0); assert_eq!(replay.next_observation_at(), Some(at)); } #[test] fn fixed_money_decimal_text_preserves_micro_units_without_float_conversion() { for text in [ "0", "100", "-100", "0.000001", "-0.000001", "12345678901234567890123456.123456", ] { assert_eq!( FixedMoney::from_decimal_str(text) .unwrap() .to_decimal_string(), text ); } let min = FixedMoney::from_raw(i128::MIN); assert!(min.to_decimal_string().starts_with('-')); }