import assert from "node:assert/strict"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "bun:test"; import { resolveCliChildJsonCommandResult, resolveCliChildJsonObject } from "./cli-child-json-recovery"; test("child JSON recovery reads full JSON from CLI stdout dump wrapper", async () => { const dir = await mkdtemp(join(tmpdir(), "unidesk-cli-child-json-dump-")); const dumpPath = join(dir, "dump.json"); await writeFile(dumpPath, JSON.stringify({ ok: true, data: { contract: "full-json" } }) + "\n"); const wrapper = { ok: true, command: "fixture", data: { outputTruncated: true, reason: "stdout-json-bytes-exceeded-threshold", dump: { path: dumpPath }, }, }; const resolved = resolveCliChildJsonObject({ stdout: JSON.stringify(wrapper), requestedStdoutType: "fixture JSON", }); assert.equal(resolved.source, "dump"); assert.equal(resolved.parsed?.ok, true); assert.deepEqual(resolved.parsed?.data, { contract: "full-json" }); assert.equal(resolved.diagnostics.stdoutKind, "dump-wrapper"); assert.equal(resolved.diagnostics.dumpPath, dumpPath); assert.equal(resolved.diagnostics.dumpReadOk, true); assert.equal(resolved.diagnostics.dumpJsonOk, true); }); test("child JSON recovery command-result helper keeps dump diagnostics", async () => { const dir = await mkdtemp(join(tmpdir(), "unidesk-cli-child-json-result-dump-")); const dumpPath = join(dir, "dump.json"); await writeFile(dumpPath, JSON.stringify({ ok: true, verified: true, bytes: 123 }) + "\n"); const resolved = resolveCliChildJsonCommandResult({ result: { stdout: JSON.stringify({ ok: true, data: { outputTruncated: true, reason: "stdout-json-bytes-exceeded-threshold", dump: { path: dumpPath }, }, }), stderr: "", exitCode: 0, timedOut: false, }, requestedStdoutType: "trans download verification JSON", acceptParsed: (value) => value.ok === true && value.verified === true, }); assert.equal(resolved.source, "dump"); assert.equal(resolved.parsed?.verified, true); assert.equal(resolved.diagnostics.requestedStdoutType, "trans download verification JSON"); assert.equal(resolved.diagnostics.dumpPath, dumpPath); }); test("child JSON recovery falls back to artifact after trans truncation summary", () => { const summary = { code: "ssh-truncation-summary", exitCode: 0, timedOut: false, stdout: { stream: "stdout", thresholdBytes: 10240, observedBytesAtTruncation: 165000, forwardedBytes: 10240, dumpPath: null, dumpError: null, }, }; const resolved = resolveCliChildJsonObject({ stdout: `UNIDESK_SSH_TRUNCATION_SUMMARY ${JSON.stringify(summary)}\n`, requestedStdoutType: "web-probe observe analyze compact JSON", artifactFallback: { path: "analysis/report.json", nextCommand: "bun scripts/cli.ts web-probe observe collect webobs-fixture --view files --file analysis/report.json", read: () => ({ ok: true, value: { ok: true, reportJsonPath: "analysis/report.json" }, reason: "analysis-artifact-contract" }), }, }); assert.equal(resolved.source, "artifact"); assert.equal(resolved.parsed?.ok, true); assert.equal(resolved.diagnostics.stdoutKind, "ssh-truncation-summary"); assert.equal(resolved.diagnostics.fallbackReason, "stdout-trans-truncated"); const artifact = resolved.diagnostics.artifact as Record; assert.equal(artifact.ok, true); assert.equal(artifact.nextCommand, "bun scripts/cli.ts web-probe observe collect webobs-fixture --view files --file analysis/report.json"); }); test("child JSON recovery falls back to artifact when stdout is not JSON", () => { const resolved = resolveCliChildJsonObject({ stdout: "remote helper wrote a bounded text summary instead of JSON", requestedStdoutType: "web-probe observe analyze compact JSON", artifactFallback: { path: "analysis/report.json", nextCommand: "collect report", read: () => ({ ok: true, value: { ok: true, counts: { network: 9 }, reportJsonPath: "analysis/report.json" } }), }, }); assert.equal(resolved.source, "artifact"); assert.equal(resolved.parsed?.ok, true); assert.deepEqual(resolved.parsed?.counts, { network: 9 }); assert.equal(resolved.diagnostics.stdoutKind, "non-json"); assert.equal(resolved.diagnostics.fallbackReason, "stdout-not-json"); }); test("child JSON recovery rejects ok-only stdout contract and uses artifact", () => { const resolved = resolveCliChildJsonObject({ stdout: JSON.stringify({ ok: true }), requestedStdoutType: "web-probe observe analyze compact JSON", acceptParsed: (value) => typeof value.reportJsonPath === "string" || typeof value.counts === "object", artifactFallback: { path: "analysis/report.json", nextCommand: "collect report", read: () => ({ ok: true, value: { ok: true, reportJsonPath: "analysis/report.json", counts: { network: 9 } } }), }, }); assert.equal(resolved.source, "artifact"); assert.equal(resolved.parsed?.reportJsonPath, "analysis/report.json"); assert.deepEqual(resolved.parsed?.counts, { network: 9 }); assert.equal(resolved.diagnostics.stdoutKind, "json"); assert.equal(resolved.diagnostics.fallbackReason, "stdout-json-contract-invalid"); assert.equal(resolved.diagnostics.stdoutContractAccepted, false); }); test("child JSON recovery drops invalid ok-only stdout when artifact fallback is missing", () => { const resolved = resolveCliChildJsonObject({ stdout: JSON.stringify({ ok: true }), requestedStdoutType: "web-probe observe analyze compact JSON", acceptParsed: (value) => typeof value.reportJsonPath === "string" || typeof value.counts === "object", artifactFallback: { path: "analysis/report.json", nextCommand: "collect report", read: () => ({ ok: false, reason: "artifact-missing", path: "analysis/report.json" }), }, }); assert.equal(resolved.parsed, null); assert.equal(resolved.source, null); assert.equal(resolved.diagnostics.stdoutKind, "json"); assert.equal(resolved.diagnostics.fallbackReason, "stdout-json-contract-invalid"); assert.equal(resolved.diagnostics.stdoutContractAccepted, false); const artifact = resolved.diagnostics.artifact as Record; assert.equal(artifact.ok, false); assert.equal(artifact.reason, "artifact-missing"); assert.equal(artifact.path, "analysis/report.json"); assert.equal(artifact.nextCommand, "collect report"); }); test("child JSON recovery reports artifact fallback failure when stdout is unusable and artifact is missing", () => { const resolved = resolveCliChildJsonObject({ stdout: "not json", requestedStdoutType: "web-probe observe analyze compact JSON", artifactFallback: { path: "analysis/report.json", nextCommand: "collect report", read: () => ({ ok: false, reason: "artifact-missing", path: "analysis/report.json" }), }, }); assert.equal(resolved.parsed, null); assert.equal(resolved.source, null); assert.equal(resolved.diagnostics.stdoutKind, "non-json"); assert.equal(resolved.diagnostics.fallbackReason, "stdout-not-json"); const artifact = resolved.diagnostics.artifact as Record; assert.equal(artifact.ok, false); assert.equal(artifact.reason, "artifact-missing"); assert.equal(artifact.path, "analysis/report.json"); assert.equal(artifact.nextCommand, "collect report"); });