//! Cross-language pin: Rust `derive_cache_key` must be byte-identical to the //! Python reference (`cores/mizan-python/.../cache/keys.py`) and to the //! committed cross-language vectors that `tests/afi` and `mizan-ts` also pin. //! //! The Python reference is the oracle: a subprocess mints the key with fixed //! inputs and the Rust output must match exactly. `never if backend == X` — //! one spec, pinned both ways. use mizan_core::derive_cache_key; use serde_json::{json, Value}; use std::collections::BTreeMap; use std::path::PathBuf; use std::process::Command; /// The `tests/afi` dir, whose venv has `mizan_core` + PyJWT installed. fn afi_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../tests/afi") .canonicalize() .expect("tests/afi exists") } /// Run the Python reference via `uv run python -c ` in tests/afi and /// return its single stdout line, trimmed. fn py(code: &str) -> String { let out = Command::new("uv") .args(["run", "python", "-c", code]) .current_dir(afi_dir()) .output() .expect("invoke uv run python"); assert!( out.status.success(), "python reference failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr), ); String::from_utf8(out.stdout).unwrap().trim().to_string() } fn tree(pairs: &[(&str, Value)]) -> BTreeMap { pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect() } #[test] fn committed_vectors_match() { // The exact pins committed in cores/mizan-python/tests/test_keys.py and // backends/mizan-ts/tests — the canonical cross-language anchor. let secret = "test-pin-secret-that-is-32bytes!"; let public = derive_cache_key(secret, "user", &tree(&[("user_id", json!("5"))]), None, 0); assert_eq!( public, "ctx:user:605a1ca5ad5994e9b765c8d1b330474c2a0d51a7b8fbbdc402f992da7ba902f6" ); let scoped = derive_cache_key( secret, "user", &tree(&[("user_id", json!("5"))]), Some("5"), 0, ); assert_eq!( scoped, "ctx:user:30fc08eb46ee4ff2cf7d317e97dca90fd616511e0587304416f71dc863338dc2" ); } #[test] fn matches_python_reference_across_inputs() { // A spread of shapes: multi-param (order-independence), numeric vs string, // bool/null normalization, user-scoped, nonzero rev. let cases: Vec<(&str, BTreeMap, Option<&str>, i64)> = vec![ ("user", tree(&[("user_id", json!("5"))]), None, 0), ("user", tree(&[("user_id", json!("5"))]), Some("5"), 0), ("user", tree(&[("user_id", json!("5"))]), Some("5"), 3), ( "search", tree(&[("q", json!("hello world")), ("page", json!(2))]), None, 0, ), ( "flags", tree(&[("on", json!(true)), ("off", json!(false)), ("nil", json!(null))]), Some("42"), 1, ), ("empty", tree(&[]), None, 0), ( "unicode", tree(&[("name", json!("café—ñ"))]), None, 0, ), ]; for (ctx, params, uid, rev) in cases { let rust = derive_cache_key("pin-secret-xyz", ctx, ¶ms, uid, rev); // Build the Python call: derive_cache_key(secret, ctx, params, user_id, rev). let params_json = serde_json::to_string( ¶ms.iter().map(|(k, v)| (k.clone(), v.clone())).collect::>(), ) .unwrap(); let uid_arg = match uid { Some(u) => format!("'{u}'"), None => "None".to_string(), }; let code = format!( "import json; from mizan_core.cache.keys import derive_cache_key; \ print(derive_cache_key('pin-secret-xyz', {ctx:?}, json.loads(r'''{params_json}'''), {uid_arg}, {rev}))", ); let expected = py(&code); assert_eq!( rust, expected, "cache-key mismatch for ctx={ctx} params={params_json} uid={uid:?} rev={rev}", ); } }