// Contract guard for the reusable delivery-graph LIBRARY App-View (issue #523, epic #519 S4). // // The Library surface (pages/delivery-graphs/library.*) LISTS saved library entries (the `listLibrary` // door) and, per row, offers Reuse (load the saved graph back into the compose textarea `#dg-json`) and // Delete (the `deleteLibraryEntry` door). Reuse crosses the App-View iframe boundary: it drives the // compose mount's NEW inbound fill seam over the shared `deliveryGraph.compose.fill` host-bridge // message. Plus a Save-to-library affordance on the staged App-View (save-from-digest) and a // Save-to-library row action on the dispatched/history grid. This test pins that wiring so it cannot // silently regress: the sidecars mount the same module, the door defaults are module-anchored off // import.meta.url (the #279/#467/#536 App-View resolution class), Reuse posts the ONE shared fill message, Delete hits the per-entry door, // the compose mount adds the inbound fill listener, and the page carries the new Library App-View node. import { test } from "node:test"; import { assert, assertEquals, assertStringIncludes } from "#test-assert"; import { readFileSync } from "node:fs"; import { DG_COMPOSE_FILL_MESSAGE } from "../pages/delivery-graphs/mount.js"; const ROOT = decodeURIComponent(new URL("../", import.meta.url).pathname); const DIR = `${ROOT}pages/delivery-graphs`; const LIBRARY_JS = readFileSync(`${DIR}/library.mount.js`, "utf8"); const COMPOSE_JS = readFileSync(`${DIR}/mount.js`, "utf8"); const STAGED_JS = readFileSync(`${DIR}/staged.mount.js`, "utf8"); const EMBED_HTML = readFileSync(`${DIR}/library-embed.html`, "utf8"); const STANDALONE_HTML = readFileSync(`${DIR}/library-standalone.html`, "utf8"); const PAGE_JSON = readFileSync(`${ROOT}pages/delivery-graphs.page.json`, "utf8"); // Pull a MODULE-ANCHORED default spec out of `const = config. ?? ;` where the // CONST is declared `const = new URL("", import.meta.url).href;` (#467/#536). function defaultSpec(src: string, name: string): string { const m = src.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`)); assert(m, `mount must default ${name} from config with a fallback constant`); const constM = src.match( new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`), ); assert( constM, `mount must default ${m![1]} to new URL("", import.meta.url) so the door is anchored to the ` + `module's own served location, not the document base (#467/#536)`, ); return constM![1]; } test("#523: the Library App-View mounts the same module standalone and embedded", () => { assert(/mountDeliveryGraphLibrary/.test(LIBRARY_JS), "library.mount.js must export mountDeliveryGraphLibrary"); for (const [file, html] of [["library-embed.html", EMBED_HTML], ["library-standalone.html", STANDALONE_HTML]] as const) { assert( /import \{ mountDeliveryGraphLibrary \} from "\.\/library\.mount\.js"/.test(html), `${file} must import mountDeliveryGraphLibrary from ./library.mount.js`, ); assert(/mountDeliveryGraphLibrary\(/.test(html), `${file} must call mountDeliveryGraphLibrary`); } }); test("#523: the page binds the Library node to the library App-View sidecars", () => { const page = JSON.parse(PAGE_JSON) as { nodes: Array> }; const library = page.nodes.find((n) => n.id === "delivery-graphs-library"); assert(library, "the page must carry the delivery-graphs-library node"); assert(library?.type === "appView", "delivery-graphs-library must be an appView (#523)"); assert(library?.props?.embed === "./delivery-graphs/library-embed.html", "it embeds the library embed sidecar"); assert(library?.props?.standalone === "./delivery-graphs/library-standalone.html", "it has the library standalone sidecar"); }); test("#523/#467/#536: the Library list door default is module-anchored and hits the listLibrary door", () => { const spec = defaultSpec(LIBRARY_JS, "libraryUrl"); assert(spec.endsWith("delivery-graph/library"), `libraryUrl default "${spec}" must hit the listLibrary door`); assert(!spec.startsWith("/"), `default libraryUrl "${spec}" must not be absolute (App-View #279 resolution class)`); assert(spec.startsWith("../"), `default libraryUrl "${spec}" must step up out of /delivery-graphs/ (module-anchored, #467/#536)`); // The list read consumes the door's `entries` array (one row per saved entry). assert(/body\.entries/.test(LIBRARY_JS), "library.mount.js must render one row per `entries[]` item the listLibrary door returns"); }); test("#523: Reuse drives the compose fill seam over the shared host-bridge message", () => { // Reuse loads the saved graph back into the SEPARATE compose App-View, so it posts the ONE shared // fill message (its type imported from ./mount.js, never re-declared) UP over the App-View boundary. assert( /import \{[^}]*\bDG_COMPOSE_FILL_MESSAGE\b[^}]*\} from "\.\/mount\.js"/.test(LIBRARY_JS), "library.mount.js must import DG_COMPOSE_FILL_MESSAGE from ./mount.js (the ONE source of truth for the fill message type)", ); assert(/data-reuse=/.test(LIBRARY_JS), "library.mount.js must render a per-row Reuse affordance carrying the entry id"); assert(/postMessage\(/.test(LIBRARY_JS), "Reuse must post the fill message across the App-View boundary"); assert(/type:\s*DG_COMPOSE_FILL_MESSAGE/.test(LIBRARY_JS), "the Reuse message must carry the shared DG_COMPOSE_FILL_MESSAGE type"); assert(/graphJson:\s*entry\.graph/.test(LIBRARY_JS), "the Reuse message must carry the saved entry's graph JSON"); }); test("#523: Delete hits the per-entry deleteLibraryEntry door", () => { assert(/data-delete=/.test(LIBRARY_JS), "library.mount.js must render a per-row Delete affordance carrying the entry id"); // The delete door is the per-entry path under the list door: DELETE .../delivery-graph/library/. assert(/method:\s*"DELETE"/.test(LIBRARY_JS), "Delete must issue an HTTP DELETE to the deleteLibraryEntry door"); assert(/encodeURIComponent\(/.test(LIBRARY_JS), "the delete path must URL-encode the entry id it appends to the library door"); }); test("#523: the compose mount exposes an INBOUND reuse-fill seam (message → #dg-json)", () => { // The compose mount previously had NO inbound prefill — its #dg-json was set only by Load-example / // typing. S4 adds a same-origin message listener that fills #dg-json through a single fillComposer seam. assert(/export const DG_COMPOSE_FILL_MESSAGE\s*=/.test(COMPOSE_JS), "mount.js must export the DG_COMPOSE_FILL_MESSAGE fill-message type"); // Pin the actual wire value the imported const carries: this is the cross-App-View host-bridge // message type the Library/compose sidecars agree on, so a silent change to the string is a // breaking wire-contract change and the compose mount must declare exactly that literal. assertEquals(DG_COMPOSE_FILL_MESSAGE, "nano-delivery-graph-compose-fill", "the shared fill-message type must be the pinned wire-contract string"); assertStringIncludes(COMPOSE_JS, `"${DG_COMPOSE_FILL_MESSAGE}"`, "mount.js must declare the fill-message type as the pinned string literal"); assert(/function fillComposer\(/.test(COMPOSE_JS), "mount.js must define the single fillComposer seam every fill routes through"); assert(/addEventListener\("message"/.test(COMPOSE_JS), "mount.js must register an inbound `message` listener for the fill seam"); assert(/data\.type !== DG_COMPOSE_FILL_MESSAGE/.test(COMPOSE_JS), "the listener must gate on the shared fill-message type"); assert(/jsonEl\.value = graphJson/.test(COMPOSE_JS), "fillComposer must load the graph JSON into the #dg-json textarea"); // Same-origin guard: a foreign origin must not be able to drive the fill. assert(/ev\.origin !== window\.location\.origin/.test(COMPOSE_JS), "the fill listener must reject cross-origin messages"); }); test("#523: Save-to-library on the staged App-View posts save-from-digest", () => { const spec = defaultSpec(STAGED_JS, "saveLibraryUrl"); assert(spec.endsWith("actions/delivery-graph/library/save"), `saveLibraryUrl default "${spec}" must hit the saveToLibrary door`); assert(!spec.startsWith("/"), `default saveLibraryUrl "${spec}" must not be absolute (App-View #279 resolution class)`); assert(spec.startsWith("../"), `default saveLibraryUrl "${spec}" must step up out of /delivery-graphs/ (module-anchored, #467/#536)`); assert(/data-save-library=/.test(STAGED_JS), "staged.mount.js must render a per-row Save-to-library affordance carrying the digest"); // Save-from-digest: it posts { name, digest } — it must NOT compile or stage a raw graph (the #460 // operator boundary the staged view enforces stays intact). assert(/post\(saveLibraryUrl,\s*\{\s*name:[^}]*digest:/.test(STAGED_JS), "Save-to-library must POST { name, digest } (save-from-digest) to the save door"); }); test("#538: the in-flight grid's column widths never over-allocate (no 0-width column, no header char-wrap)", () => { // Regression guard: the grid is `table-layout:fixed` with `overflow-wrap:anywhere` headers, so a // column squeezed to 0 width wraps its title one character per line (a ~250px-tall header row). That // happens when the declared widths over-allocate. Pin the contract: every data column carries a // percentage width, and their sum leaves headroom (>=5%) for the implicit row-action column — so no // column can collapse to 0. const page = JSON.parse(PAGE_JSON) as { nodes: Array> }; const grid = page.nodes.find((n) => n.id === "delivery-graphs-inflight"); assert(grid, "the page must carry the delivery-graphs-inflight grid"); const columns = (grid?.props?.columns ?? []) as Array>; assert(columns.length > 0, "the grid must declare columns"); let sum = 0; for (const col of columns) { const w = col.width; assert(typeof w === "string" && /^\d+(\.\d+)?%$/.test(w), `column "${col.header}" must declare a percentage width so it can't collapse to 0 (got ${JSON.stringify(w)})`); const parsed = Number.parseFloat(w); assert(parsed > 0, `column "${col.header}" must declare a positive width so it can't collapse to 0 (got ${JSON.stringify(w)})`); sum += parsed; } assert(sum <= 95, `declared column widths sum to ${sum}% — they must leave >=5% for the row-action column so nothing squeezes to 0 (#538)`); }); test("#523: Save-to-library is offered on a dispatched/history grid row (save-from-dispatched)", () => { const page = JSON.parse(PAGE_JSON) as { nodes: Array> }; const grid = page.nodes.find((n) => n.id === "delivery-graphs-inflight"); assert(grid, "the page must carry the delivery-graphs-inflight grid"); const action = (grid?.props?.rowActions ?? []).find((a: Record) => a.label === "Save to library"); assert(action, "the in-flight grid must offer a `Save to library` row action"); assert( action?.action?.path === "/app/api/actions/delivery-graph/library/save", "the Save-to-library row action must post to the saveToLibrary door", ); assert( action?.action?.body?.digest === "{{row.digest}}" && action?.action?.body?.name === "{{row.title}}", "the Save-to-library row action must save-from-digest, naming the entry from the run title", ); }); test("#523: the x-hook-secret guard is gated on a same-origin door (no cross-origin secret exfil)", () => { // A `?library=` / `?staged=` / `?preview=` override can point a door at a full `https://…` URL on a // FOREIGN origin. The shared guard secret must NEVER ride along to an arbitrary host, so each mount // attaches `x-hook-secret` only when the resolved door URL is same-origin. for (const [name, src] of [ ["library.mount.js", LIBRARY_JS], ["mount.js", COMPOSE_JS], ["staged.mount.js", STAGED_JS], ] as const) { assert(/function isSameOrigin\(url\)/.test(src), `${name} must define an isSameOrigin(url) guard`); assert( /new URL\(url,\s*window\.location\.href\)\.origin === window\.location\.origin/.test(src), `${name} isSameOrigin must compare the resolved URL origin against window.location.origin`, ); assert( /config\.hookSecret && isSameOrigin\(url\)\s*\?\s*\{\s*"x-hook-secret"/.test(src), `${name} must attach x-hook-secret ONLY when hookSecret is set AND the door URL is same-origin`, ); assert( /headers:\s*headers\(url\)/.test(src), `${name} must thread the request URL into headers(url) so the secret gate sees the target origin`, ); } });