diff --git a/AGENTS.md b/AGENTS.md index 4f46dd7..0c88863 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,13 +145,41 @@ Run these before committing. All must pass. ```bash cargo test # full suite +cargo test --all-features # feature matrix (see below) cargo clippy --all-targets -- -D warnings cargo fmt --check cargo doc --no-deps # if docs changed -cargo build --target wasm32-unknown-unknown --release # if deps/wasm-relevant code changed +cargo build --target wasm32-unknown-unknown --release # standing, unconditional (see below) cargo publish --dry-run --allow-dirty # before a release grep -rn '—' src/ docs/ README.md AGENTS.md .opencode/agents/ tests/ \ Cargo.toml # must be zero hits; see convention 12 +taskgraph validate # after touching anything under tasks/ +``` + +Two postures are standing and unconditional rather than condition-run: + +- **The wasm build** (ADR-003, made standing by the wave-2 + conformance battery): `cargo test` may pass everywhere but wasm is + where platform-only dependencies betray themselves, and this crate + promises wasm-clean always. Build the release target on every + change that lands, not only when deps seem affected. The + `grep -rn ... | grep handlebars | head` style feature inspection is + not needed by hand; the posture to preserve is: no handlebars + features (`dir_source`, `script_helper`) may end up enabled in the + final dependency set. +- **The feature matrix** (ADR-009), documented honestly per the + wave-2 battery: `cargo test` with `default` features and with + `--all-features` both pass, and they coincide today because + `bast-codegen` gates nothing yet. The coincide-today statement + holds until the first feature starts gating something; when one + does, this list grows the distinct postures that feature needs + rather than the matrix staying implicit. + +```bash +# feature inspection posture (ADR-002): +cargo tree --all-targets -f "{p} {f}" | grep handlebars +# expected: handlebars features shown are only the substrate defaults +# this crate builds with (no dir_source, no script_helper) ``` ## Task files (taskgraph) diff --git a/tasks/implementation/w2-conformance-battery.md b/tasks/implementation/w2-conformance-battery.md index 2ec803e..7e74168 100644 --- a/tasks/implementation/w2-conformance-battery.md +++ b/tasks/implementation/w2-conformance-battery.md @@ -1,7 +1,7 @@ --- id: w2-conformance-battery name: "Wave 2: the conformance battery (strict-mode matrix, error surface, two sinks, wasm, feature matrix)" -status: pending +status: completed depends_on: [w2-ssr-proof, w2-code-proof] scope: moderate risk: low @@ -101,8 +101,102 @@ workaround. ## Notes -> To be filled by implementation agent +Implementation landed as `tests/conformance.rs`, a standalone test +binary (13 rows) in the wave-1/2 convention of one fixture module per +test binary. The battery reuses the proof fixture files +(`tests/fixtures/ssr_proof/`, `tests/fixtures/code_proof/`) rather +than restating them, and does not restate the proof binaries' goldens; +its rows assert the obligations themselves against fresh public-API +renders. + +What landed, per the acceptance criteria: + +- **Strict-mode matrix**: five rows. Top-level missing variable, + missing variable inside an `{{#each}}` body, missing variable inside + a taken `{{#if}}` branch, missing variable inside a taken + `{{else}}` branch: each fails typed (`Error::Render`) through both + sinks, with the detail strings naming the missing variables, and + the present-variable case still rendering. Plus the write-sink + on-failure row: a template that streams a prefix before failing + leaves those bytes in the target (`"prefix-ok\n"` pinned) and + returns the typed error; the row states the no-atomicity contract + (consumers needing atomicity buffer first), which is what wave 1 + pinned and nothing more. +- **Error-surface completeness**: six rows, each mapping to exactly + one variant of the two (the enum has no catch-all by construction, + so variant assignment is asserted): malformed `register_template` + and `register_partial` text resolves to `Error::Registration` and + the name never survives half-registered, including the failed- + overwrite case where the previous registration and its kind stay + intact (re-exercised from wave 1's unit rows); unknown template + name (including the empty name) resolves to `Error::Render` through + both sinks; partial-as-render-target resolves to `Error::Render` + through both sinks; strict missing variable resolves to + `Error::Render` through both sinks; unknown helper resolves to + `Error::Render` naming the helper, through both sinks; hostile + context values (script tags, quotes, braces, entity-lookalikes, + emoji, NUL/tab/newline control bytes) render typed-clean through + both sinks under both escape policies with two byte-pinned + expectations. No input the battery sends reaches a panic path. +- **Two-sink correctness**: two rows, one per fixture posture. The + ssr-shaped posture (ssr-proof page + fragments + caller-supplied + card text, HTML escaper) and the code-shaped posture (code-proof + writer + validator + the composed field-write-line partial, no-op + escaper, document-ordered fields) each produce byte-identical + output through `render` and `render_to_write`, with posture- + identity assertions (trusted raw slot / verbatim literals and + document order) proving the exercised rows are the real postures. +- **AGENTS.md edits (both recorded here as required)**: (1) the + wasm release build is now a standing, unconditional line in the + verification command list (comment changed from "if deps/wasm- + relevant code changed" to "standing, unconditional", ADR-003), with + a paragraph explaining the posture and the feature-inspection + posture (no `dir_source` / `script_helper` in the final dep set) + made explicit; (2) `cargo test --all-features` joined the command + list and the feature-matrix posture is documented honestly: default + and `--all-features` coincide today because `bast-codegen` gates + nothing yet, and the list grows the distinct postures when the + first feature starts gating something (ADR-009). + +Verification run on the final tree: + +- `cargo test`: 19 lib + 13 conformance + 5 code_fixtures + 7 + code_proof + 3 ssr + 4 ssr_proof, all pass. +- `cargo test --all-features`: identical (the coincide holds). +- `cargo clippy --all-targets -- -D warnings`: clean (after + switching the battery to `expect_err` and a shared + `assert_both_sinks` helper so clippy's `err_expect` and + `single_element_loop` lints stay silent). +- `cargo fmt --check`, `cargo doc --no-deps`: clean. +- `cargo build --target wasm32-unknown-unknown --release`: passes. +- Tree inspection: `cargo tree --all-targets -f "{p} {f}" | grep + handlebars` shows only `handlebars v6.4.4 default` (no + `dir_source`, no `script_helper`). +- `taskgraph validate`: 18 tasks valid. +- The standing em-dash grep: zero hits outside the grep command + itself pre-existing in AGENTS.md (convention 12). + +Stop-and-book: no core change was needed; every row passed against +the shipped wave-2 tree. One substrate observation surfaced while +authoring the hostile-values row and is dispositioned as data, not a +gap: handlebars treats the expression name `raw` as keyword-shaped +and never resolves it as a context variable (`{{& raw}}` renders +empty even in strict mode). The battery avoids the name and documents +why in a doc comment; no consumer behavior change is implied and no +core change is booked for v0.1.0. ## Summary -> To be filled on completion +The wave-2 conformance battery landed: `tests/conformance.rs` stands +as the standing verification home with 13 rows across the strict-mode +matrix (top-level, `{{#each}}`, `{{#if}}` both branches, write-sink +no-atomicity contract), error-surface completeness (six failure paths +each mapping to exactly one typed variant, no half registrations, no +panic paths, hostile data through both sinks and both escape +policies), and two-sink byte-equality over the ssr-shaped and code- +shaped fixture postures, re-exercised over the proof fixture files. +The wasm release build became a standing unconditional line in +AGENTS.md's verification list together with the feature-matrix +coincide-today posture (ADR-009) and the explicit handlebars feature- +inspection posture. Full verification set green, including wasm +release and `--all-features`. `review-wave-2` is unblocked. diff --git a/tests/conformance.rs b/tests/conformance.rs new file mode 100644 index 0000000..d26ea5f --- /dev/null +++ b/tests/conformance.rs @@ -0,0 +1,513 @@ +//! 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" + ); +}