Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 7 additions & 3 deletions bin/aas-gui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export function renderPage() {
<body><h1>Agent Action Stack</h1><p>Run the local decide, act, and prove flow using the reviewed component lock.</p>
<div class="state"><label>Response <select id="response"><option value="pass">pass</option><option value="fail">fail</option></select></label>
<label>Fault <select id="fault"><option value="none">none</option><option value="duplicate">duplicate</option></select></label>
<label>Domain <select id="domain"><option value="refund">refund</option><option value="inventory">inventory allocation</option></select></label>
<label><input id="dispute" type="checkbox"> force dispute proof</label>
<label>Prove <select id="prove"><option value="simulate">simulation</option><option value="rail">same-case rail review</option></select></label>
<br><button id="run">Run stack</button>
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -460,23 +461,26 @@ 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;
}
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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Duplicate domains bypass validation

A request with domain=refund&domain=payments passes because get() inspects only the first value. The server runs a refund instead of rejecting the unknown domain.

Suggested change
const selectedDomain = url.searchParams.get("domain") ?? "refund";
const selectedDomains = url.searchParams.getAll("domain");
const selectedDomain = selectedDomains.length === 0 ? "refund" : selectedDomains.length === 1 ? selectedDomains[0] : null;
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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(
Expand Down
22 changes: 22 additions & 0 deletions docs/release-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion test/browser/workbench.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
41 changes: 40 additions & 1 deletion test/gui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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]; } };
Expand Down Expand Up @@ -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, /<select id="domain">/);
assert.match(page, /<option value="refund">refund<\/option>/);
assert.match(page, /<option value="inventory">inventory allocation<\/option>/);
const script = pageScript();
assert.match(script, /domain:document\.getElementById\('domain'\)\.value/);
});

test("GUI run accepts a domain and rejects an unknown one", async () => {
const outputRoot = mkdtempSync(join(tmpdir(), "aas-gui-domain-"));
const seen = [];
const server = createGuiServer({
outputRoot,
runDemoFn: async (args, options) => {
seen.push(args);
return runDemo(args, { ...options, runId: "domain-run", componentResolver: () => [] });
},
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address();
const origin = `http://127.0.0.1:${address.port}`;
const posted = await requestServer(server, "/api/run?response=pass&fault=none&domain=inventory", {
method: "POST",
headers: { origin },
});
assert.equal(posted.status, 200);
assert.deepEqual(seen[0].slice(-2), ["--domain", "inventory"]);
const bogus = await requestServer(server, "/api/run?response=pass&fault=none&domain=payments", {
method: "POST",
headers: { origin },
});
assert.equal(bogus.status, 400);
} finally {
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
Loading