fix: cancelled threads show distinct status instead of completed
CI / test (pull_request) Failing after 34s
CI / test (pull_request) Failing after 34s
Fixes #522
This commit is contained in:
+1
-1
@@ -12,4 +12,4 @@ packages/workflow-template-develop/develop.esm.js
|
||||
.DS_Store
|
||||
*.py
|
||||
.claude
|
||||
tmp
|
||||
tmp.worktrees/
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
],
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"uwf": "./src/cli.ts"
|
||||
"uwf": "./dist/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/json-cas": "^0.5.3",
|
||||
|
||||
@@ -40,6 +40,7 @@ describe("resolveHeadHash", () => {
|
||||
workflow: workflowHash,
|
||||
head: headHash,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
|
||||
const result = await resolveHeadHash(tmpDir, threadId);
|
||||
@@ -64,6 +65,7 @@ describe("resolveHeadHash", () => {
|
||||
workflow: workflowHash,
|
||||
head: historicalHash,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
|
||||
const result = await resolveHeadHash(tmpDir, threadId);
|
||||
@@ -87,18 +89,21 @@ describe("resolveHeadHash", () => {
|
||||
workflow: workflowHash,
|
||||
head: hash1,
|
||||
completedAt: Date.now() - 2000,
|
||||
reason: null,
|
||||
});
|
||||
await appendThreadHistory(tmpDir, {
|
||||
thread: threadId2,
|
||||
workflow: workflowHash,
|
||||
head: hash2,
|
||||
completedAt: Date.now() - 1000,
|
||||
reason: null,
|
||||
});
|
||||
await appendThreadHistory(tmpDir, {
|
||||
thread: threadId3,
|
||||
workflow: workflowHash,
|
||||
head: hash3,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
|
||||
const result = await resolveHeadHash(tmpDir, threadId2);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { appendThreadHistory, loadThreadHistory } from "../store.js";
|
||||
|
||||
describe("thread cancel status", () => {
|
||||
test("cancelled history entry has reason 'cancelled'", async () => {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
||||
const threadId = "01JTEST000000000000CANCEL1" as ThreadId;
|
||||
|
||||
await appendThreadHistory(tmpDir, {
|
||||
thread: threadId,
|
||||
workflow: "test-workflow",
|
||||
head: "test-head-hash" as CasRef,
|
||||
completedAt: Date.now(),
|
||||
reason: "cancelled",
|
||||
});
|
||||
|
||||
const history = await loadThreadHistory(tmpDir);
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0]?.reason).toBe("cancelled");
|
||||
});
|
||||
|
||||
test("completed history entry has reason 'completed'", async () => {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
||||
const threadId = "01JTEST000000000000CANCEL2" as ThreadId;
|
||||
|
||||
await appendThreadHistory(tmpDir, {
|
||||
thread: threadId,
|
||||
workflow: "test-workflow",
|
||||
head: "test-head-hash" as CasRef,
|
||||
completedAt: Date.now(),
|
||||
reason: "completed",
|
||||
});
|
||||
|
||||
const history = await loadThreadHistory(tmpDir);
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0]?.reason).toBe("completed");
|
||||
});
|
||||
|
||||
test("legacy history entry without reason parses as null", async () => {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
||||
const threadId = "01JTEST000000000000CANCEL3" as ThreadId;
|
||||
|
||||
// Simulate legacy entry without reason field
|
||||
await appendThreadHistory(tmpDir, {
|
||||
thread: threadId,
|
||||
workflow: "test-workflow",
|
||||
head: "test-head-hash" as CasRef,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
|
||||
const history = await loadThreadHistory(tmpDir);
|
||||
expect(history).toHaveLength(1);
|
||||
expect(history[0]?.reason).toBeNull();
|
||||
});
|
||||
|
||||
test("mixed completed and cancelled entries preserve distinct reasons", async () => {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
||||
|
||||
await appendThreadHistory(tmpDir, {
|
||||
thread: "01JTEST000000000000CANCEL4" as ThreadId,
|
||||
workflow: "test-workflow",
|
||||
head: "head1" as CasRef,
|
||||
completedAt: Date.now(),
|
||||
reason: "completed",
|
||||
});
|
||||
|
||||
await appendThreadHistory(tmpDir, {
|
||||
thread: "01JTEST000000000000CANCEL5" as ThreadId,
|
||||
workflow: "test-workflow",
|
||||
head: "head2" as CasRef,
|
||||
completedAt: Date.now(),
|
||||
reason: "cancelled",
|
||||
});
|
||||
|
||||
const history = await loadThreadHistory(tmpDir);
|
||||
expect(history).toHaveLength(2);
|
||||
expect(history[0]?.reason).toBe("completed");
|
||||
expect(history[1]?.reason).toBe("cancelled");
|
||||
});
|
||||
});
|
||||
@@ -74,6 +74,7 @@ async function completeThread(
|
||||
workflow: workflowHash,
|
||||
head: headHash,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -758,6 +758,7 @@ describe("cmdStepList with completed threads", () => {
|
||||
workflow: workflowHash,
|
||||
head: step2Hash,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
|
||||
const result = await cmdStepList(tmpDir, threadId);
|
||||
@@ -886,6 +887,7 @@ describe("cmdStepShow with completed threads", () => {
|
||||
workflow: workflowHash,
|
||||
head: stepHash,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
|
||||
const result = await cmdStepShow(tmpDir, stepHash);
|
||||
@@ -949,6 +951,7 @@ describe("cmdThreadRead with completed threads", () => {
|
||||
workflow: workflowHash,
|
||||
head: stepHash,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
|
||||
const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
|
||||
@@ -1011,6 +1014,7 @@ describe("cmdThreadRead with completed threads", () => {
|
||||
workflow: workflowHash,
|
||||
head: step3Hash,
|
||||
completedAt: Date.now(),
|
||||
reason: null,
|
||||
});
|
||||
|
||||
const markdown = await cmdThreadRead(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
#!/usr/bin/env node
|
||||
|
||||
import type { CasRef, ThreadId } from "@uncaged/workflow-protocol";
|
||||
import { Command } from "commander";
|
||||
@@ -181,11 +181,11 @@ function parseStatusFilter(status: string | undefined): ThreadStatus[] | null {
|
||||
if (raw === "active") return ["idle", "running"];
|
||||
|
||||
const parts = raw.split(",").map((s) => s.trim());
|
||||
const validStatuses: ThreadStatus[] = ["idle", "running", "completed"];
|
||||
const validStatuses: ThreadStatus[] = ["idle", "running", "completed", "cancelled"];
|
||||
for (const part of parts) {
|
||||
if (!validStatuses.includes(part as ThreadStatus)) {
|
||||
process.stderr.write(
|
||||
`Invalid status: ${part}. Must be one of: idle, running, completed, active\n`,
|
||||
`Invalid status: ${part}. Must be one of: idle, running, completed, cancelled, active\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -238,7 +238,7 @@ thread
|
||||
.description("List threads")
|
||||
.option(
|
||||
"--status <status>",
|
||||
"Filter by status: idle, running, completed, active (idle+running), or comma-separated values",
|
||||
"Filter by status: idle, running, completed, cancelled, active (idle+running), or comma-separated values",
|
||||
)
|
||||
.option("--after <date>", "Filter threads created after this date (ISO or relative like '7d')")
|
||||
.option("--before <date>", "Filter threads created before this date (ISO or relative like '7d')")
|
||||
|
||||
@@ -331,7 +331,7 @@ export async function cmdThreadShow(storageRoot: string, threadId: ThreadId): Pr
|
||||
fail(`thread not found: ${threadId}`);
|
||||
}
|
||||
|
||||
export type ThreadStatus = "idle" | "running" | "completed";
|
||||
export type ThreadStatus = "idle" | "running" | "completed" | "cancelled";
|
||||
|
||||
export type ThreadListItemWithStatus = ThreadListItem & {
|
||||
status: ThreadStatus;
|
||||
@@ -389,7 +389,7 @@ async function collectCompletedThreads(
|
||||
thread: entry.thread,
|
||||
workflow: entry.workflow,
|
||||
head: entry.head,
|
||||
status: "completed",
|
||||
status: entry.reason === "cancelled" ? "cancelled" : "completed",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -444,7 +444,10 @@ export async function cmdThreadList(
|
||||
let items = await collectActiveThreads(storageRoot, uwf, index);
|
||||
|
||||
// Collect completed threads (if relevant for status filter)
|
||||
const includeCompleted = statusFilter === null || statusFilter.includes("completed");
|
||||
const includeCompleted =
|
||||
statusFilter === null ||
|
||||
statusFilter.includes("completed") ||
|
||||
statusFilter.includes("cancelled");
|
||||
if (includeCompleted) {
|
||||
const activeIds = new Set(items.map((i) => i.thread));
|
||||
const completedItems = await collectCompletedThreads(storageRoot, activeIds);
|
||||
@@ -811,6 +814,7 @@ async function archiveThread(
|
||||
workflow,
|
||||
head,
|
||||
completedAt: Date.now(),
|
||||
reason: "completed",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1147,6 +1151,7 @@ export async function cmdThreadCancel(
|
||||
workflow,
|
||||
head,
|
||||
completedAt: Date.now(),
|
||||
reason: "cancelled",
|
||||
};
|
||||
await appendThreadHistory(storageRoot, historyEntry);
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export function getHistoryPath(storageRoot: string): string {
|
||||
|
||||
export type ThreadHistoryLine = ThreadListItem & {
|
||||
completedAt: number;
|
||||
reason: "completed" | "cancelled" | null;
|
||||
};
|
||||
|
||||
export type UwfStore = {
|
||||
@@ -228,7 +229,15 @@ export async function loadThreadHistory(storageRoot: string): Promise<ThreadHist
|
||||
typeof head === "string" &&
|
||||
typeof completedAt === "number"
|
||||
) {
|
||||
lines.push({ thread: thread as ThreadId, workflow, head, completedAt });
|
||||
const reason = rec.reason;
|
||||
const parsedReason = reason === "completed" || reason === "cancelled" ? reason : null;
|
||||
lines.push({
|
||||
thread: thread as ThreadId,
|
||||
workflow,
|
||||
head,
|
||||
completedAt,
|
||||
reason: parsedReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
|
||||
Reference in New Issue
Block a user