tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/nakui/core/tests/manifest_validation.rs Rust · 283 líneas · 8.6 KB Historia Quién tocó qué crudo

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

1//! Manifest::validate covers the contract between authors (humans or AI)
2//! and Nakui. Each test inline-builds a manifest with one specific defect
3//! and asserts the right diagnostic fires.
4//!
5//! Most tests point at `modules/treasury/` so the schema/script paths
6//! resolve. Two tests need a synthetic tempdir to express their defect
7//! (missing schema file, duplicate schema across files).
8
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use nakui_core::executor::Executor;
13use nakui_core::manifest::{
14    ConserveRule, Invariants, Manifest, MorphismInput, MorphismSpec, ValidationError,
15};
16use uuid::Uuid;
17
18fn workspace_root() -> PathBuf {
19    Path::new(env!("CARGO_MANIFEST_DIR"))
20        .parent()
21        .expect("workspace root above core/")
22        .to_path_buf()
23}
24
25fn treasury_dir() -> PathBuf {
26    workspace_root().join("modules/treasury")
27}
28
29fn caja_input() -> MorphismInput {
30    MorphismInput {
31        role: "caja".into(),
32        entity: "Caja".into(),
33    }
34}
35
36fn baseline_morphism() -> MorphismSpec {
37    MorphismSpec {
38        name: "test_op".into(),
39        inputs: vec![caja_input()],
40        reads: vec!["caja.saldo".into()],
41        writes: vec!["caja.saldo".into()],
42        invariants: Invariants::default(),
43        depends_on: vec![],
44        script: "morphisms/register_cash_move.rhai".into(),
45    }
46}
47
48fn baseline_manifest() -> Manifest {
49    Manifest {
50        module: "test".into(),
51        schemas: vec![],
52        morphisms: vec![baseline_morphism()],
53    }
54}
55
56#[test]
57fn production_modules_validate_clean() {
58    for name in ["treasury", "inventory", "sales"] {
59        let dir = workspace_root().join("modules").join(name);
60        let manifest = Manifest::load(&dir.join("nsmc.json"))
61            .unwrap_or_else(|e| panic!("load {}: {}", name, e));
62        manifest
63            .validate(&dir)
64            .unwrap_or_else(|e| panic!("validate {}: {}", name, e));
65    }
66}
67
68#[test]
69fn rejects_duplicate_morphism_name() {
70    let mut m = baseline_manifest();
71    m.morphisms.push(baseline_morphism()); // same name as the first
72    match m.validate(&treasury_dir()) {
73        Err(ValidationError::DuplicateMorphism(name)) => assert_eq!(name, "test_op"),
74        other => panic!("expected DuplicateMorphism, got {:?}", other),
75    }
76}
77
78#[test]
79fn rejects_duplicate_role_within_morphism() {
80    let mut m = baseline_manifest();
81    m.morphisms[0].inputs.push(caja_input()); // same role twice
82    match m.validate(&treasury_dir()) {
83        Err(ValidationError::DuplicateRole { morphism, role }) => {
84            assert_eq!(morphism, "test_op");
85            assert_eq!(role, "caja");
86        }
87        other => panic!("expected DuplicateRole, got {:?}", other),
88    }
89}
90
91#[test]
92fn rejects_input_unknown_entity() {
93    let mut m = baseline_manifest();
94    m.morphisms[0].inputs[0].entity = "Banana".into();
95    match m.validate(&treasury_dir()) {
96        Err(ValidationError::InputUnknownEntity {
97            morphism,
98            entity,
99            known,
100        }) => {
101            assert_eq!(morphism, "test_op");
102            assert_eq!(entity, "Banana");
103            assert!(known.contains(&"Caja".to_string()));
104        }
105        other => panic!("expected InputUnknownEntity, got {:?}", other),
106    }
107}
108
109#[test]
110fn rejects_writes_unknown_role() {
111    let mut m = baseline_manifest();
112    m.morphisms[0].writes = vec!["ghost.saldo".into()];
113    match m.validate(&treasury_dir()) {
114        Err(ValidationError::WritesUnknownRole {
115            morphism,
116            token,
117            role,
118            ..
119        }) => {
120            assert_eq!(morphism, "test_op");
121            assert_eq!(token, "ghost.saldo");
122            assert_eq!(role, "ghost");
123        }
124        other => panic!("expected WritesUnknownRole, got {:?}", other),
125    }
126}
127
128#[test]
129fn rejects_writes_unknown_entity() {
130    let mut m = baseline_manifest();
131    m.morphisms[0].writes = vec!["BananaSplit".into()];
132    match m.validate(&treasury_dir()) {
133        Err(ValidationError::WritesUnknownEntity { morphism, token }) => {
134            assert_eq!(morphism, "test_op");
135            assert_eq!(token, "BananaSplit");
136        }
137        other => panic!("expected WritesUnknownEntity, got {:?}", other),
138    }
139}
140
141#[test]
142fn rejects_conserve_unknown_entity() {
143    let mut m = baseline_manifest();
144    m.morphisms[0].invariants.conserve = vec![ConserveRule {
145        entity: "Banana".into(),
146        field: "x".into(),
147        group_by: None,
148    }];
149    match m.validate(&treasury_dir()) {
150        Err(ValidationError::ConserveUnknownEntity { morphism, entity }) => {
151            assert_eq!(morphism, "test_op");
152            assert_eq!(entity, "Banana");
153        }
154        other => panic!("expected ConserveUnknownEntity, got {:?}", other),
155    }
156}
157
158#[test]
159fn rejects_depends_on_unknown_morphism() {
160    let mut m = baseline_manifest();
161    m.morphisms[0].depends_on = vec!["ghost_morphism".into()];
162    match m.validate(&treasury_dir()) {
163        Err(ValidationError::DependsOnUnknown { morphism, dep }) => {
164            assert_eq!(morphism, "test_op");
165            assert_eq!(dep, "ghost_morphism");
166        }
167        other => panic!("expected DependsOnUnknown, got {:?}", other),
168    }
169}
170
171#[test]
172fn rejects_missing_script() {
173    let mut m = baseline_manifest();
174    m.morphisms[0].script = "morphisms/ghost.rhai".into();
175    match m.validate(&treasury_dir()) {
176        Err(ValidationError::ScriptMissing { morphism, script, .. }) => {
177            assert_eq!(morphism, "test_op");
178            assert_eq!(script, "morphisms/ghost.rhai");
179        }
180        other => panic!("expected ScriptMissing, got {:?}", other),
181    }
182}
183
184#[test]
185fn rejects_missing_schema_file() {
186    let mut m = baseline_manifest();
187    m.schemas = vec!["nonexistent.k".into()];
188    match m.validate(&treasury_dir()) {
189        Err(ValidationError::SchemaFileMissing { path, .. }) => {
190            assert_eq!(path, "nonexistent.k");
191        }
192        other => panic!("expected SchemaFileMissing, got {:?}", other),
193    }
194}
195
196#[test]
197fn rejects_duplicate_schema_across_files() {
198    // Synthesize a tempdir with two .ncl files that both declare
199    // `Caja` en el record top-level.
200    let tmp = std::env::temp_dir().join(format!("nakui_dup_{}", Uuid::new_v4()));
201    fs::create_dir_all(&tmp).unwrap();
202    fs::create_dir_all(tmp.join("morphisms")).unwrap();
203    fs::write(
204        tmp.join("a.ncl"),
205        "{\n  Caja = { saldo | Number },\n}\n",
206    )
207    .unwrap();
208    fs::write(
209        tmp.join("b.ncl"),
210        "{\n  Caja = { monto | Number },\n}\n",
211    )
212    .unwrap();
213    fs::write(tmp.join("morphisms/op.rhai"), "[]").unwrap();
214
215    let m = Manifest {
216        module: "dup".into(),
217        schemas: vec!["a.ncl".into(), "b.ncl".into()],
218        morphisms: vec![MorphismSpec {
219            name: "op".into(),
220            inputs: vec![MorphismInput {
221                role: "caja".into(),
222                entity: "Caja".into(),
223            }],
224            reads: vec![],
225            writes: vec![],
226            invariants: Invariants::default(),
227            depends_on: vec![],
228            script: "morphisms/op.rhai".into(),
229        }],
230    };
231
232    match m.validate(&tmp) {
233        Err(ValidationError::DuplicateSchema { name, files }) => {
234            assert_eq!(name, "Caja");
235            assert!(files.contains(&"a.ncl".to_string()));
236            assert!(files.contains(&"b.ncl".to_string()));
237        }
238        other => panic!("expected DuplicateSchema, got {:?}", other),
239    }
240
241    let _ = fs::remove_dir_all(&tmp);
242}
243
244#[test]
245fn executor_load_module_runs_validation() {
246    // Synthesize a module dir whose manifest references a missing script —
247    // load_module must surface ManifestValidation, not a runtime kernel error.
248    let tmp = std::env::temp_dir().join(format!("nakui_bad_{}", Uuid::new_v4()));
249    fs::create_dir_all(&tmp).unwrap();
250    fs::write(
251        tmp.join("schema.ncl"),
252        "{\n  Caja = {\n    saldo | std.contract.from_predicate (fun n => std.is_number n && n >= 0),\n  },\n}\n",
253    )
254    .unwrap();
255    fs::write(
256        tmp.join("nsmc.json"),
257        r#"{
258            "module": "bad",
259            "morphisms": [{
260                "name": "op",
261                "inputs": [{"role": "caja", "entity": "Caja"}],
262                "reads": [],
263                "writes": ["caja.saldo"],
264                "depends_on": [],
265                "script": "morphisms/missing.rhai"
266            }]
267        }"#,
268    )
269    .unwrap();
270
271    let err = match Executor::load_module(&tmp) {
272        Ok(_) => panic!("must fail validation"),
273        Err(e) => e,
274    };
275    let msg = err.to_string();
276    assert!(
277        msg.contains("validation") && msg.contains("missing.rhai"),
278        "expected validation diagnostic naming the missing script, got `{}`",
279        msg
280    );
281
282    let _ = fs::remove_dir_all(&tmp);
283}