From ace68b09146519a0369fd85cdb488727905bc748 Mon Sep 17 00:00:00 2001 From: Daniel Oon Date: Sun, 6 Sep 2026 15:37:27 +0800 Subject: [PATCH] feat: GUI domain selector with batched release evidence Track B parity: the CLI has supported --domain since the inventory domain landed, but the GUI had no way to select it. Added a Domain selector (default refund), validated on the server like every other run option, and pushed through to the orchestrator. Also records the batched release evidence for the review-workspace, replay, integrator-example, settled-review, history/comparison, inventory, and extension-guide merges, including the pending run-lifecycle and replay-stdin entries, and distinguishes browser tests from component and orchestrator unit tests. --- README.md | 3 ++- bin/aas-gui.mjs | 10 +++++--- docs/release-readiness.md | 22 ++++++++++++++++++ test/browser/workbench.spec.mjs | 12 +++++++++- test/gui.test.mjs | 41 ++++++++++++++++++++++++++++++++- 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 64c9cad..b185c7a 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ node ./bin/aas.mjs demo --domain inventory --fault duplicate --prove rail Both domains keep their own policy fixture and their own remedy scope field (`max_amount_minor` for refunds, `max_quantity` for allocations), so neither -is disguised as the other. Everything remains synthetic: no warehouse, +is disguised as the other. The GUI exposes the same choice with a Domain +selector. Everything remains synthetic: no warehouse, merchant, payment, or external provider integration is involved, and a recorded review proves the handoff rather than any real-world reversibility. diff --git a/bin/aas-gui.mjs b/bin/aas-gui.mjs index c90f6e0..98e2289 100644 --- a/bin/aas-gui.mjs +++ b/bin/aas-gui.mjs @@ -255,6 +255,7 @@ export function renderPage() {

Agent Action Stack

Run the local decide, act, and prove flow using the reviewed component lock.

+
@@ -307,7 +308,7 @@ runButton.addEventListener('click',async()=>{ bindings.innerHTML=''; clearImported(); output.textContent='Running...'; - const query=new URLSearchParams({response:document.getElementById('response').value,fault:document.getElementById('fault').value,prove:document.getElementById('prove').value}); + const query=new URLSearchParams({response:document.getElementById('response').value,fault:document.getElementById('fault').value,prove:document.getElementById('prove').value,domain:document.getElementById('domain').value}); if(document.getElementById('dispute').checked) query.set('dispute','1'); let runBody; try { @@ -460,7 +461,7 @@ export function createGuiServer({ return; } if (request.method === "POST" && url.pathname === "/api/run") { - const allowedKeys = new Set(["response", "fault", "dispute", "prove"]); + const allowedKeys = new Set(["response", "fault", "dispute", "prove", "domain"]); if ([...url.searchParams.keys()].some((key) => !allowedKeys.has(key))) { sendJson(response, 400, { error: "Invalid options" }); return; @@ -468,15 +469,18 @@ export function createGuiServer({ const selectedResponse = url.searchParams.get("response") ?? "pass"; const selectedFault = url.searchParams.get("fault") ?? "none"; const selectedProve = url.searchParams.get("prove") ?? "simulate"; + const selectedDomain = url.searchParams.get("domain") ?? "refund"; if (!new Set(["pass", "fail"]).has(selectedResponse) || !new Set(["none", "duplicate"]).has(selectedFault) || !new Set([null, "1"]).has(url.searchParams.get("dispute")) - || !new Set(["simulate", "rail"]).has(selectedProve)) { + || !new Set(["simulate", "rail"]).has(selectedProve) + || !new Set(["refund", "inventory"]).has(selectedDomain)) { sendJson(response, 400, { error: "Invalid options" }); return; } const args = ["--response", selectedResponse, "--fault", selectedFault, "--json"]; if (url.searchParams.get("dispute") === "1") args.push("--dispute"); + if (selectedDomain !== "refund") args.push("--domain", selectedDomain); if (selectedProve !== "simulate") args.push("--prove", selectedProve); try { assertFullStackNodeVersion( diff --git a/docs/release-readiness.md b/docs/release-readiness.md index bff00b8..9966f42 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -197,6 +197,28 @@ path were exercised live against real child processes. No browser harness exists in this repository; DOM event dispatch is covered through a stub-DOM test of the real page script. +## Integration evidence, review-workspace and domain batch + +Component pins: testbench `16b2faa7` and mandatebound `e526c4c` unchanged; +Consequence Rail at `6c61e9f` (inventory-allocation domain, PR #22). This +batch covers the GUI review workspace (PR #26), portable replay (PR #27), +the integrator example (PR #28), settled-review integration coverage +(PR #30), case history and comparison (PR #35), the inventory domain in the +stack (PR #36), the integrator extension guide and connector conformance +example (PR #37), and the pending run-lifecycle and replay-stdin entries +from PRs #32 and #33. + +Unit-test coverage: stack suite 107/107; consequence-rail 162/162; +mandatebound 228/228; testbench 120/120. Full-stack coverage: `npm run +integration` (pass, refusal, dispute, settled-review, rail-review, and +inventory paths plus export/replay and history/compare), the integrator +example and connector-conformance example, and 10 real browser workflow +tests, on Ubuntu and Windows with Node.js 22.12.0 and 24 and Python 3.13. + +Browser tests are a distinct category from component and orchestrator unit +tests: they drive real clicks, file selection, and asynchronous responses +against the pinned components, and are the only evidence for UI behaviour. + ## Publication boundary This document is a readiness checklist, not a publication approval. A separate diff --git a/test/browser/workbench.spec.mjs b/test/browser/workbench.spec.mjs index dff2723..7fd11c6 100644 --- a/test/browser/workbench.spec.mjs +++ b/test/browser/workbench.spec.mjs @@ -16,10 +16,11 @@ function caseFile(name, contents) { return path; } -async function runStack(page, { response = "pass", fault = "none", prove = "simulate", dispute = false } = {}) { +async function runStack(page, { response = "pass", fault = "none", prove = "simulate", domain = "refund", dispute = false } = {}) { await page.selectOption("#response", response); await page.selectOption("#fault", fault); await page.selectOption("#prove", prove); + await page.selectOption("#domain", domain); if (dispute) await page.check("#dispute"); else await page.uncheck("#dispute"); await page.click("#run"); @@ -135,6 +136,15 @@ test("case history loads and two cases can be compared through the UI", async ({ await expect(page.locator("#compare-result")).toContainText("matching metadata does not prove matching evidence"); }); +test("the inventory domain runs through the UI with its own policy", async ({ page }) => { + await page.goto("/"); + await runStack(page, { domain: "inventory", fault: "duplicate", prove: "rail" }); + await expect(page.locator("#summary")).toContainText("decide: passed"); + await expect(page.locator("#summary")).toContainText("policy aas-inventory-gate-v1"); + await expect(page.locator("#summary")).toContainText("mode rail-review"); + await expect(page.locator("#bindings")).toContainText("recomputed match"); +}); + test("comparison reports an explicit error when a selection is missing", async ({ page }) => { await page.goto("/"); await page.click("#compare"); diff --git a/test/gui.test.mjs b/test/gui.test.mjs index 7454e2a..287efee 100644 --- a/test/gui.test.mjs +++ b/test/gui.test.mjs @@ -293,7 +293,7 @@ function pageScript() { function stubDocument() { const elements = {}; - for (const id of ["response", "fault", "dispute", "prove", "run", "download", "output", "summary", "bindings", "case-file", "replay", "import-status", "import-result", "load-history", "left-case", "right-case", "compare", "compare-status", "compare-result", "history-list"]) { + for (const id of ["response", "fault", "dispute", "prove", "run", "download", "output", "summary", "bindings", "case-file", "replay", "import-status", "import-result", "load-history", "left-case", "right-case", "compare", "compare-status", "compare-result", "history-list", "domain"]) { elements[id] = { value: "pass", checked: false, disabled: false, textContent: "", innerHTML: "", href: null, style: {}, listeners: {}, addEventListener(name, fn) { this.listeners[name] = fn; }, removeAttribute(name) { delete this[name]; } }; @@ -618,3 +618,42 @@ test("GUI history and compare endpoints serve summaries and classifications", as await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } }); + +test("GUI exposes a domain selector defaulting to refund", () => { + const page = renderPage(); + assert.match(page, /