Files
brahman/crates/modules/nakui/core/tests/sales.rs
T
Sergio b05de24c24 refactor(nakui-core): KCL → Nickel — kcl_wrapper reemplazado por evaluación in-process
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>
2026-05-10 02:59:48 +00:00

165 lines
5.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 = stock1, 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"
);
}