b05de24c24
Cierra el plan original. El motor de validación de entities deja de shellear el binario externo `kcl` y pasa a evaluar Nickel contracts in-process via la dep nickel-lang (la misma que usa el brazo de cards). Los 3 schemas de sales/inventory/treasury migran de .k a .ncl. nakui-core: - Nueva dep nickel-lang = "2.0.0". - Borrado kcl_wrapper.rs. - Nuevo nickel_validator.rs con vet(schema_path, state, entity) que evalúa `let bundle = (import "<schema>") in (std.deserialize 'Json m%%"<json>"%%) | bundle.<entity>`. - executor.rs: KclError → NickelError, KclPre/Post/PostCreate → SchemaPre/Post/PostCreate, kcl_check → validate_entity. build_schema_bundle ahora emite `(import "X") & (import "Y") & ...` en lugar de concatenar bytes (cada .ncl es expresión completa). - manifest.rs: default schema "schema.ncl", extract_schema_names reescrito para sintaxis Nickel record (CapitalCase keys con 2-space indent). Schemas migrados: - sales/schema.ncl: Venta con std.contract.Sequence [record, from_predicate] para combinar shape + invariante cross-field (total == cantidad * precio_unitario). El patrón directo `record | from_predicate` rebota con "missing definition" porque el predicate evalúa antes de que el value populate el record; documentado en cada schema. - inventory/schema.ncl, treasury/schema.ncl: idem. - 3 schema.k viejos borrados; sales/nsmc.json paths actualizados. Tests: refs Kcl* renombradas; paths .k → .ncl; tests inline que escribían schema.k cambian a schema.ncl con sintaxis Nickel. 84 tests verdes en nakui-core. Doc-only borrados: - crates/core/ente-card/schema/card.k (REFERENCE ONLY). - crates/core/ente-brain/schema/rule.k (REFERENCE ONLY). Beneficios: sin dep externa al binario `kcl` (build CI limpio), errores Nickel en línea con caret pointing al field, mismo motor que cards (una dep para todo el repo), sin tempfile JSON intermedio. Cierra el plan original yahweh + KCL + card.k. Pendientes salen de nuevo trabajo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
165 lines
5.0 KiB
Rust
165 lines
5.0 KiB
Rust
//! Cross-module integration tests. The `sales` module references entities
|
||
//! defined in `treasury` and `inventory` via its manifest's `schemas` list.
|
||
//! These tests assert:
|
||
//! - The kernel correctly bundles multiple .k files at module load.
|
||
//! - Per-entity KCL post-checks fire against the right schema even when
|
||
//! three are concatenated.
|
||
//! - A non-conserving morphism (sale = stock−1, caja+price) passes the
|
||
//! kernel cleanly because no `invariants.conserve` was declared.
|
||
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use nakui_core::executor::{ExecError, Executor};
|
||
use nakui_core::store::{MemoryStore, Store};
|
||
use serde_json::{Value, json};
|
||
use uuid::Uuid;
|
||
|
||
fn workspace_root() -> PathBuf {
|
||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||
.parent()
|
||
.expect("workspace root above core/")
|
||
.to_path_buf()
|
||
}
|
||
|
||
fn sales_module() -> PathBuf {
|
||
workspace_root().join("modules/sales")
|
||
}
|
||
|
||
fn caja_saldo(store: &MemoryStore, id: Uuid) -> i64 {
|
||
store
|
||
.load("Caja", id)
|
||
.and_then(|v| v.get("saldo").and_then(Value::as_i64))
|
||
.expect("caja with saldo")
|
||
}
|
||
|
||
fn stock_cantidad(store: &MemoryStore, id: Uuid) -> i64 {
|
||
store
|
||
.load("Stock", id)
|
||
.and_then(|v| v.get("cantidad").and_then(Value::as_i64))
|
||
.expect("stock with cantidad")
|
||
}
|
||
|
||
fn seed(store: &mut MemoryStore) -> (Uuid, Uuid) {
|
||
let stock = Uuid::new_v4();
|
||
let caja = Uuid::new_v4();
|
||
store.seed(
|
||
"Stock",
|
||
stock,
|
||
json!({
|
||
"id": stock.to_string(),
|
||
"sku_id": "sku-test",
|
||
"ubicacion": "test-loc",
|
||
"cantidad": 500_i64,
|
||
}),
|
||
);
|
||
store.seed(
|
||
"Caja",
|
||
caja,
|
||
json!({
|
||
"id": caja.to_string(),
|
||
"name": "Caja Test",
|
||
"saldo": 1_000_000_i64,
|
||
"currency": "USD",
|
||
}),
|
||
);
|
||
(stock, caja)
|
||
}
|
||
|
||
#[test]
|
||
fn sale_decreases_stock_and_increases_caja() {
|
||
let exec = Executor::load_module(sales_module()).expect("load module");
|
||
let mut store = MemoryStore::new();
|
||
let (stock, caja) = seed(&mut store);
|
||
|
||
let venta_id = Uuid::new_v4();
|
||
let ops = exec
|
||
.run(
|
||
&mut store,
|
||
"vender",
|
||
&[("stock", stock), ("caja", caja)],
|
||
json!({
|
||
"cantidad": 100_i64,
|
||
"precio_unitario": 5_000_i64,
|
||
"timestamp": "2026-05-04T10:00:00Z",
|
||
"venta_id": venta_id.to_string(),
|
||
}),
|
||
)
|
||
.expect("sale must succeed");
|
||
|
||
assert_eq!(ops.len(), 3, "2 sets + 1 create");
|
||
assert_eq!(stock_cantidad(&store, stock), 400);
|
||
assert_eq!(caja_saldo(&store, caja), 1_500_000);
|
||
|
||
let venta = store
|
||
.load("Venta", venta_id)
|
||
.expect("venta must be persisted");
|
||
assert_eq!(venta.get("total").and_then(Value::as_i64), Some(500_000));
|
||
assert_eq!(venta.get("cantidad").and_then(Value::as_i64), Some(100));
|
||
assert_eq!(
|
||
venta.get("currency").and_then(Value::as_str),
|
||
Some("USD")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn overdraw_stock_rejected_by_inventory_post_check() {
|
||
let exec = Executor::load_module(sales_module()).expect("load module");
|
||
let mut store = MemoryStore::new();
|
||
let (stock, caja) = seed(&mut store);
|
||
|
||
let result = exec.run(
|
||
&mut store,
|
||
"vender",
|
||
&[("stock", stock), ("caja", caja)],
|
||
json!({
|
||
"cantidad": 9999_i64,
|
||
"precio_unitario": 100_i64,
|
||
"timestamp": "2026-05-04T10:00:00Z",
|
||
"venta_id": Uuid::new_v4().to_string(),
|
||
}),
|
||
);
|
||
|
||
match result {
|
||
Err(ExecError::SchemaPost { role, entity, .. }) => {
|
||
assert_eq!(role, "stock");
|
||
assert_eq!(entity, "Stock");
|
||
}
|
||
other => panic!("expected SchemaPost on stock, got {:?}", other),
|
||
}
|
||
assert_eq!(stock_cantidad(&store, stock), 500);
|
||
assert_eq!(caja_saldo(&store, caja), 1_000_000);
|
||
}
|
||
|
||
#[test]
|
||
fn venta_total_invariant_caught_when_corrupted() {
|
||
// The Venta schema's check block enforces `total == cantidad * precio`.
|
||
// The production script always produces a consistent total. To prove
|
||
// the schema check fires, this test would need a buggy script — that's
|
||
// covered indirectly: if anyone breaks the script, this fails. For now
|
||
// we just confirm a clean sale's Venta passes its own invariant.
|
||
let exec = Executor::load_module(sales_module()).expect("load module");
|
||
let mut store = MemoryStore::new();
|
||
let (stock, caja) = seed(&mut store);
|
||
let venta_id = Uuid::new_v4();
|
||
|
||
exec.run(
|
||
&mut store,
|
||
"vender",
|
||
&[("stock", stock), ("caja", caja)],
|
||
json!({
|
||
"cantidad": 7_i64,
|
||
"precio_unitario": 13_i64,
|
||
"timestamp": "2026-05-04T10:00:00Z",
|
||
"venta_id": venta_id.to_string(),
|
||
}),
|
||
)
|
||
.expect("sale must pass");
|
||
|
||
let venta = store.load("Venta", venta_id).expect("venta");
|
||
assert_eq!(
|
||
venta.get("total").and_then(Value::as_i64),
|
||
Some(7 * 13),
|
||
"Venta.total must equal cantidad * precio"
|
||
);
|
||
}
|