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
65 changes: 65 additions & 0 deletions extensions/workflows/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import type { TUI } from "@earendil-works/pi-tui";
import type { Theme, WorkflowDetails } from "./model.ts";
import { SPINNER_INTERVAL_MS } from "../shared/spinner.ts";
import { safeStringify } from "./serialization.ts";

// runsDir() resolves against getAgentDir(), which reads this env var.
const agentDir = mkdtempSync(join(tmpdir(), "my-pi-setup-workflows-"));
Expand Down Expand Up @@ -85,6 +86,70 @@ test("persisted nonterminal invocation facts are projected as uncertain", () =>
assert.equal(restored?.agents[0]?.invocation?.outcome, "uncertain");
});

test("unknown or missing persisted states fail closed without breaking known aliases", () => {
for (const state of [undefined, "future-state"]) {
const restored = normalizePersistedWorkflowDetails("wf_unknown", {
...(state === undefined ? {} : { status: state }),
agents: [
{
index: 1,
label: "unknown",
...(state === undefined ? {} : { state }),
},
],
phases: [],
});
assert.equal(restored?.status, "uncertain");
assert.equal(restored?.agents[0]?.state, "uncertain");
}

const legacy = normalizePersistedWorkflowDetails("wf_legacy_aliases", {
status: "completed",
agents: [
{ index: 1, label: "done", state: "completed" },
{ index: 2, label: "failed", state: "failed" },
],
phases: [],
});
assert.equal(legacy?.status, "completed");
assert.deepEqual(
legacy?.agents.map((agent) => agent.state),
["done", "error"],
);

const unknownAgent = normalizePersistedWorkflowDetails("wf_unknown_agent", {
status: "completed",
agents: [{ index: 1, label: "unknown" }],
phases: [],
});
assert.equal(unknownAgent?.status, "uncertain");
assert.equal(unknownAgent?.agents[0]?.state, "uncertain");
});

test("an oversized workflow truncation stub cannot become completed", () => {
const stub = JSON.parse(
safeStringify(
{ status: "completed", payload: "x".repeat(2_000) },
{ maxBytes: 256 },
),
);
assert.equal(stub.truncated, true);
assert.equal(
normalizePersistedWorkflowDetails("wf_truncated", stub)?.status,
"uncertain",
);
});

test("a terminal persisted run cannot retain running agents", () => {
const restored = normalizePersistedWorkflowDetails("wf_contradictory", {
status: "completed",
agents: [{ index: 1, label: "still running", state: "running" }],
phases: [],
});
assert.equal(restored?.status, "uncertain");
assert.equal(restored?.agents[0]?.state, "uncertain");
});

test("persisted transcripts retain exact tool call identities", () => {
const details = normalizePersistedWorkflowDetails("wf_tools", {
status: "completed",
Expand Down
56 changes: 41 additions & 15 deletions extensions/workflows/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,35 @@ function normalizeTranscript(value: unknown): TranscriptEntry[] {
return transcript;
}

function normalizeAgentState(value: unknown): AgentRecord["state"] {
switch (value) {
case "done":
case "completed":
return "done";
case "error":
case "failed":
return "error";
case "running":
case "uncertain":
return value;
default:
return "uncertain";
}
}

function normalizeWorkflowStatus(value: unknown): WorkflowDetails["status"] {
switch (value) {
case "running":
case "completed":
case "failed":
case "aborted":
case "uncertain":
return value;
default:
return "uncertain";
}
}

/** Leniently normalize a workflow.json (including runs from older tooling). */
export function normalizePersistedWorkflowDetails(
runId: string,
Expand All @@ -283,14 +312,7 @@ export function normalizePersistedWorkflowDetails(
for (const item of rawAgents) {
if (!item || typeof item !== "object") continue;
const a = item as Record<string, unknown>;
const state =
a.state === "error" || a.state === "failed"
? "error"
: a.state === "uncertain"
? "uncertain"
: a.state === "running"
? "running"
: "done";
const state = normalizeAgentState(a.state);
const index = typeof a.index === "number" ? a.index : agents.length + 1;
const decodedInvocation = decodeInvocationRecord(a.invocation);
const invocation =
Expand Down Expand Up @@ -409,13 +431,17 @@ export function normalizePersistedWorkflowDetails(
});
}

const status =
record.status === "running" ||
record.status === "failed" ||
record.status === "aborted" ||
record.status === "uncertain"
? record.status
: "completed";
let status = normalizeWorkflowStatus(record.status);
if (status !== "running") {
for (const agent of agents) {
if (agent.state !== "running" && agent.state !== "uncertain") continue;
status = "uncertain";
agent.state = "uncertain";
agent.error =
agent.error ??
"Persisted terminal workflow contained an agent without terminal evidence";
}
}

return {
runId,
Expand Down
Loading