⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc
crates/modules/nakui/core/tests/state_hash.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
chore: absorbe nakui (ERP matemático) en modules/nakui
1//! Tests for the `Store::iter` / `Store::hash_state` contract under
2//! realistic WAL flows: a live store and a log-replayed store must hash
3//! identically, drift must be detectable as a hash mismatch, and the
4//! property must hold across backends (within a backend — cross-backend
5//! parity is a separate concern, see notes below).
6
7use std::path::{Path, PathBuf};
8
9use nakui_core::event_log::{EventLog, execute_and_log, replay, seed_and_log};
10use nakui_core::executor::Executor;
11use nakui_core::store::{MemoryStore, Store};
12use serde_json::json;
13use uuid::Uuid;
14
15fn workspace_root() -> PathBuf {
16 Path::new(env!("CARGO_MANIFEST_DIR"))
17 .parent()
18 .expect("workspace root above core/")
19 .to_path_buf()
20}
21
22fn treasury_module() -> PathBuf {
23 workspace_root().join("modules/treasury")
24}
25
26fn fresh_log_path() -> PathBuf {
27 std::env::temp_dir().join(format!("nakui_hash_{}.jsonl", Uuid::new_v4()))
28}
29
30fn seed_two_cajas(
31 exec: &Executor,
32 store: &mut MemoryStore,
33 log: &mut EventLog,
34 a: Uuid,
35 b: Uuid,
36) {
37 seed_and_log(
38 exec,
39 store,
40 log,
41 "Caja",
42 a,
43 json!({"id": a.to_string(), "name": "A", "saldo": 200_000_i64, "currency": "USD"}),
44 )
45 .unwrap();
46 seed_and_log(
47 exec,
48 store,
49 log,
50 "Caja",
51 b,
52 json!({"id": b.to_string(), "name": "B", "saldo": 50_000_i64, "currency": "USD"}),
53 )
54 .unwrap();
55}
56
57#[test]
58fn live_store_hash_matches_replayed_store_hash() {
59 let exec = Executor::load_module(treasury_module()).expect("load");
60 let log_path = fresh_log_path();
61 let mut log = EventLog::open(&log_path).unwrap();
62 let mut live = MemoryStore::new();
63
64 let a = Uuid::new_v4();
65 let b = Uuid::new_v4();
66 seed_two_cajas(&exec, &mut live, &mut log, a, b);
67
68 execute_and_log(
69 &exec,
70 &mut live,
71 &mut log,
72 "register_cash_move",
73 &[("caja", a)],
74 json!({
75 "monto": 25_000_i64,
76 "tipo": "in",
77 "timestamp": "2026-05-04T10:00:00Z",
78 "memo": "x",
79 "movimiento_id": Uuid::new_v4().to_string(),
80 }),
81 )
82 .unwrap();
83 execute_and_log(
84 &exec,
85 &mut live,
86 &mut log,
87 "transfer_between_cajas",
88 &[("source", a), ("dest", b)],
89 json!({
90 "monto": 75_000_i64,
91 "timestamp": "2026-05-04T10:30:00Z",
92 "memo": "xf",
93 "transfer_id": Uuid::new_v4().to_string(),
94 }),
95 )
96 .unwrap();
97
98 let replayed = replay(&log).expect("replay");
99
100 assert_eq!(
101 live.hash_state().unwrap(),
102 replayed.hash_state().unwrap(),
103 "live and replayed stores must hash identically"
104 );
105
106 let _ = std::fs::remove_file(&log_path);
107}
108
109#[test]
110fn drift_is_detectable_via_hash_diff() {
111 let exec = Executor::load_module(treasury_module()).expect("load");
112 let log_path = fresh_log_path();
113 let mut log = EventLog::open(&log_path).unwrap();
114 let mut live = MemoryStore::new();
115
116 let a = Uuid::new_v4();
117 let b = Uuid::new_v4();
118 seed_two_cajas(&exec, &mut live, &mut log, a, b);
119
120 let baseline = live.hash_state().unwrap();
121 let replayed_baseline = replay(&log).unwrap().hash_state().unwrap();
122 assert_eq!(baseline, replayed_baseline);
123
124 // Drift the live store out-of-band — exactly what the drift detector
125 // is meant to catch.
126 live.seed(
127 "Caja",
128 a,
129 json!({"id": a.to_string(), "name": "A", "saldo": 999_999_i64, "currency": "USD"}),
130 );
131
132 let drifted = live.hash_state().unwrap();
133 let log_canonical = replay(&log).unwrap().hash_state().unwrap();
134 assert_ne!(
135 drifted, log_canonical,
136 "the whole point of hash_state: this comparison must surface the drift"
137 );
138
139 let _ = std::fs::remove_file(&log_path);
140}
141
142#[test]
143fn hash_state_is_stable_across_repeated_calls() {
144 // The hash must not drift just because we asked for it twice.
145 // Sounds obvious; protects against an iteration order that depends
146 // on a HashMap's per-process random seed sneaking past the sort.
147 let mut store = MemoryStore::new();
148 for _ in 0..10 {
149 let id = Uuid::new_v4();
150 store.seed(
151 "Caja",
152 id,
153 json!({"id": id.to_string(), "saldo": 100_i64, "currency": "USD"}),
154 );
155 }
156 let h1 = store.hash_state().unwrap();
157 let h2 = store.hash_state().unwrap();
158 assert_eq!(h1, h2, "hash must be a function of state, not call order");
159}