tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definitions

⎇ d3649bfd1a80153652c2a028315369bb4b184024 crates/modules/nakui/core/tests/kernel_guards.rs Rust · 297 lines · 9.4 KB History Who touched what raw

refactor(nakui-core): KCL → Nickel — kcl_wrapper reemplazado por evaluación in-process b05de24c2 · Sergio ·

1//! Regression tests for the kernel's enforcement layers.
2//!
3//! Each test runs a deliberately-broken morphism that should be rejected by
4//! a *specific* layer of the executor pipeline. After every rejection we also
5//! assert the store is untouched — the kernel must never half-apply a delta.
6//!
7//! Layers exercised (in pipeline order):
8//!   1. CapabilityViolation  (untracked write)
9//!   2. ConservationViolation (delta sum != 0)
10//!   3. SchemaPostCreate         (created record fails its schema)
11
12use std::path::{Path, PathBuf};
13
14use nakui_core::executor::{ExecError, Executor};
15use nakui_core::graph::ManifestGraph;
16use nakui_core::manifest::{
17    ConserveRule, Invariants, Manifest, MorphismInput, MorphismSpec,
18};
19use nakui_core::rhai_executor::RhaiExecutor;
20use nakui_core::store::{MemoryStore, Store};
21use serde_json::{Value, json};
22use uuid::Uuid;
23
24fn workspace_root() -> PathBuf {
25    Path::new(env!("CARGO_MANIFEST_DIR"))
26        .parent()
27        .expect("workspace root above core/")
28        .to_path_buf()
29}
30
31fn fixtures_dir() -> PathBuf {
32    Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
33}
34
35fn build_executor(spec: MorphismSpec) -> Executor {
36    let manifest = Manifest {
37        module: "kernel_guards_test".into(),
38        schemas: vec![],
39        morphisms: vec![spec],
40    };
41    let graph = ManifestGraph::build(&manifest).expect("graph builds");
42    Executor {
43        manifest,
44        graph,
45        // module_dir is where script paths resolve; we point it at fixtures.
46        module_dir: fixtures_dir(),
47        // schema_path stays on the real treasury schema so we exercise the
48        // production check blocks. `owned_bundle: false` so Drop leaves it
49        // alone — it belongs to the source tree.
50        schema_path: workspace_root().join("modules/treasury/schema.ncl"),
51        rhai: RhaiExecutor::new_sandboxed(),
52        owned_bundle: false,
53        // Inline-built executors don't go through `load_module`, so they
54        // have no schema-hash cache. These guard tests don't write to a
55        // log, so verify_log never runs against this executor.
56        schema_hashes: std::collections::HashMap::new(),
57        schema_bundle_hash: [0u8; 32],
58    }
59}
60
61fn seed_caja(store: &mut MemoryStore, id: Uuid, name: &str, saldo: i64, currency: &str) {
62    store.seed(
63        "Caja",
64        id,
65        json!({
66            "id": id.to_string(),
67            "name": name,
68            "saldo": saldo,
69            "currency": currency,
70        }),
71    );
72}
73
74fn caja_saldo(store: &MemoryStore, id: Uuid) -> i64 {
75    store
76        .load("Caja", id)
77        .and_then(|v| v.get("saldo").and_then(Value::as_i64))
78        .expect("caja with saldo")
79}
80
81#[test]
82fn capability_violation_blocks_write_to_untracked_caja() {
83    let spec = MorphismSpec {
84        name: "evil_capability".into(),
85        inputs: vec![MorphismInput {
86            role: "caja".into(),
87            entity: "Caja".into(),
88        }],
89        reads: vec!["caja.saldo".into()],
90        writes: vec!["caja.saldo".into()],
91        invariants: Invariants::default(),
92        depends_on: vec![],
93        script: "capability_violation.rhai".into(),
94    };
95    let exec = build_executor(spec);
96
97    let mut store = MemoryStore::new();
98    let caja_id = Uuid::new_v4();
99    let phantom_id = Uuid::new_v4();
100    seed_caja(&mut store, caja_id, "tracked", 100_000, "USD");
101    seed_caja(&mut store, phantom_id, "phantom", 100_000, "USD");
102
103    let params = json!({ "phantom_id": phantom_id.to_string() });
104
105    let result = exec.run(&mut store, "evil_capability", &[("caja", caja_id)], params);
106
107    match result {
108        Err(ExecError::CapabilityViolation { token, .. }) => {
109            assert!(
110                token.contains("untracked"),
111                "expected token to flag untracked id, got `{}`",
112                token
113            );
114        }
115        other => panic!("expected CapabilityViolation, got {:?}", other),
116    }
117
118    // Neither caja moved.
119    assert_eq!(caja_saldo(&store, caja_id), 100_000);
120    assert_eq!(caja_saldo(&store, phantom_id), 100_000);
121}
122
123#[test]
124fn conservation_violation_blocks_unbalanced_transfer() {
125    let spec = MorphismSpec {
126        name: "evil_conservation".into(),
127        inputs: vec![
128            MorphismInput {
129                role: "source".into(),
130                entity: "Caja".into(),
131            },
132            MorphismInput {
133                role: "dest".into(),
134                entity: "Caja".into(),
135            },
136        ],
137        reads: vec![
138            "source.saldo".into(),
139            "source.currency".into(),
140            "dest.saldo".into(),
141            "dest.currency".into(),
142        ],
143        writes: vec!["source.saldo".into(), "dest.saldo".into()],
144        invariants: Invariants {
145            conserve: vec![ConserveRule {
146                entity: "Caja".into(),
147                field: "saldo".into(),
148                group_by: Some("currency".into()),
149            }],
150        },
151        depends_on: vec![],
152        script: "conservation_violation.rhai".into(),
153    };
154    let exec = build_executor(spec);
155
156    let mut store = MemoryStore::new();
157    let source = Uuid::new_v4();
158    let dest = Uuid::new_v4();
159    seed_caja(&mut store, source, "A", 200_000, "USD");
160    seed_caja(&mut store, dest, "B", 50_000, "USD");
161
162    let result = exec.run(
163        &mut store,
164        "evil_conservation",
165        &[("source", source), ("dest", dest)],
166        json!({}),
167    );
168
169    match result {
170        Err(ExecError::ConservationViolation {
171            entity,
172            field,
173            total,
174            ..
175        }) => {
176            assert_eq!(entity, "Caja");
177            assert_eq!(field, "saldo");
178            assert_eq!(total, -101, "expected Δ = -100 + -1 = -101");
179        }
180        other => panic!("expected ConservationViolation, got {:?}", other),
181    }
182
183    assert_eq!(caja_saldo(&store, source), 200_000);
184    assert_eq!(caja_saldo(&store, dest), 50_000);
185}
186
187#[test]
188fn capability_rejects_entity_mismatch_on_tracked_id() {
189    // The script writes `Stock.cantidad` using the Caja's UUID. The id is
190    // tracked (it's the caja role's id) but the entity differs — the
191    // capability layer must catch this regardless of UUID coincidence.
192    let spec = MorphismSpec {
193        name: "evil_entity_mismatch".into(),
194        inputs: vec![MorphismInput {
195            role: "caja".into(),
196            entity: "Caja".into(),
197        }],
198        reads: vec!["caja.saldo".into()],
199        writes: vec!["caja.saldo".into()],
200        invariants: Invariants::default(),
201        depends_on: vec![],
202        script: "entity_mismatch.rhai".into(),
203    };
204    let exec = build_executor(spec);
205
206    let mut store = MemoryStore::new();
207    let caja_id = Uuid::new_v4();
208    seed_caja(&mut store, caja_id, "tracked", 100_000, "USD");
209
210    let result = exec.run(&mut store, "evil_entity_mismatch", &[("caja", caja_id)], json!({}));
211
212    match result {
213        Err(ExecError::CapabilityViolation { token, .. }) => {
214            assert!(
215                token.contains("entity-mismatch"),
216                "expected entity-mismatch token, got `{}`",
217                token
218            );
219        }
220        other => panic!("expected CapabilityViolation, got {:?}", other),
221    }
222    assert_eq!(caja_saldo(&store, caja_id), 100_000);
223}
224
225#[test]
226fn delete_primary_skips_post_check_and_removes_record() {
227    // A morphism that deletes its primary input must succeed without the
228    // post-check running against a stale-then-stripped state.
229    let spec = MorphismSpec {
230        name: "delete_caja".into(),
231        inputs: vec![MorphismInput {
232            role: "caja".into(),
233            entity: "Caja".into(),
234        }],
235        reads: vec![],
236        writes: vec!["Caja".into()],
237        invariants: Invariants::default(),
238        depends_on: vec![],
239        script: "delete_primary.rhai".into(),
240    };
241    let exec = build_executor(spec);
242
243    let mut store = MemoryStore::new();
244    let caja_id = Uuid::new_v4();
245    seed_caja(&mut store, caja_id, "doomed", 100_000, "USD");
246
247    let ops = exec
248        .run(&mut store, "delete_caja", &[("caja", caja_id)], json!({}))
249        .expect("delete must succeed");
250
251    assert_eq!(ops.len(), 1);
252    assert!(matches!(&ops[0], nakui_core::delta::FieldOp::Delete { .. }));
253    assert!(
254        store.load("Caja", caja_id).is_none(),
255        "Caja must be gone after Delete"
256    );
257}
258
259#[test]
260fn bad_created_record_blocks_negative_movimiento() {
261    let spec = MorphismSpec {
262        name: "evil_create".into(),
263        inputs: vec![MorphismInput {
264            role: "caja".into(),
265            entity: "Caja".into(),
266        }],
267        reads: vec!["caja.saldo".into()],
268        writes: vec!["Movimiento".into()],
269        invariants: Invariants::default(),
270        depends_on: vec![],
271        script: "bad_created_record.rhai".into(),
272    };
273    let exec = build_executor(spec);
274
275    let mut store = MemoryStore::new();
276    let caja_id = Uuid::new_v4();
277    seed_caja(&mut store, caja_id, "A", 100_000, "USD");
278    let mov_id = Uuid::new_v4();
279
280    let params = json!({ "mov_id": mov_id.to_string() });
281
282    let result = exec.run(&mut store, "evil_create", &[("caja", caja_id)], params);
283
284    match result {
285        Err(ExecError::SchemaPostCreate { entity, .. }) => {
286            assert_eq!(entity, "Movimiento");
287        }
288        other => panic!("expected SchemaPostCreate, got {:?}", other),
289    }
290
291    // Caja unchanged, Movimiento never landed.
292    assert_eq!(caja_saldo(&store, caja_id), 100_000);
293    assert!(
294        store.load("Movimiento", mov_id).is_none(),
295        "Movimiento must not be persisted"
296    );
297}