⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc
crates/modules/nakui/core/tests/inventory.rs
History
Who touched what
raw
refactor(nakui-core): KCL → Nickel — kcl_wrapper reemplazado por evaluación in-process
1//! Inventory module integration tests. The point: prove the kernel is
2//! module-agnostic — these tests use the SAME executor code path as
3//! treasury, just pointed at a different module dir, and the conservation
4//! rule is just declarative (Stock.cantidad group_by sku_id).
5
6use std::path::{Path, PathBuf};
7
8use nakui_core::executor::{ExecError, Executor};
9use nakui_core::store::{MemoryStore, Store};
10use serde_json::{Value, json};
11use uuid::Uuid;
12
13fn workspace_root() -> PathBuf {
14 Path::new(env!("CARGO_MANIFEST_DIR"))
15 .parent()
16 .expect("workspace root above core/")
17 .to_path_buf()
18}
19
20fn inventory_module() -> PathBuf {
21 workspace_root().join("modules/inventory")
22}
23
24fn cantidad(store: &MemoryStore, id: Uuid) -> i64 {
25 store
26 .load("Stock", id)
27 .and_then(|v| v.get("cantidad").and_then(Value::as_i64))
28 .expect("stock with cantidad")
29}
30
31fn seed_stock(store: &mut MemoryStore, id: Uuid, sku: &str, cantidad: i64) {
32 store.seed(
33 "Stock",
34 id,
35 json!({
36 "id": id.to_string(),
37 "sku_id": sku,
38 "ubicacion": "test-loc",
39 "cantidad": cantidad,
40 }),
41 );
42}
43
44#[test]
45fn transfer_conserves_units_across_same_sku() {
46 let exec = Executor::load_module(inventory_module()).expect("load module");
47 let mut store = MemoryStore::new();
48
49 let a = Uuid::new_v4();
50 let b = Uuid::new_v4();
51 seed_stock(&mut store, a, "sku-X", 500);
52 seed_stock(&mut store, b, "sku-X", 100);
53
54 let ops = exec
55 .run(
56 &mut store,
57 "transferir_stock",
58 &[("source", a), ("dest", b)],
59 json!({
60 "cantidad": 150_i64,
61 "timestamp": "2026-05-04T00:00:00Z",
62 "transfer_id": Uuid::new_v4().to_string(),
63 }),
64 )
65 .expect("transfer must pass");
66
67 assert_eq!(ops.len(), 3, "2 sets + 1 create = 3 ops");
68 assert_eq!(cantidad(&store, a), 350);
69 assert_eq!(cantidad(&store, b), 250);
70 // Total preserved.
71 assert_eq!(cantidad(&store, a) + cantidad(&store, b), 600);
72}
73
74#[test]
75fn transfer_across_different_skus_is_rejected_by_conservation() {
76 // Construct a buggy synthetic morphism that mimics transfer but skips
77 // the in-script same-sku check. We do this by pointing at a fixture
78 // script that lacks the `throw if source.sku_id != dest.sku_id`.
79 //
80 // Without that fixture we can rely on the production script's `throw`
81 // to fire first — which is itself fine but proves the SCRIPT, not the
82 // KERNEL. To prove the kernel-level conservation works on inventory,
83 // see kernel_guards.rs (treasury) — that test exercises the same
84 // executor logic with Caja.saldo grouped by currency. Here we just
85 // assert the production script rejects cross-SKU.
86 let exec = Executor::load_module(inventory_module()).expect("load module");
87 let mut store = MemoryStore::new();
88
89 let a = Uuid::new_v4();
90 let c = Uuid::new_v4();
91 seed_stock(&mut store, a, "sku-X", 500);
92 seed_stock(&mut store, c, "sku-Y", 200);
93
94 let result = exec.run(
95 &mut store,
96 "transferir_stock",
97 &[("source", a), ("dest", c)],
98 json!({
99 "cantidad": 50_i64,
100 "timestamp": "2026-05-04T00:00:00Z",
101 "transfer_id": Uuid::new_v4().to_string(),
102 }),
103 );
104
105 match result {
106 Err(ExecError::Rhai(_)) => {}
107 other => panic!("expected Rhai (script throw on sku mismatch), got {:?}", other),
108 }
109 assert_eq!(cantidad(&store, a), 500);
110 assert_eq!(cantidad(&store, c), 200);
111}
112
113#[test]
114fn overdraw_transfer_blocked_by_kcl_post_check() {
115 let exec = Executor::load_module(inventory_module()).expect("load module");
116 let mut store = MemoryStore::new();
117
118 let a = Uuid::new_v4();
119 let b = Uuid::new_v4();
120 seed_stock(&mut store, a, "sku-X", 100);
121 seed_stock(&mut store, b, "sku-X", 0);
122
123 let result = exec.run(
124 &mut store,
125 "transferir_stock",
126 &[("source", a), ("dest", b)],
127 json!({
128 "cantidad": 999_i64,
129 "timestamp": "2026-05-04T00:00:00Z",
130 "transfer_id": Uuid::new_v4().to_string(),
131 }),
132 );
133
134 match result {
135 Err(ExecError::SchemaPost { role, entity, .. }) => {
136 assert_eq!(role, "source");
137 assert_eq!(entity, "Stock");
138 }
139 other => panic!("expected SchemaPost on source, got {:?}", other),
140 }
141 assert_eq!(cantidad(&store, a), 100);
142 assert_eq!(cantidad(&store, b), 0);
143}
144
145#[test]
146fn recibir_increases_stock_and_creates_movimiento() {
147 let exec = Executor::load_module(inventory_module()).expect("load module");
148 let mut store = MemoryStore::new();
149
150 let a = Uuid::new_v4();
151 seed_stock(&mut store, a, "sku-X", 100);
152
153 let mov_id = Uuid::new_v4();
154 let ops = exec
155 .run(
156 &mut store,
157 "recibir_stock",
158 &[("stock", a)],
159 json!({
160 "cantidad": 50_i64,
161 "timestamp": "2026-05-04T00:00:00Z",
162 "movimiento_id": mov_id.to_string(),
163 }),
164 )
165 .expect("recibir must pass");
166
167 assert_eq!(ops.len(), 2, "1 set + 1 create");
168 assert_eq!(cantidad(&store, a), 150);
169 assert!(
170 store.load("MovimientoStock", mov_id).is_some(),
171 "movimiento must be persisted"
172 );
173}