tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Sutichaykuna

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/nakui/core/tests/state_hash.rs Rust · 159 siq'ikuna · 4.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: absorbe nakui (ERP matemático) en modules/nakui 4d50bfc58 · Sergio ·

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}