//! The standing conformance battery: the verification rows the core's //! consumers and every later wave lean on, in one home so future waves //! grow rows here rather than scattering them. Rows the wave-1 unit //! tests and the wave-2 proof binaries already pin golden output are //! re-exercised here, not restated: the battery reuses the proof //! fixture files under `tests/fixtures/ssr_proof/` and //! `tests/fixtures/code_proof/` and asserts the obligations themselves //! (strict-mode matrix, error-surface completeness, two-sink equality) //! against fresh renders through the public API. use alkgen::{Error, EscapePolicy, TemplateSet}; use serde_json::json; /// Register a well-formed fixture template, unwrapping registration /// only for fixtures the battery authors itself in-file; rows under /// test always go through the typed error surface. fn fixture_template(set: &mut TemplateSet, name: &str, text: &str) { set.register_template(name, text) .expect("battery fixture template should be well-formed"); } fn fixture_partial(set: &mut TemplateSet, name: &str, text: &str) { set.register_partial(name, text) .expect("battery fixture partial should be well-formed"); } /// The shared two-sink row helper: the same context through both /// sinks, with the sink outputs asserted byte-identical. fn assert_both_sinks(set: &TemplateSet, name: &str, context: &T) { let string_sink = set .render(name, context) .unwrap_or_else(|_| panic!("{name} should render by the string sink")); let mut byte_sink = Vec::new(); set.render_to_write(name, context, &mut byte_sink) .unwrap_or_else(|_| panic!("{name} should render by the write sink")); assert_eq!( &byte_sink[..], string_sink.as_bytes(), "both sinks must agree byte for byte on {name}" ); } /// The context shape the hostile-values row renders over. The key /// `raw` is avoided on purpose: handlebars treats that name as a /// keyword-shaped expression and never resolves it, and the row /// should stay inside ordinary data. const HOSTILE_TEMPLATE: &str = "a{{value}}b{{& trusted}}c"; // ------------------------------------------------------------------ // Strict-mode behavior matrix (ADR-002 decision 2, core.md) // // Every row: a missing context variable is a typed `Error::Render` // through both sinks, never silent output. Top-level variables and // variables nested inside `{{#each}}` and `{{#if}}` branches are each // covered, and the write sink's on-failure behavior is stated by its // row: a typed error, with the partial bytes already streamed left in // place (no atomicity; consumers that need it buffer first). // ------------------------------------------------------------------ #[test] fn strict_matrix_missing_top_level_variable_fails_typed_through_both_sinks() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut set, "tpl", "hello {{name}}"); let context = json!({}); let string_err = set .render("tpl", &context) .expect_err("missing variable must fail the string sink, never render silently"); assert!( matches!(string_err, Error::Render(_)), "the string sink's failure must be a typed Error::Render, got: {string_err:?}" ); let mut sink = Vec::new(); let write_err = set .render_to_write("tpl", &context, &mut sink) .expect_err("missing variable must fail the write sink, never render silently"); assert!( matches!(write_err, Error::Render(_)), "the write sink's failure must be a typed Error::Render, got: {write_err:?}" ); assert_eq!( String::from_utf8_lossy(&sink), "hello ", "the write sink streams incrementally: bytes rendered before the failure \ (the literal text ahead of the variable) stay in the target; consumers \ needing atomicity buffer first (the wave-1 contract)" ); let present = set .render("tpl", &json!({ "name": "world" })) .expect("present variables still render"); assert_eq!(present, "hello world"); } #[test] fn strict_matrix_missing_variable_inside_each_branch_fails_typed_through_both_sinks() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template( &mut set, "cells", "{{#each items}}{{label}}{{/each}}", ); let context = json!({ "items": [ { "label": "fine" }, {} ] }); let string_err = set .render("cells", &context) .expect_err("a missing variable inside an {{#each}} body must fail the string sink"); assert!(matches!(string_err, Error::Render(_))); assert!( format!("{string_err}").contains("label"), "the strict-mode detail names the missing variable" ); let mut sink = Vec::new(); let write_err = set .render_to_write("cells", &context, &mut sink) .expect_err("a missing variable inside an {{#each}} body must fail the write sink"); assert!(matches!(write_err, Error::Render(_))); } #[test] fn strict_matrix_missing_variable_in_if_then_branch_fails_typed_through_both_sinks() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template( &mut set, "banner", "{{#if flag}}
{{banner_text}}
{{/if}}", ); let then_context = json!({ "flag": true }); let string_err = set .render("banner", &then_context) .expect_err("a missing variable in a taken {{#if}} branch must fail the string sink"); assert!(matches!(string_err, Error::Render(_))); assert!(format!("{string_err}").contains("banner_text")); let mut sink = Vec::new(); let write_err = set .render_to_write("banner", &then_context, &mut sink) .expect_err("a missing variable in a taken {{#if}} branch must fail the write sink"); assert!(matches!(write_err, Error::Render(_))); } #[test] fn strict_matrix_missing_variable_in_if_else_branch_fails_typed_through_both_sinks() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template( &mut set, "banner", "{{#if flag}}
{{banner_text}}
{{else}}

{{fallback}}

{{/if}}", ); let else_context = json!({ "flag": false }); let string_err = set .render("banner", &else_context) .expect_err("a missing variable in a taken {{else}} branch must fail the string sink"); assert!(matches!(string_err, Error::Render(_))); assert!(format!("{string_err}").contains("fallback")); let mut sink = Vec::new(); let write_err = set .render_to_write("banner", &else_context, &mut sink) .expect_err("a missing variable in a taken {{else}} branch must fail the write sink"); assert!(matches!(write_err, Error::Render(_))); } #[test] fn write_sink_failure_is_typed_and_carries_the_no_atomicity_contract() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut set, "report", "prefix-ok\n{{missing}}\ntrailing"); let context = json!({}); let mut sink = Vec::new(); let write_err = set .render_to_write("report", &context, &mut sink) .expect_err("a strict failure past already-streamed text must still be a typed error"); assert!(matches!(write_err, Error::Render(_))); assert_eq!( String::from_utf8_lossy(&sink), "prefix-ok\n", "the write sink streams incrementally: bytes rendered before a failure stay \ in the target, so consumers needing atomicity buffer first (the wave-1 contract)" ); } // ------------------------------------------------------------------ // Error-surface completeness (core.md error model, convention 2) // // Every engine-owned failure path maps to exactly one typed variant // with no catch-all, nothing survives a failed registration as a // half-registered name, and no input the API accepts reaches a panic. // ------------------------------------------------------------------ #[test] fn error_surface_malformed_template_text_maps_to_registration_and_never_half_registers() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut set, "keep", "ok"); let template_err = set .register_template("broken", "{{#if name") .expect_err("malformed template text must fail registration"); assert!(matches!(template_err, Error::Registration(_))); assert!( !set.is_template("broken"), "a failed first registration must not survive as a half-registered name" ); let partial_err = set .register_partial("broken-partial", "{{#each items") .expect_err("malformed partial text must fail registration"); assert!(matches!(partial_err, Error::Registration(_))); assert!(!set.is_template("broken-partial")); let overwrite_err = set .register_template("keep", "{{#if name") .expect_err("malformed overwrite text must fail registration"); assert!(matches!(overwrite_err, Error::Registration(_))); assert!( set.is_template("keep"), "a failed overwrite leaves the previous registration and its kind intact" ); } #[test] fn error_surface_unknown_template_name_maps_to_render_through_both_sinks() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut set, "present", "ok"); let context = json!({}); let string_err = set .render("no-such-template", &context) .expect_err("an unknown template name must fail the string sink typed"); assert!(matches!(string_err, Error::Render(_))); let mut sink = Vec::new(); let write_err = set .render_to_write("no-such-template", &context, &mut sink) .expect_err("an unknown template name must fail the write sink typed"); assert!(matches!(write_err, Error::Render(_))); assert!( sink.is_empty(), "nothing leaks into the target when the name never resolves" ); let present_err = set .render("", &context) .expect_err("an empty name is an unknown name too"); assert!(matches!(present_err, Error::Render(_))); } #[test] fn error_surface_partial_as_render_target_maps_to_render_through_both_sinks() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut set, "body", "{{> chunk}}"); fixture_partial(&mut set, "chunk", "chunk: {{value}}"); let context = json!({ "value": 1 }); let string_err = set .render("chunk", &context) .expect_err("a partial's name must be rejected as a render target"); assert!(matches!(string_err, Error::Render(_))); let mut sink = Vec::new(); let write_err = set .render_to_write("chunk", &context, &mut sink) .expect_err("the write sink must reject a partial's name too"); assert!(matches!(write_err, Error::Render(_))); } #[test] fn error_surface_strict_missing_variable_maps_to_render_through_both_sinks() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut set, "tpl", "value: {{value}}"); let context = json!({}); let string_err = set .render("tpl", &context) .expect_err("the strict missing variable must fail the string sink typed"); assert!(matches!(string_err, Error::Render(_))); let mut sink = Vec::new(); let write_err = set .render_to_write("tpl", &context, &mut sink) .expect_err("the strict missing variable must fail the write sink typed"); assert!(matches!(write_err, Error::Render(_))); } #[test] fn error_surface_unknown_helper_maps_to_render_through_both_sinks() { let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut set, "loud", "{{shout word}}"); let context = json!({ "word": "echo" }); let string_err = set .render("loud", &context) .expect_err("an unregistered helper must fail the string sink typed"); assert!(matches!(string_err, Error::Render(_))); assert!( format!("{string_err}").contains("shout"), "the failure names the helper that was never registered" ); let mut sink = Vec::new(); let write_err = set .render_to_write("loud", &context, &mut sink) .expect_err("an unregistered helper must fail the write sink typed"); assert!(matches!(write_err, Error::Render(_))); } #[test] fn error_surface_hostile_context_values_render_clean_or_typed_never_panic() { let hostile_values = [ "", "\"quoted\" & 'single' = equal", "backslash \\ backtick ` brace }", "'=&", "emoji \u{1F6A0} unicode e-acute \u{00E9}", "control \u{0000} byte and tab \t newline \n", ]; let mut html_set = TemplateSet::new(EscapePolicy::Html); fixture_template(&mut html_set, "page", HOSTILE_TEMPLATE); for (i, hostile) in hostile_values.iter().enumerate() { let context = json!({ "value": hostile, "trusted": "ok" }); let rendered = html_set .render("page", &context) .expect("hostile interpolated values are data: they render typed-clean, never panic"); assert!( !rendered.contains("", "trusted": "" }), ) .expect("the pinned hostile value should render"); assert_eq!( exact, "a<script>alert(1)</script>bc", "html-policy escaping of the hostile value is entity-escaping, byte-pinned" ); let mut noop_set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut noop_set, "plain", HOSTILE_TEMPLATE); for (i, hostile) in hostile_values.iter().enumerate() { let context = json!({ "value": hostile, "trusted": hostile }); let rendered = noop_set .render("plain", &context) .expect("the no-op policy passes data through verbatim, never panicking"); assert_eq!( rendered, format!("a{hostile}b{hostile}c"), "the no-op policy output is the input verbatim (case {i})" ); let mut sink = Vec::new(); noop_set .render_to_write("plain", &context, &mut sink) .expect("the write sink handles the same hostile values the same way"); assert_eq!( &sink[..], rendered.as_bytes(), "both sinks agree on hostile values too (case {i})" ); } } // ------------------------------------------------------------------ // Two-sink correctness rows (core.md render entry point) // // For each fixture posture, `render` and `render_to_write` must // produce byte-identical output on the same context. Both postures // re-exercise the wave-2 proof fixture files; their goldens stay // pinned by the proof binaries. // ------------------------------------------------------------------ /// The ssr-shaped fixture posture: the wave-2 ssr proof's page and /// fragment partials under the HTML escaper, with the caller-supplied /// card text registering through the same API (ADR-008 machinery /// equality). fn ssr_shaped_set() -> TemplateSet { const PAGE: &str = include_str!("fixtures/ssr_proof/page.hbs"); const HEADER: &str = include_str!("fixtures/ssr_proof/header.hbs"); const NAV: &str = include_str!("fixtures/ssr_proof/nav.hbs"); const FOOTER: &str = include_str!("fixtures/ssr_proof/footer.hbs"); const CARD_TEXT: &str = "

{{title}}

{{body}}

"; let mut set = TemplateSet::new(EscapePolicy::Html); fixture_template(&mut set, "page", PAGE); fixture_template(&mut set, "card", CARD_TEXT); fixture_partial(&mut set, "header", HEADER); fixture_partial(&mut set, "nav", NAV); fixture_partial(&mut set, "footer", FOOTER); set } fn ssr_shaped_context() -> serde_json::Value { json!({ "site_name": "Alk Forge", "tagline": "Your repositories", "nav_links": [ { "href": "/", "label": "Home" }, { "href": "/search?q=" } ], "cards": [ { "kind": "repo", "title": "Repo \"settings\" & ", "body": "if (a < b) { alert(\"hi\") }" }, { "kind": "link", "title": "Plain card", "body": "A plain body." } ], "show_banner": true, "banner_text": "scheduled maintenance", "trusted_embed": "", "year": 2026 }) } #[test] fn two_sinks_agree_byte_for_byte_on_the_ssr_shaped_fixture_posture() { let set = ssr_shaped_set(); let context = ssr_shaped_context(); assert_both_sinks(&set, "page", &context); let card_context = &context["cards"][0]; assert_both_sinks(&set, "card", card_context); let rendered = set .render("page", &context) .expect("the ssr-shaped posture should render"); assert!( rendered.contains(r#""#) && rendered.contains(r#"<script>alert('x')</script>"#), "the exercised row is the real ssr posture: hostile data escaped, trusted slot raw" ); } /// The code-shaped fixture posture: the wave-2 code proof's writer and /// validator templates over the composed field-write-line partial /// under the no-op escaper, with document-ordered fields and /// builder-decided call names carried straight into the context. fn code_shaped_set() -> TemplateSet { const WRITER: &str = include_str!("fixtures/code_proof/writer.hbs"); const VALIDATOR: &str = include_str!("fixtures/code_proof/validator.hbs"); const FIELD_WRITE_LINE: &str = include_str!("fixtures/code_proof/field_write_line.hbs"); let mut set = TemplateSet::new(EscapePolicy::NoOp); fixture_template(&mut set, "writer", WRITER); fixture_template(&mut set, "validator", VALIDATOR); fixture_partial(&mut set, "field-write-line", FIELD_WRITE_LINE); set } #[test] fn two_sinks_agree_byte_for_byte_on_the_code_shaped_fixture_posture() { let set = code_shaped_set(); let context = json!({ "struct_name": "PacketHeader", "fields": [ { "name": "zeta", "writer_call": "write_str", "check_call": "check_str_len", "is_string": true, "max_len_const": "ZETA_MAX" }, { "name": "magic", "writer_call": "write_u64", "check_call": "check_u64", "is_string": false, "max_len_const": "MAGIC_MAX" }, { "name": "alpha", "writer_call": "write_bool", "check_call": "check_bool", "is_string": false, "max_len_const": "ALPHA_MAX" }, { "name": "length", "writer_call": "write_str", "check_call": "check_str_len", "is_string": true, "max_len_const": "LENGTH_MAX" } ], "default_consts": [ { "const_name": "ZETA_DEFAULT", "literal": "\"v<1>&\\\"raw\\\"\"" } ] }); assert_both_sinks(&set, "writer", &context); assert_both_sinks(&set, "validator", &context); let rendered = set .render("writer", &context) .expect("the code-shaped posture should render"); assert!( rendered.contains(r#"out.write_str(&self.zeta)?;"#) && rendered.contains(r#"pub const ZETA_DEFAULT: &str = "v<1>&\"raw\"";"#), "the exercised row is the real code posture: document order and verbatim literals" ); let validator = set .render("validator", &context) .expect("the validator should render"); assert!( validator.contains("check_str_len(&self.zeta, ZETA_MAX)?;") && validator.contains("check_u64(&self.magic)?;"), "the validator half rides the same document-ordered context" ); }