//! 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::{json, Value}; 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" ); }