The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.1 KiB
Rust
121 lines
4.1 KiB
Rust
//! 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 <code>` 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<String, Value> {
|
|
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<String, Value>, 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::<serde_json::Map<_, _>>(),
|
|
)
|
|
.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}",
|
|
);
|
|
}
|
|
}
|