// Contract guard for the filesystem IMPORT wiring on the Delivery Graphs compose App-View (issue #524, // epic #519 S5). The Import control is ADDED into the PRE-EXISTING compose mount (pages/delivery-graphs/ // mount.js — the one #523 reshaped), alongside #523's inbound reuse-fill seam. It must: render an // ``, read the picked file's text client-side, POST it to the // importToLibrary door (module-anchored relative spec, App-View #279 + #467/#536 resolution class), route a successful import back // through #523's SINGLE `fillComposer()` seam (no second inbound fill path), and render path-qualified // compile errors inline on a 400. This test pins that wiring so it can't silently regress. import { test } from "node:test"; import { assert } from "#test-assert"; import { readFileSync } from "node:fs"; const ROOT = decodeURIComponent(new URL("../", import.meta.url).pathname); const DIR = `${ROOT}pages/delivery-graphs`; const MOUNT_JS = readFileSync(`${DIR}/mount.js`, "utf8"); const CSS = readFileSync(`${DIR}/delivery-graphs.css`, "utf8"); const EMBED_HTML = readFileSync(`${DIR}/embed.html`, "utf8"); const STANDALONE_HTML = readFileSync(`${DIR}/standalone.html`, "utf8"); // Pull the module-anchored default URL *spec* (the "" literal inside // `new URL("", import.meta.url)`, e.g. "../app/api/…") out of // `const = config. ?? ;` where the CONST is declared // `const = new URL("", import.meta.url).href;`. This is the unresolved // relative spec, not a resolved URL string — hence `Spec`, not `Url`. function defaultSpec(name: string): string { const m = MOUNT_JS.match(new RegExp(`${name}\\s*=\\s*config\\.\\w+\\s*\\?\\?\\s*(\\w+);`)); assert(m, `mount.js must default ${name} from config with a fallback constant`); const constM = MOUNT_JS.match( new RegExp(`const ${m![1]}\\s*=\\s*new URL\\(\\s*"([^"]*)"\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\.href`), ); assert( constM, `mount.js 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("#524: the compose mount renders a file-input Import control accepting .json", () => { assert(/id="dg-import"/.test(MOUNT_JS), "mount.js must render an Import file input with id=dg-import"); assert(/type="file"/.test(MOUNT_JS), "the Import control must be an "); assert(/accept="[^"]*\.json[^"]*"/.test(MOUNT_JS), "the Import file input must accept .json files"); }); test("#524: Import wires the importToLibrary door (module-anchored relative), reading the file text client-side", () => { const importSpec = defaultSpec("importUrl"); assert( importSpec.endsWith("actions/delivery-graph/library/import"), `importUrl default spec "${importSpec}" must hit the importToLibrary door`, ); assert(!importSpec.startsWith("/"), `default importUrl spec "${importSpec}" must be relative, not absolute (App-View #279 + #467/#536 resolution class)`); // Pin the MODULE-anchored invariant, not merely "not absolute": the spec must step UP out of the // /delivery-graphs/ shell base with "../" (resolved against import.meta.url), so a regression back to a // document-base-relative "app/api/…" — which 404s under the shell base (#467/#536) — fails the guard. assert( importSpec.startsWith("../"), `default importUrl spec "${importSpec}" must be module-anchored with a "../" prefix (steps out of the ` + `/delivery-graphs/ shell base off import.meta.url), not document-base-relative (#467/#536)`, ); // The file's text is read CLIENT-SIDE and POSTed as graphJson to the import door. assert(/\.text\(\)/.test(MOUNT_JS), "mount.js must read the selected file's text client-side via File.text()"); assert(/post\(importUrl,\s*\{\s*graphJson:/.test(MOUNT_JS), "the Import handler must POST the file text as graphJson to the import door"); // The must actually be WIRED to the handler: without a change listener that invokes // importFile(), the door + handler could stay intact while Import is inert (every other assertion // here still green). Pin the change→importFile wiring so removing the listener fails the suite. assert( /addEventListener\(\s*["']change["'][\s\S]{0,200}?importFile\(/.test(MOUNT_JS), "the Import file input's change listener must invoke importFile() so picking a file triggers an import", ); }); test("#524: a successful import routes through #523's single fillComposer() seam", () => { // #523 owns the inbound fill seam; #524 builds on it rather than adding a second inbound fill path. assert(/function fillComposer\(/.test(MOUNT_JS), "the #523 fillComposer() seam must still be present"); const importHandler = MOUNT_JS.slice(MOUNT_JS.indexOf("async function importFile")); assert(importHandler.length > 0, "mount.js must define the importFile handler"); assert(/fillComposer\(text\b/.test(importHandler), "a successful import must route the imported text through the fillComposer() seam"); }); test("#524: an import failure renders the door's path-qualified compile errors inline", () => { const importHandler = MOUNT_JS.slice(MOUNT_JS.indexOf("async function importFile")); assert( /renderErrors\(body\.error,\s*body\.errors\)/.test(importHandler), "an import 400 must render the door's path-qualified errors inline", ); }); test("#524: both shells forward importUrl into the mount (embed via cfg, standalone via ?import=)", () => { // The mount resolves `config.importUrl ?? DEFAULT_IMPORT_URL`, so the default-URL guard above passes // even if a shell drops the `importUrl` forwarding entirely — silently breaking embedded custom // deployments and the standalone `?import=` override. Pin BOTH forwardings, exactly as the shells // already forward previewUrl/stageUrl. assert( /importUrl:\s*cfg\.importUrl/.test(EMBED_HTML), "embed.html must forward the console-injected importUrl (importUrl: cfg.importUrl) into the mount", ); assert( /importUrl:\s*params\.get\("import"\)/.test(STANDALONE_HTML), "standalone.html must forward the ?import= override (importUrl: params.get(\"import\")) into the mount", ); // mount.js must actually read config.importUrl (not hard-code the default), so the forwarding matters. assert( /const importUrl\s*=\s*config\.importUrl\s*\?\?/.test(MOUNT_JS), "mount.js must resolve importUrl from config.importUrl with a fallback default", ); }); test("#524: the native file input is visually hidden and the label reads as a button", () => { assert(/\.dg-import-input/.test(CSS), "the CSS must style the Import file input"); assert(/clip:\s*rect\(0,\s*0,\s*0,\s*0\)/.test(CSS), "the native file input must be visually hidden (the label is the button)"); });