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
22 changes: 14 additions & 8 deletions bin/aas.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ function booleanField(payload, field, label) {
return payload[field];
}

function parseStageJson(label, result) {
try {
return parseJsonOutput(result.stdout, label);
} catch (error) {
if (result.status !== 0) throw childProcessError(label, result);
throw error;
}
}

function pythonCandidates() {
if (process.platform === "win32") {
return [["py", ["-3"]], ["python", []], ["python3", []]];
Expand Down Expand Up @@ -195,14 +204,10 @@ export function runDecide(
lastError = result.error;
continue;
}
const evaluation = parseStageJson("decide", result);
if (result.status !== 0) {
const payload = (result.stdout || "").trim();
if (payload.startsWith("{")) {
return { ok: false, raw: parseJsonOutput(payload, "decide"), status: result.status };
}
throw childProcessError("decide", result);
return { ok: false, raw: evaluation, status: result.status };
Comment on lines 208 to +209

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nonzero decision checks report success

When runDecide receives JSON with a nonzero status, it returns a normal policy rejection. The demo exits zero, so automation accepts a failed child process.

Prompt for agents
Nonzero decide results now preserve structured stdout but flow through runDemo's ordinary policy-rejection branch, which deliberately leaves exitCode at zero for valid policy denials. Distinguish a child process's nonzero status from a zero-status policy rejection while retaining the stage artifact. Update bin/aas.mjs so a structured nonzero decide result records the failed stage and bundle but produces a nonzero run/CLI/GUI result. Add an integration test covering runDemo with a real runDecide result whose runner returns parseable stdout and a nonzero status.
Devin Review

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

}
const evaluation = parseJsonOutput(result.stdout, "decide");
return { ok: booleanField(evaluation, "passed", "decide"), raw: evaluation, status: 0 };
}
throw new Error(`Python not found for decide stage${lastError ? ` (${lastError.code ?? "spawn-error"})` : ""}`);
Expand Down Expand Up @@ -237,8 +242,9 @@ export function runProve(
[cli, "simulate", "--scenario", scenario],
{ cwd: join(depsDir, "mandatebound") },
);
if (result.status !== 0) throw childProcessError("prove", result);
const payload = parseJsonOutput(result.stdout, "prove");
if (result.error) throw childProcessError("prove", result);
const payload = parseStageJson("prove", result);
if (result.status !== 0) return { ok: false, raw: payload, status: result.status };
return { ok: booleanField(payload, "ok", "prove"), raw: payload, status: 0 };
}

Expand Down
70 changes: 70 additions & 0 deletions test/stack.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,56 @@ test("decide and prove reject non-boolean success fields", () => {
assert.throws(() => runProve("unused", { runner }), /boolean ok field/);
});

test("decide reads logged JSON on a nonzero exit", () => {
const result = runDecide("unused", {
runner: () => ({
status: 1,
stdout: 'evaluating policy\n{\n "passed": false,\n "policy_id": "refund-v1"\n}\n',
stderr: "",
error: null,
}),
});
assert.equal(result.ok, false);
assert.equal(result.status, 1);
assert.equal(result.raw.policy_id, "refund-v1");
});

test("prove treats nonzero JSON as an unsuccessful proof", () => {
const result = runProve("unused", {
runner: () => ({
status: 2,
stdout: '{"ok":false,"error":{"code":"ALB_CLI_USAGE","message":"Simulate accepts one scenario."}}\n',
stderr: '{"level":"error","code":"ALB_CLI_USAGE"}\n',
error: null,
}),
});
assert.equal(result.ok, false);
assert.equal(result.status, 2);
assert.equal(result.raw.error.code, "ALB_CLI_USAGE");
});

test("prove fail-closes a nonzero payload that claims success", () => {
const result = runProve("unused", {
runner: () => ({
status: 5,
stdout: '{"ok":true,"result":{}}\n',
stderr: "",
error: null,
}),
});
assert.equal(result.ok, false);
assert.equal(result.status, 5);
});

test("prove still throws when a nonzero child has no JSON", () => {
assert.throws(
() => runProve("unused", {
runner: () => ({ status: 2, stdout: "simulate failed\n", stderr: "", error: null }),
}),
/exited with status 2/,
);
});

test("child output parser accepts logged pretty-printed JSON", () => {
assert.deepEqual(
parseJsonOutput('starting child\n{\n "ok": true,\n "result": { "count": 2 }\n}\n', "fixture"),
Expand Down Expand Up @@ -233,6 +283,26 @@ test("an unsuccessful proof fails the run", async () => {
assert.equal(result.manifest.stages.prove.status, "failed");
});

test("nonzero prove JSON is recorded as a failed proof, not a child-process error", async () => {
const outputRoot = tempRoot();
const options = stubOptions(outputRoot, { runId: "prove-json-fail-run" });
delete options.runProveFn;
options.runner = () => ({
status: 2,
stdout: '{"ok":false,"error":{"code":"ALB_CLI_USAGE","message":"Simulate accepts one scenario."}}\n',
stderr: "",
error: null,
});
const result = await runDemo(["--response", "pass", "--dispute"], options);
assert.equal(result.exitCode, 1);
assert.equal(result.manifest.stages.prove.status, "failed");
assert.equal(result.report.stages.prove.ok, false);
assert.deepEqual(
JSON.parse(readFileSync(join(result.bundleDir, "stages", "prove.json"), "utf8")).error,
{ code: "ALB_CLI_USAGE", message: "Simulate accepts one scenario." },
);
});

test("child-process errors are visible as safe stage errors and downstream skips", async () => {
const outputRoot = tempRoot();
const result = await runDemo(["--response", "pass"], stubOptions(outputRoot, {
Expand Down
Loading