test(ssr-proof): wave-2 ssr-html proof through the public API (posture B)
- full consumer-shaped fixture set registered from the test binary via
register_template/register_partial only (ADR-008 machinery equality
in three caller-supplied text shapes: fixtures, runtime String,
inline literal)
- page composes fragment partials; the card fragment registers as its
own template, renders directly by name, and composes via include
- exact goldens pin html-escape posture (hostile data escaped, = and '
escapes, raw-output {{& }} slot verbatim)
- strict-mode render failure maps consumer-side to an error page built
from the typed Error's data, panic-free; source chain reachable
- determinism pinned across repeats, registry rebuilds, fresh
contexts, and both sinks (byte-identical, ADR-003)
- posture-B hygiene rows: no banner, no filesystem; no core changes
required (stop-and-book: nothing to book)
This commit is contained in:
1 parent
a4ec8be3e8
commit
08803b3d17
7 files changed
+394
-11
No files matched your search
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: w2-ssr-proof
|
||||
name: "Wave 2: the ssr-html proof (posture B, consumer-shaped fixture through the public API)"
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
@@ -58,21 +58,21 @@ core invisibly (ADR-001 falsifiability discipline).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] the full fixture set registers from outside the crate through
|
||||
- [x] the full fixture set registers from outside the crate through
|
||||
the public API only (caller-supplied text; ADR-008 equality
|
||||
exercised at posture-B scale)
|
||||
- [ ] a page template composes fragment partials; at least one
|
||||
- [x] a page template composes fragment partials; at least one
|
||||
fragment is a registered template rendered directly by name
|
||||
- [ ] HTML-escape goldens pin: interpolated hostile data escaped, a
|
||||
- [x] HTML-escape goldens pin: interpolated hostile data escaped, a
|
||||
deliberate raw-output slot verbatim
|
||||
- [ ] a strict-mode render failure maps toward the error-page shape
|
||||
- [x] a strict-mode render failure maps toward the error-page shape
|
||||
through the typed error, panic-free, using only public API
|
||||
data
|
||||
- [ ] repeated renders produce byte-identical output
|
||||
- [ ] posture-B hygiene rows confirmed: no banner, no filesystem
|
||||
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
- [x] repeated renders produce byte-identical output
|
||||
- [x] posture-B hygiene rows confirmed: no banner, no filesystem
|
||||
- [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`cargo fmt --check` pass
|
||||
- [ ] no core changes required; any gap found is booked for the gate
|
||||
- [x] no core changes required; any gap found is booked for the gate
|
||||
in Notes per the stop-and-book rule
|
||||
|
||||
## References
|
||||
@@ -91,8 +91,59 @@ core invisibly (ADR-001 falsifiability discipline).
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
> Filled by implementation (2026-11-06). The shipped core served the
|
||||
> full set with **no core changes required**; the stop-and-book rule
|
||||
> found nothing to book. Details:
|
||||
|
||||
- ADR-008 machinery equality exercised in three caller-supplied text
|
||||
shapes at once: `include_str!` fixture files (page, header, nav,
|
||||
footer, settings_page), a runtime-built `String` (the card fragment
|
||||
template, assembled from `String::push_str` parts), and an inline
|
||||
literal (the error page). All register through the identical
|
||||
`register_template` / `register_partial` machinery; no second-class
|
||||
path exists in the API to even attempt.
|
||||
- Composition and the render-name rule coexist cleanly: `card`
|
||||
registers as its own template, is included in the page via
|
||||
`{{> card }}` inside `{{#each}}` (posture B: fragments a consumer
|
||||
serves directly are render targets), and renders directly by name;
|
||||
the header partial's name is rejected by `render` as typed
|
||||
`Error::Render`.
|
||||
- Error-page mapping: a strict-mode failure on `settings_page`
|
||||
(missing `account.email`) returns `Error::Render`; the proof maps it
|
||||
to a consumer-built fallback page from `err.to_string()` and
|
||||
`err.source()` (the substrate detail, reachable via
|
||||
`std::error::Error::source`). Substrate detail observed: template
|
||||
name, line/column, and the missing path all survive into the mapped
|
||||
page, entity-escaped. Panic-free by construction.
|
||||
- Determinism pin: repeated renders byte-identical, byte-identical
|
||||
across registry rebuilds and fresh contexts, and the write sink
|
||||
(`render_to_write`) agrees byte-for-byte with the string sink
|
||||
(ADR-003; the substrate alkforge caching leans on).
|
||||
- Hygiene rows: the pinned page output is unadorned served HTML
|
||||
(starts at the doctype, no DO-NOT-EDIT banner, no "Auto-generated"
|
||||
marker); registration is string-only by construction, no filesystem
|
||||
path exists. No emit-layer hygiene beyond what `render` returns.
|
||||
- Template whitespace note (observation, not a gap): the substrate
|
||||
consumes the newline around lines whose only content is a block
|
||||
expression (`{{#each cards}}{{> card}}{{/each}}`) while keeping
|
||||
standalone partial lines' newlines. The goldens pin this behavior,
|
||||
so it is now part of the pinned surface; consumers relying on exact
|
||||
byte layouts have the golden as precedent.
|
||||
- Goldens follow convention 7 and are exact; the handlebars
|
||||
html-escape posture (including `=` as `=` and `'` as `'`
|
||||
inside interpolated attribute values) is exercised and pinned.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Implemented `tests/ssr_proof.rs` plus fixtures under
|
||||
`tests/fixtures/ssr_proof/` (page, header, nav, footer, settings_page
|
||||
templates) following the wave-1 per-binary fixture convention. Four
|
||||
tests cover: the full consumer fixture set rendered as an exact
|
||||
golden with escape and raw-slot rows (page), the card fragment
|
||||
registered as a template and rendered directly, the strict-failure to
|
||||
error-page mapping built from the typed error's public data, and
|
||||
determinism across repeats, rebuilds, contexts, and both sinks. All
|
||||
registration flows through the public API from the test binary;
|
||||
no core change was needed (stop-and-book: nothing to book).
|
||||
Verification: `cargo test` (31 passed), `cargo clippy --all-targets
|
||||
-- -D warnings`, and `cargo fmt --check` all pass.
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
<footer><p>© {{year}} {{site_name}}</p></footer>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
<header><h1>{{site_name}}</h1>{{#if tagline}}<p class="tagline">{{tagline}}</p>{{/if}}</header>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
<nav><ul>{{#each nav_links}}<li><a href="{{href}}">{{label}}</a></li>{{/each}}</ul></nav>
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{> header}}
|
||||
<body>
|
||||
{{> nav}}
|
||||
<main>
|
||||
{{#each cards}}{{> card}}{{/each}}
|
||||
</main>
|
||||
{{#if show_banner}}<div class="banner">{{banner_text}}</div>{{/if}}
|
||||
{{& trusted_embed}}
|
||||
{{> footer}}
|
||||
</body>
|
||||
</html>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{> header}}
|
||||
<body>
|
||||
<main><h2>Account settings</h2><p>Signed in as {{account.email}}</p></main>
|
||||
{{> footer}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,308 @@
|
||||
//! The wave-2 ssr-html proof (posture B, consumer-shaped): a full
|
||||
//! fixture set modeled on the alkforge posture, rendered entirely through
|
||||
//! the public API and pinned by goldens. Where the wave-1 double proved
|
||||
//! the machinery minimally, this proof walks the ssr-html target's real
|
||||
//! shape at fuller scale: if the shipped core serves this set without a
|
||||
//! core change, ADR-001's generality claim holds on shipped code.
|
||||
|
||||
use std::error::Error as StdError;
|
||||
|
||||
use alkgen::{Error, EscapePolicy, TemplateSet};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
mod fixtures {
|
||||
pub const PAGE: &str = include_str!("fixtures/ssr_proof/page.hbs");
|
||||
pub const HEADER: &str = include_str!("fixtures/ssr_proof/header.hbs");
|
||||
pub const NAV: &str = include_str!("fixtures/ssr_proof/nav.hbs");
|
||||
pub const FOOTER: &str = include_str!("fixtures/ssr_proof/footer.hbs");
|
||||
pub const SETTINGS_PAGE: &str = include_str!("fixtures/ssr_proof/settings_page.hbs");
|
||||
}
|
||||
|
||||
/// The caller-supplied error page the failed render maps toward: built
|
||||
/// on the consumer side from the typed error's data, never from a core
|
||||
/// path.
|
||||
const ERROR_PAGE: &str = "<!doctype html>\n<html lang=\"en\">\n<body><h1>Something went wrong</h1><p class=\"detail\">{{message}}</p></body>\n</html>\n";
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NavLink {
|
||||
href: String,
|
||||
label: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Card {
|
||||
kind: String,
|
||||
title: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PageContext {
|
||||
site_name: String,
|
||||
tagline: String,
|
||||
nav_links: Vec<NavLink>,
|
||||
cards: Vec<Card>,
|
||||
show_banner: bool,
|
||||
banner_text: String,
|
||||
trusted_embed: String,
|
||||
year: u16,
|
||||
}
|
||||
|
||||
fn nav_link(href: &str, label: &str) -> NavLink {
|
||||
NavLink {
|
||||
href: href.to_owned(),
|
||||
label: label.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn card(kind: &str, title: &str, body: &str) -> Card {
|
||||
Card {
|
||||
kind: kind.to_owned(),
|
||||
title: title.to_owned(),
|
||||
body: body.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn context_with(show_banner: bool, banner_text: &str) -> PageContext {
|
||||
PageContext {
|
||||
site_name: "Alk Forge".into(),
|
||||
tagline: "Your repositories".into(),
|
||||
nav_links: vec![
|
||||
nav_link("/", "Home"),
|
||||
nav_link("/search?q=<script>&lang=en", "<script>alert('x')</script>"),
|
||||
],
|
||||
cards: vec![
|
||||
card(
|
||||
"repo",
|
||||
"Repo \"settings\" & <config>",
|
||||
"if (a < b) { alert(\"<b>hi</b>\") }",
|
||||
),
|
||||
card("link", "Plain card", "A plain body."),
|
||||
],
|
||||
show_banner,
|
||||
banner_text: banner_text.to_owned(),
|
||||
trusted_embed: r#"<aside id="notice"><b>system notice</b></aside>"#.into(),
|
||||
year: 2026,
|
||||
}
|
||||
}
|
||||
|
||||
/// The card fragment's template text arrives as a consumer-built
|
||||
/// `String`: the caller owns the text however it wants (inline literal,
|
||||
/// runtime-built, or fixture file) and registers it through the same
|
||||
/// API engine-owned sets use (ADR-008 machinery equality).
|
||||
fn caller_supplied_card_text() -> String {
|
||||
let mut text = String::from("<article class=\"card\" data-kind=\"{{kind}}\">");
|
||||
text.push_str("<h3>{{title}}</h3>");
|
||||
text.push_str("<p>{{body}}</p>");
|
||||
text.push_str("</article>");
|
||||
text
|
||||
}
|
||||
|
||||
/// The full consumer-shaped fixture set: pages, fragments, and the
|
||||
/// error page all register from the test binary through the public API.
|
||||
/// Partials exist to be composed; fragments a consumer serves directly
|
||||
/// register as their own templates.
|
||||
fn consumer_fixture_set() -> TemplateSet {
|
||||
let mut set = TemplateSet::new(EscapePolicy::Html);
|
||||
set.register_template("page", fixtures::PAGE)
|
||||
.expect("fixture page template should be well-formed");
|
||||
set.register_template("card", &caller_supplied_card_text())
|
||||
.expect("caller-supplied card template should be well-formed");
|
||||
set.register_template("settings_page", fixtures::SETTINGS_PAGE)
|
||||
.expect("fixture settings page template should be well-formed");
|
||||
set.register_template("error_page", ERROR_PAGE)
|
||||
.expect("caller-supplied error page template should be well-formed");
|
||||
set.register_partial("header", fixtures::HEADER)
|
||||
.expect("fixture header partial should be well-formed");
|
||||
set.register_partial("nav", fixtures::NAV)
|
||||
.expect("fixture nav partial should be well-formed");
|
||||
set.register_partial("footer", fixtures::FOOTER)
|
||||
.expect("fixture footer partial should be well-formed");
|
||||
set
|
||||
}
|
||||
|
||||
const GOLDEN_PAGE: &str = concat!(
|
||||
"<!doctype html>\n",
|
||||
"<html lang=\"en\">\n",
|
||||
"<header><h1>Alk Forge</h1><p class=\"tagline\">Your repositories</p></header>\n",
|
||||
"<body>\n",
|
||||
"<nav><ul><li><a href=\"/\">Home</a></li><li><a href=\"/search?q=<script>&lang=en\"><script>alert('x')</script></a></li></ul></nav>\n",
|
||||
"<main>\n",
|
||||
"<article class=\"card\" data-kind=\"repo\"><h3>Repo "settings" & <config></h3><p>if (a < b) { alert("<b>hi</b>") }</p></article><article class=\"card\" data-kind=\"link\"><h3>Plain card</h3><p>A plain body.</p></article>\n</main>\n",
|
||||
"<div class=\"banner\"><em>scheduled</em> maintenance</div>\n",
|
||||
"<aside id=\"notice\"><b>system notice</b></aside>\n",
|
||||
"<footer><p>© 2026 Alk Forge</p></footer>\n",
|
||||
"</body>\n",
|
||||
"</html>\n",
|
||||
);
|
||||
|
||||
const GOLDEN_CARD: &str = concat!(
|
||||
"<article class=\"card\" data-kind=\"repo\">",
|
||||
"<h3>Repo "settings" & <config></h3>",
|
||||
"<p>if (a < b) { alert("<b>hi</b>") }</p>",
|
||||
"</article>",
|
||||
);
|
||||
|
||||
const GOLDEN_ERROR_PAGE: &str = concat!(
|
||||
"<!doctype html>\n",
|
||||
"<html lang=\"en\">\n",
|
||||
"<body><h1>Something went wrong</h1><p class=\"detail\">render failed: Error rendering "settings_page" line 5, col 48: Failed to access variable in strict mode Some("account.email")</p></body>\n",
|
||||
"</html>\n",
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn page_golden_pins_composition_escaping_and_raw_slot_at_fuller_scale() {
|
||||
let set = consumer_fixture_set();
|
||||
let context = context_with(true, "<em>scheduled</em> maintenance");
|
||||
|
||||
let rendered = set
|
||||
.render("page", &context)
|
||||
.expect("the full consumer fixture set should render through the public API");
|
||||
assert_eq!(rendered, GOLDEN_PAGE);
|
||||
|
||||
assert!(
|
||||
rendered.contains(r#"<a href="/search?q=<script>&lang=en"><script>alert('x')</script></a>"#),
|
||||
"hostile interpolated data must be entity-escaped"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains(r#"<h3>Repo "settings" & <config></h3>"#),
|
||||
"hostile card data must be entity-escaped everywhere it interpolates"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains(r#"<aside id="notice"><b>system notice</b></aside>"#),
|
||||
"the deliberate raw-output slot ({{& }}) must pass through verbatim"
|
||||
);
|
||||
assert!(
|
||||
rendered.starts_with("<!doctype html>"),
|
||||
"the output unit is the served page, unadorned"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("DO NOT EDIT"),
|
||||
"posture-B hygiene: rendered HTML is the product, not a checked-in generated artifact"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("Auto-generated"),
|
||||
"posture-B hygiene: no emit-layer banner on rendered HTML"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_fragment_registers_as_template_and_renders_directly_by_name() {
|
||||
let set = consumer_fixture_set();
|
||||
assert!(set.is_template("card"));
|
||||
|
||||
let rendered = set
|
||||
.render(
|
||||
"card",
|
||||
&card(
|
||||
"repo",
|
||||
"Repo \"settings\" & <config>",
|
||||
"if (a < b) { alert(\"<b>hi</b>\") }",
|
||||
),
|
||||
)
|
||||
.expect("a fragment registered as its own template must render directly by name");
|
||||
assert_eq!(rendered, GOLDEN_CARD);
|
||||
|
||||
let err = set
|
||||
.render("header", &context_with(false, ""))
|
||||
.expect_err("partial names are never render targets (the render-name rule)");
|
||||
assert!(matches!(err, Error::Render(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_failure_maps_to_the_error_page_shape_panic_free() {
|
||||
let set = consumer_fixture_set();
|
||||
let context = json!({
|
||||
"site_name": "Alk Forge",
|
||||
"tagline": "",
|
||||
"year": 2026
|
||||
});
|
||||
|
||||
let err = set
|
||||
.render("settings_page", &context)
|
||||
.expect_err("a strict-mode render failure must return the typed error, never a panic");
|
||||
assert!(matches!(err, Error::Render(_)));
|
||||
|
||||
let source = err
|
||||
.source()
|
||||
.expect("everything a consumer needs for the mapping is reachable through the public enum");
|
||||
assert!(
|
||||
format!("{source}").contains("account.email"),
|
||||
"the substrate's missing-variable detail stays reachable consumer-side"
|
||||
);
|
||||
|
||||
let error_page = set
|
||||
.render("error_page", &json!({ "message": err.to_string() }))
|
||||
.expect("the consumer-built error page must render over the error's data");
|
||||
assert_eq!(error_page, GOLDEN_ERROR_PAGE);
|
||||
assert!(
|
||||
error_page.contains(""settings_page"")
|
||||
&& error_page.contains(""account.email""),
|
||||
"the failing template's identity and the missing path surface entity-escaped"
|
||||
);
|
||||
let detail_begin = error_page
|
||||
.find(r#"<p class="detail">"#)
|
||||
.map_or(0, |at| at + r#"<p class="detail">"#.len());
|
||||
let detail_end = error_page[detail_begin..]
|
||||
.find("</p>")
|
||||
.expect("the detail slot closes");
|
||||
let detail = &error_page[detail_begin..detail_begin + detail_end];
|
||||
assert!(
|
||||
detail.contains(""") && !detail.contains('"') && !detail.contains('<'),
|
||||
"interpolated error detail is entity-escaped; the static markup alone carries quotes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_renders_across_rebuilds_and_sinks_are_byte_identical() {
|
||||
let set_a = consumer_fixture_set();
|
||||
let context = context_with(true, "<em>scheduled</em> maintenance");
|
||||
|
||||
let first = set_a
|
||||
.render("page", &context)
|
||||
.expect("the page should render");
|
||||
for _ in 0..4 {
|
||||
let again = set_a
|
||||
.render("page", &context)
|
||||
.expect("repeated renders must succeed");
|
||||
assert_eq!(
|
||||
again, first,
|
||||
"repeated renders must be byte-identical (ADR-003, the caching substrate)"
|
||||
);
|
||||
}
|
||||
|
||||
let rebuilt_context = context_with(true, "<em>scheduled</em> maintenance");
|
||||
let rebuilt = consumer_fixture_set()
|
||||
.render("page", &rebuilt_context)
|
||||
.expect("a rebuilt set should render");
|
||||
assert_eq!(
|
||||
rebuilt, first,
|
||||
"same registry contents rebuilt must render byte-identical output"
|
||||
);
|
||||
|
||||
let card_again = consumer_fixture_set()
|
||||
.render(
|
||||
"card",
|
||||
&card(
|
||||
"repo",
|
||||
"Repo \"settings\" & <config>",
|
||||
"if (a < b) { alert(\"<b>hi</b>\") }",
|
||||
),
|
||||
)
|
||||
.expect("the fragment should render from the rebuilt set");
|
||||
assert_eq!(
|
||||
card_again, GOLDEN_CARD,
|
||||
"fragment renders are byte-identical across registry rebuilds"
|
||||
);
|
||||
|
||||
let mut sink = Vec::new();
|
||||
consumer_fixture_set()
|
||||
.render_to_write("page", &context, &mut sink)
|
||||
.expect("the write sink should render the page");
|
||||
assert_eq!(
|
||||
&sink[..],
|
||||
first.as_bytes(),
|
||||
"both sinks must agree byte for byte"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user