diff --git a/src/commands/flows.test.ts b/src/commands/flows.test.ts index 3fc05a9a2b80..426f8e1c34a2 100644 --- a/src/commands/flows.test.ts +++ b/src/commands/flows.test.ts @@ -172,6 +172,27 @@ describe("flows commands", () => { }); }); + it("shows TaskFlows with Date-invalid timestamps without crashing", async () => { + await withTaskFlowCommandStateDir(async () => { + const flow = createManagedTaskFlow({ + ownerKey: "agent:main:main", + controllerId: "tests/flows-command", + goal: "Inspect malformed flow timestamp", + status: "running", + createdAt: 100, + updatedAt: 8_700_000_000_000_000, + }); + + const runtime = createRuntime(); + await flowsShowCommand({ lookup: flow.flowId, json: false }, runtime); + + const lines = vi.mocked(runtime.log).mock.calls.map(([line]) => String(line)); + expect(lines).toContain(`flowId: ${flow.flowId}`); + expect(lines).toContain("createdAt: 1970-01-01T00:00:00.100Z"); + expect(lines).toContain("updatedAt: n/a"); + }); + }); + it("sanitizes TaskFlow text output before printing to the terminal", async () => { await withTaskFlowCommandStateDir(async () => { const unsafeOwnerKey = "agent:main:\u001b[31mowner"; diff --git a/src/commands/flows.ts b/src/commands/flows.ts index 879da5983ef1..90cc4a93b783 100644 --- a/src/commands/flows.ts +++ b/src/commands/flows.ts @@ -4,6 +4,7 @@ import { formatCliCommand } from "../cli/command-format.js"; import { getRuntimeConfig } from "../config/config.js"; import { info } from "../globals.js"; import type { RuntimeEnv } from "../runtime.js"; +import { timestampMsToIsoString } from "../shared/number-coercion.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { listTasksForFlowId } from "../tasks/runtime-internal.js"; import { cancelFlowById, getFlowTaskSummary } from "../tasks/task-executor.js"; @@ -50,6 +51,10 @@ function shortToken(value: string | undefined, maxChars = ID_PAD): string { return truncate(trimmed, maxChars); } +function formatFlowTimestamp(value: number | undefined | null): string { + return timestampMsToIsoString(value) ?? "n/a"; +} + function formatFlowStatusCell(status: TaskFlowStatus, rich: boolean) { const padded = status.padEnd(STATUS_PAD); if (!rich) { @@ -228,11 +233,11 @@ export async function flowsShowCommand( `notify: ${flow.notifyPolicy}`, ...(stateSummary ? [`state: ${safeFlowDisplayText(stateSummary)}`] : []), ...(flow.cancelRequestedAt - ? [`cancelRequestedAt: ${new Date(flow.cancelRequestedAt).toISOString()}`] + ? [`cancelRequestedAt: ${formatFlowTimestamp(flow.cancelRequestedAt)}`] : []), - `createdAt: ${new Date(flow.createdAt).toISOString()}`, - `updatedAt: ${new Date(flow.updatedAt).toISOString()}`, - `endedAt: ${flow.endedAt ? new Date(flow.endedAt).toISOString() : "n/a"}`, + `createdAt: ${formatFlowTimestamp(flow.createdAt)}`, + `updatedAt: ${formatFlowTimestamp(flow.updatedAt)}`, + `endedAt: ${formatFlowTimestamp(flow.endedAt)}`, `tasks: ${taskSummary.total} total · ${taskSummary.active} active · ${taskSummary.failures} issues`, ]; for (const line of lines) {