AFI parity: close all 35 gaps — every adapter wires every AFI-common capability
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>
This commit is contained in:
90
cores/mizan-rust/tests/invalidate_header_pin.rs
Normal file
90
cores/mizan-rust/tests/invalidate_header_pin.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! Cross-language pin: Rust `format_invalidate_header` must be byte-identical
|
||||
//! to `cores/mizan-python/.../invalidation.py::format_invalidate_header`.
|
||||
//!
|
||||
//! The `X-Mizan-Invalidate` header is co-equal with the JSON body channel in
|
||||
//! the spec; Edge parses it to purge. The Python reference is the oracle.
|
||||
|
||||
use mizan_core::{format_invalidate_header, InvalidationTarget};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn afi_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../tests/afi")
|
||||
.canonicalize()
|
||||
.expect("tests/afi exists")
|
||||
}
|
||||
|
||||
fn py_header(json_list: &str) -> String {
|
||||
let code = format!(
|
||||
"import json; from mizan_core.invalidation import format_invalidate_header; \
|
||||
print(format_invalidate_header(json.loads(r'''{json_list}''')))",
|
||||
);
|
||||
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: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
// Trim the trailing newline only — the header value itself may be empty.
|
||||
let s = String::from_utf8(out.stdout).unwrap();
|
||||
s.strip_suffix('\n').unwrap_or(&s).to_string()
|
||||
}
|
||||
|
||||
fn scoped(ctx: &str, params: &[(&str, serde_json::Value)]) -> InvalidationTarget {
|
||||
InvalidationTarget::ScopedContext {
|
||||
context: ctx.to_string(),
|
||||
params: params.iter().map(|(k, v)| (k.to_string(), v.clone())).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_python_reference() {
|
||||
let cases: Vec<(Vec<InvalidationTarget>, &str)> = vec![
|
||||
(vec![InvalidationTarget::Context("user".into())], r#"["user"]"#),
|
||||
(
|
||||
vec![
|
||||
InvalidationTarget::Context("user".into()),
|
||||
InvalidationTarget::Context("notifications".into()),
|
||||
],
|
||||
r#"["user", "notifications"]"#,
|
||||
),
|
||||
(
|
||||
vec![scoped("user", &[("user_id", json!(5))])],
|
||||
r#"[{"context": "user", "params": {"user_id": 5}}]"#,
|
||||
),
|
||||
(
|
||||
vec![scoped("search", &[("q", json!("hello world"))])],
|
||||
r#"[{"context": "search", "params": {"q": "hello world"}}]"#,
|
||||
),
|
||||
(
|
||||
// Multiple params → sorted by key, semicolon-joined.
|
||||
vec![scoped("u", &[("b", json!("2")), ("a", json!("1"))])],
|
||||
r#"[{"context": "u", "params": {"b": "2", "a": "1"}}]"#,
|
||||
),
|
||||
(
|
||||
// Special chars that must percent-encode: &, =, /, space, unicode.
|
||||
vec![scoped("c", &[("k", json!("a&b=c/d e—ñ"))])],
|
||||
r#"[{"context": "c", "params": {"k": "a&b=c/d e—ñ"}}]"#,
|
||||
),
|
||||
(
|
||||
// Mixed bare + scoped.
|
||||
vec![
|
||||
scoped("user", &[("user_id", json!(5))]),
|
||||
InvalidationTarget::Context("notifications".into()),
|
||||
],
|
||||
r#"[{"context": "user", "params": {"user_id": 5}}, "notifications"]"#,
|
||||
),
|
||||
];
|
||||
|
||||
for (targets, json_list) in cases {
|
||||
let rust = format_invalidate_header(&targets);
|
||||
let expected = py_header(json_list);
|
||||
assert_eq!(rust, expected, "header mismatch for {json_list}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user