From 31462199fe472d37b71bb858eda4b6d7dc3c4e7d Mon Sep 17 00:00:00 2001 From: TheDesignFounder Date: Sun, 4 Oct 2026 12:19:51 -0400 Subject: [PATCH] fix(cli): preserve active execution status when streaming stops --- src/cli.ts | 3 ++- src/render.ts | 8 ++++++++ test/render.test.mjs | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 test/render.test.mjs diff --git a/src/cli.ts b/src/cli.ts index 6a54e04..72382f0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -277,7 +277,8 @@ async function run( } if (outcome.status !== "completed" || !outcome.asset) { - progress.stop(options.json ? undefined : "Failed"); + progress.stop(options.json ? undefined : ["failed", "cancelled"].includes(outcome.status) + ? "Failed" : "Output not confirmed; check the saved execution."); if (outcome.status === "failed" && outcome.execution_id) { const terminal = terminalExecutionError(await api.getExecution(outcome.execution_id)); if (terminal) throw terminal; diff --git a/src/render.ts b/src/render.ts index 16f3713..97cec64 100644 --- a/src/render.ts +++ b/src/render.ts @@ -96,6 +96,14 @@ export async function consume( if (event.event === "started") { outcome.execution_id = String(event.data.execution_id); outcome.conversation_id = String(event.data.conversation_id); + outcome.status = "queued"; + } else if (event.event === "job") { + // A dropped/idle stream is not an unknown or failed execution. Keep + // the most recent server-reported state alongside its recovery ID. + const status = String(event.data.status); + if (["queued", "running", "completed", "failed", "cancelled"].includes(status)) { + outcome.status = status; + } } else if (event.event === "asset") { outcome.asset = { asset_id: String(event.data.asset_id), diff --git a/test/render.test.mjs b/test/render.test.mjs new file mode 100644 index 0000000..2636d35 --- /dev/null +++ b/test/render.test.mjs @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { consume, Progress } from "../dist/render.js"; + +const started = { event: "started", id: "1", data: { execution_id: "execution-1", conversation_id: "conversation-1" } }; + +test("stream interruption preserves running status and execution identity", async () => { + const failure = new Error("stream idle"); + async function* events() { + yield started; + yield { event: "job", id: "2", data: { status: "running" } }; + throw failure; + } + await assert.rejects(consume(events(), new Progress(false)), (error) => { + assert.equal(error, failure); + assert.equal(error.partialOutcome.status, "running"); + assert.equal(error.partialOutcome.execution_id, "execution-1"); + assert.equal(error.partialOutcome.last_event_id, "2"); + return true; + }); +}); + +test("a started execution without a terminal event stays queued, not failed", async () => { + async function* events() { yield started; } + const outcome = await consume(events(), new Progress(false)); + assert.equal(outcome.status, "queued"); + assert.equal(outcome.asset, null); +}); + +test("terminal status remains authoritative after progress", async () => { + async function* events() { + yield started; + yield { event: "job", id: "2", data: { status: "running" } }; + yield { event: "done", id: "3", data: { status: "failed" } }; + } + assert.equal((await consume(events(), new Progress(false))).status, "failed"); +});