diff --git a/bin/aas.mjs b/bin/aas.mjs index 134e608..079fc36 100644 --- a/bin/aas.mjs +++ b/bin/aas.mjs @@ -112,22 +112,25 @@ export function parseJsonOutput(text, label) { if (!trimmed) { throw new Error(`${label} produced empty output.`); } + let parseError; + try { + return JSON.parse(trimmed); + } catch (error) { + parseError = error; + } const lines = trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0); - for (let index = lines.length - 1; index >= 0; index -= 1) { - const line = lines[index].trim(); - if (line.startsWith("{") || line.startsWith("[")) { + for (let end = lines.length; end > 0; end -= 1) { + if (!/[}\]]$/.test(lines[end - 1].trim())) continue; + for (let start = 0; start < end; start += 1) { + if (!/^[\[{]/.test(lines[start].trim())) continue; try { - return JSON.parse(line); + return JSON.parse(lines.slice(start, end).join("\n")); } catch { - // Continue until the complete output is tried. + // Keep looking for the last complete JSON range. } } } - try { - return JSON.parse(trimmed); - } catch (error) { - throw new Error(`${label} did not return JSON: ${error.message}`); - } + throw new Error(`${label} did not return JSON: ${parseError.message}`); } function pythonCandidates() { diff --git a/test/stack.test.mjs b/test/stack.test.mjs index e577805..f327413 100644 --- a/test/stack.test.mjs +++ b/test/stack.test.mjs @@ -6,6 +6,7 @@ import test from "node:test"; import { fileURLToPath } from "node:url"; import { loadComponentLock, inspectDependencyDirectory, npmInvocation } from "../scripts/bootstrap.mjs"; import { + parseJsonOutput, persistRunBundle, resolveComponentProvenance, runAct, @@ -139,6 +140,20 @@ test("act rejects a zero-exit payload without a valid outcome", () => { ); }); +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"), + { ok: true, result: { count: 2 } }, + ); +}); + +test("child output parser accepts JSON before a trailing log", () => { + assert.deepEqual( + parseJsonOutput('{"ok":true}\nchild complete\n', "fixture"), + { ok: true }, + ); +}); + test("pass bundle contains stage status, provenance, and only current artifacts", async () => { const outputRoot = tempRoot(); const result = await runDemo(["--response", "pass"], stubOptions(outputRoot, { runId: "pass-run" }));