@united-workforce/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/__tests__/adapter-json-roundtrip.test.js +1 -1
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
- package/dist/__tests__/build-step-entry.test.d.ts +2 -0
- package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
- package/dist/__tests__/build-step-entry.test.js +173 -0
- package/dist/__tests__/build-step-entry.test.js.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
- package/dist/__tests__/config.test.js +26 -302
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/current-role.test.js +7 -6
- package/dist/__tests__/current-role.test.js.map +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js +20 -23
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- package/dist/__tests__/pid-recycling.test.d.ts +2 -0
- package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +271 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +321 -0
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +4 -4
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/setup-agent-discovery.test.js +21 -30
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-complexity.test.js +2 -168
- package/dist/__tests__/setup-complexity.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
- package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
- package/dist/__tests__/setup-no-llm.test.js +52 -0
- package/dist/__tests__/setup-no-llm.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +24 -27
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.d.ts +2 -0
- package/dist/__tests__/step-ask.test.d.ts.map +1 -0
- package/dist/__tests__/step-ask.test.js +499 -0
- package/dist/__tests__/step-ask.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.js +1 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-timing.test.js +2 -0
- package/dist/__tests__/step-timing.test.js.map +1 -1
- package/dist/__tests__/store-global-cas.test.js +2 -2
- package/dist/__tests__/store-global-cas.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +9 -9
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +6 -6
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-list-filters.test.js +344 -9
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-poke.test.d.ts +2 -0
- package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
- package/dist/__tests__/thread-poke.test.js +412 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.js +10 -14
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-show-status.test.js +17 -28
- package/dist/__tests__/thread-show-status.test.js.map +1 -1
- package/dist/__tests__/thread-suspend-step.test.js +8 -14
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-suspended-display.test.js +10 -22
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
- package/dist/__tests__/thread.test.js +4 -4
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +49 -21
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
- package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-list-recursive.test.js +283 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +36 -21
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-show-resolution.test.js +210 -0
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
- package/dist/__tests__/workflow-validate.test.d.ts +2 -0
- package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-validate.test.js +687 -0
- package/dist/__tests__/workflow-validate.test.js.map +1 -0
- package/dist/background/background.d.ts +22 -1
- package/dist/background/background.d.ts.map +1 -1
- package/dist/background/background.js +83 -6
- package/dist/background/background.js.map +1 -1
- package/dist/background/index.d.ts +1 -1
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -1
- package/dist/background/index.js.map +1 -1
- package/dist/background/types.d.ts +1 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/cli.js +66 -31
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +3 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +7 -33
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +15 -2
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +7 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +27 -302
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +44 -1
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +255 -11
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +16 -3
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +379 -140
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +9 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +130 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/moderator/__tests__/evaluate.test.js +31 -17
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
- package/dist/moderator/evaluate.d.ts.map +1 -1
- package/dist/moderator/evaluate.js +4 -16
- package/dist/moderator/evaluate.js.map +1 -1
- package/dist/moderator/index.d.ts +1 -2
- package/dist/moderator/index.d.ts.map +1 -1
- package/dist/moderator/index.js +0 -1
- package/dist/moderator/index.js.map +1 -1
- package/dist/moderator/types.d.ts +6 -10
- package/dist/moderator/types.d.ts.map +1 -1
- package/dist/moderator/types.js +1 -3
- package/dist/moderator/types.js.map +1 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -3
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +28 -9
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +75 -16
- package/dist/store.js.map +1 -1
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +83 -66
- package/dist/validate-semantic.js.map +1 -1
- package/dist/validate.d.ts +6 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +24 -0
- package/dist/validate.js.map +1 -1
- package/package.json +8 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +1 -1
- package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
- package/src/__tests__/build-step-entry.test.ts +203 -0
- package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
- package/src/__tests__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +20 -23
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
- package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
- package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- package/src/__tests__/pid-recycling.test.ts +328 -0
- package/src/__tests__/prompt.test.ts +397 -0
- package/src/__tests__/resolve-head-hash.test.ts +4 -4
- package/src/__tests__/setup-agent-discovery.test.ts +26 -51
- package/src/__tests__/setup-complexity.test.ts +1 -203
- package/src/__tests__/setup-no-llm.test.ts +68 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +24 -30
- package/src/__tests__/step-ask.test.ts +670 -0
- package/src/__tests__/step-show-json.test.ts +1 -0
- package/src/__tests__/step-timing.test.ts +2 -0
- package/src/__tests__/store-global-cas.test.ts +2 -2
- package/src/__tests__/store-unified-threads.test.ts +9 -9
- package/src/__tests__/thread-cancel-status.test.ts +6 -6
- package/src/__tests__/thread-list-filters.test.ts +434 -8
- package/src/__tests__/thread-poke.test.ts +545 -0
- package/src/__tests__/thread-resume.test.ts +10 -14
- package/src/__tests__/thread-show-status.test.ts +17 -29
- package/src/__tests__/thread-suspend-step.test.ts +8 -14
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread.test.ts +4 -4
- package/src/__tests__/validate-semantic.test.ts +59 -31
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +39 -21
- package/src/__tests__/workflow-show-resolution.test.ts +285 -0
- package/src/__tests__/workflow-validate.test.ts +806 -0
- package/src/background/background.ts +88 -6
- package/src/background/index.ts +2 -0
- package/src/background/types.ts +1 -0
- package/src/cli.ts +97 -47
- package/src/commands/config.ts +7 -35
- package/src/commands/prompt.ts +15 -2
- package/src/commands/setup.ts +29 -357
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +463 -169
- package/src/commands/workflow.ts +159 -4
- package/src/moderator/__tests__/evaluate.test.ts +34 -17
- package/src/moderator/evaluate.ts +5 -17
- package/src/moderator/index.ts +1 -6
- package/src/moderator/types.ts +6 -14
- package/src/schemas.ts +13 -3
- package/src/store.ts +86 -20
- package/src/validate-semantic.ts +109 -78
- package/src/validate.ts +27 -0
- package/dist/__tests__/setup-validate.test.d.ts +0 -2
- package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
- package/dist/__tests__/setup-validate.test.js +0 -108
- package/dist/__tests__/setup-validate.test.js.map +0 -1
- package/src/__tests__/setup-validate.test.ts +0 -148
- /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
|
@@ -230,11 +230,11 @@ describe("Global CAS directory", () => {
|
|
|
230
230
|
const { createThreadIndexEntry } = await import("@united-workforce/protocol");
|
|
231
231
|
|
|
232
232
|
setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash));
|
|
233
|
-
completeThread(uwf.varStore, threadId, "
|
|
233
|
+
completeThread(uwf.varStore, threadId, "end");
|
|
234
234
|
|
|
235
235
|
const entry = getThread(uwf.varStore, threadId);
|
|
236
236
|
expect(entry?.head).toBe(headHash);
|
|
237
|
-
expect(entry?.status).toBe("
|
|
237
|
+
expect(entry?.status).toBe("end");
|
|
238
238
|
|
|
239
239
|
const { access } = await import("node:fs/promises");
|
|
240
240
|
await access(join(globalCasDir, "vars"));
|
|
@@ -46,7 +46,7 @@ describe("unified thread storage", () => {
|
|
|
46
46
|
|
|
47
47
|
setThread(uwf.varStore, threadId2, {
|
|
48
48
|
head: head2,
|
|
49
|
-
status: "
|
|
49
|
+
status: "end",
|
|
50
50
|
suspendedRole: null,
|
|
51
51
|
suspendMessage: null,
|
|
52
52
|
completedAt: Date.now(),
|
|
@@ -110,7 +110,7 @@ describe("unified thread storage", () => {
|
|
|
110
110
|
|
|
111
111
|
setThread(uwf.varStore, threadId2, {
|
|
112
112
|
head: head2,
|
|
113
|
-
status: "
|
|
113
|
+
status: "end",
|
|
114
114
|
suspendedRole: null,
|
|
115
115
|
suspendMessage: null,
|
|
116
116
|
completedAt: Date.now(),
|
|
@@ -145,11 +145,11 @@ describe("unified thread storage", () => {
|
|
|
145
145
|
completedAt: null,
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
completeThread(uwf.varStore, threadId, "
|
|
148
|
+
completeThread(uwf.varStore, threadId, "end");
|
|
149
149
|
|
|
150
150
|
const entry = getThread(uwf.varStore, threadId);
|
|
151
151
|
expect(entry).not.toBeNull();
|
|
152
|
-
expect(entry?.status).toBe("
|
|
152
|
+
expect(entry?.status).toBe("end");
|
|
153
153
|
expect(entry?.completedAt).toBeDefined();
|
|
154
154
|
expect(entry?.completedAt).toBeGreaterThan(0);
|
|
155
155
|
});
|
|
@@ -191,11 +191,11 @@ describe("unified thread storage", () => {
|
|
|
191
191
|
completedAt: null,
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
completeThread(uwf.varStore, threadId, "
|
|
194
|
+
completeThread(uwf.varStore, threadId, "end");
|
|
195
195
|
|
|
196
196
|
const entry = getThread(uwf.varStore, threadId);
|
|
197
197
|
expect(entry).not.toBeNull();
|
|
198
|
-
expect(entry?.status).toBe("
|
|
198
|
+
expect(entry?.status).toBe("end");
|
|
199
199
|
expect(entry?.suspendedRole).toBeNull();
|
|
200
200
|
expect(entry?.suspendMessage).toBeNull();
|
|
201
201
|
});
|
|
@@ -206,7 +206,7 @@ describe("unified thread storage", () => {
|
|
|
206
206
|
const threadId = "01JTEST000000000000NOEXIST" as ThreadId;
|
|
207
207
|
|
|
208
208
|
// Should not throw
|
|
209
|
-
completeThread(uwf.varStore, threadId, "
|
|
209
|
+
completeThread(uwf.varStore, threadId, "end");
|
|
210
210
|
|
|
211
211
|
const entry = getThread(uwf.varStore, threadId);
|
|
212
212
|
expect(entry).toBeNull();
|
|
@@ -221,7 +221,7 @@ describe("unified thread storage", () => {
|
|
|
221
221
|
|
|
222
222
|
setThread(uwf.varStore, threadId, {
|
|
223
223
|
head,
|
|
224
|
-
status: "
|
|
224
|
+
status: "end",
|
|
225
225
|
suspendedRole: null,
|
|
226
226
|
suspendMessage: null,
|
|
227
227
|
completedAt: now,
|
|
@@ -229,7 +229,7 @@ describe("unified thread storage", () => {
|
|
|
229
229
|
|
|
230
230
|
const entry = getThread(uwf.varStore, threadId);
|
|
231
231
|
expect(entry).not.toBeNull();
|
|
232
|
-
expect(entry?.status).toBe("
|
|
232
|
+
expect(entry?.status).toBe("end");
|
|
233
233
|
expect(entry?.completedAt).toBe(now);
|
|
234
234
|
});
|
|
235
235
|
});
|
|
@@ -61,11 +61,11 @@ describe("thread cancel status", () => {
|
|
|
61
61
|
completedAt: null,
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
completeThread(uwf.varStore, threadId, "
|
|
64
|
+
completeThread(uwf.varStore, threadId, "end");
|
|
65
65
|
|
|
66
66
|
const entry = getThread(uwf.varStore, threadId);
|
|
67
67
|
expect(entry).not.toBeNull();
|
|
68
|
-
expect(entry?.status).toBe("
|
|
68
|
+
expect(entry?.status).toBe("end");
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
test("loadHistoryThreads returns completed and cancelled", async () => {
|
|
@@ -82,7 +82,7 @@ describe("thread cancel status", () => {
|
|
|
82
82
|
suspendMessage: null,
|
|
83
83
|
completedAt: null,
|
|
84
84
|
});
|
|
85
|
-
completeThread(uwf.varStore, threadId1, "
|
|
85
|
+
completeThread(uwf.varStore, threadId1, "end");
|
|
86
86
|
|
|
87
87
|
const threadId2 = "01JTEST000000000000CANCEL5" as ThreadId;
|
|
88
88
|
setThread(uwf.varStore, threadId2, {
|
|
@@ -99,7 +99,7 @@ describe("thread cancel status", () => {
|
|
|
99
99
|
const statuses = Object.values(history)
|
|
100
100
|
.map((entry) => entry.status)
|
|
101
101
|
.sort();
|
|
102
|
-
expect(statuses).toEqual(["cancelled", "
|
|
102
|
+
expect(statuses).toEqual(["cancelled", "end"]);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
test("mixed completed and cancelled entries preserve distinct statuses", async () => {
|
|
@@ -116,7 +116,7 @@ describe("thread cancel status", () => {
|
|
|
116
116
|
suspendMessage: null,
|
|
117
117
|
completedAt: null,
|
|
118
118
|
});
|
|
119
|
-
completeThread(uwf.varStore, threadId1, "
|
|
119
|
+
completeThread(uwf.varStore, threadId1, "end");
|
|
120
120
|
|
|
121
121
|
const threadId2 = "01JTEST000000000000CANCEL7" as ThreadId;
|
|
122
122
|
setThread(uwf.varStore, threadId2, {
|
|
@@ -133,6 +133,6 @@ describe("thread cancel status", () => {
|
|
|
133
133
|
const statuses = Object.values(history)
|
|
134
134
|
.map((entry) => entry.status)
|
|
135
135
|
.sort();
|
|
136
|
-
expect(statuses).toEqual(["cancelled", "
|
|
136
|
+
expect(statuses).toEqual(["cancelled", "end"]);
|
|
137
137
|
});
|
|
138
138
|
});
|
|
@@ -5,7 +5,7 @@ import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
|
5
5
|
import { createThreadIndexEntry } from "@united-workforce/protocol";
|
|
6
6
|
import { extractUlidTimestamp, generateUlid } from "@united-workforce/util";
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
8
|
-
import { createMarker, deleteMarker } from "../background/index.js";
|
|
8
|
+
import { createMarker, deleteMarker, getProcessStartTime } from "../background/index.js";
|
|
9
9
|
import { cmdThreadList } from "../commands/thread.js";
|
|
10
10
|
import { parseTimeInput } from "../commands/thread-time-parser.js";
|
|
11
11
|
import type { UwfStore } from "../store.js";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
completeThread as completeThreadInStore,
|
|
14
14
|
createUwfStore,
|
|
15
15
|
loadAllThreads,
|
|
16
|
+
saveWorkflowRegistry,
|
|
16
17
|
setThread,
|
|
17
18
|
} from "../store.js";
|
|
18
19
|
|
|
@@ -66,6 +67,7 @@ async function markThreadRunning(storageRoot: string, threadId: ThreadId, workfl
|
|
|
66
67
|
workflow,
|
|
67
68
|
pid: process.pid, // Use current process PID so isPidAlive returns true
|
|
68
69
|
startedAt: Date.now(),
|
|
70
|
+
processStartTime: getProcessStartTime(process.pid),
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -76,7 +78,7 @@ async function completeThread(
|
|
|
76
78
|
_headHash: CasRef,
|
|
77
79
|
) {
|
|
78
80
|
const uwfIdx = await createUwfStore(storageRoot);
|
|
79
|
-
completeThreadInStore(uwfIdx.varStore, threadId, "
|
|
81
|
+
completeThreadInStore(uwfIdx.varStore, threadId, "end");
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
// ── test setup ────────────────────────────────────────────────────────────────
|
|
@@ -135,7 +137,7 @@ describe("cmdThreadList status filter", () => {
|
|
|
135
137
|
if (thread3Head === undefined) throw new Error("thread3 head not found");
|
|
136
138
|
await completeThread(tmpDir, thread3, workflowHash, thread3Head);
|
|
137
139
|
|
|
138
|
-
const result = await cmdThreadList(tmpDir, ["idle", "
|
|
140
|
+
const result = await cmdThreadList(tmpDir, ["idle", "end"], null, null, null, null);
|
|
139
141
|
|
|
140
142
|
// Clean up marker
|
|
141
143
|
await deleteMarker(tmpDir, thread2);
|
|
@@ -160,14 +162,14 @@ describe("cmdThreadList status filter", () => {
|
|
|
160
162
|
if (thread3Head === undefined) throw new Error("thread3 head not found");
|
|
161
163
|
await completeThread(tmpDir, thread3, workflowHash, thread3Head);
|
|
162
164
|
|
|
163
|
-
const result = await cmdThreadList(tmpDir, ["
|
|
165
|
+
const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null);
|
|
164
166
|
|
|
165
167
|
expect(result).toHaveLength(1);
|
|
166
168
|
expect(result[0]?.thread).toBe(thread3);
|
|
167
|
-
expect(result[0]?.status).toBe("
|
|
169
|
+
expect(result[0]?.status).toBe("end");
|
|
168
170
|
});
|
|
169
171
|
|
|
170
|
-
test("should return
|
|
172
|
+
test("should return only active threads when no filter and no --all", async () => {
|
|
171
173
|
const uwf = await makeUwfStore(tmpDir);
|
|
172
174
|
const workflowHash = await createTestWorkflow(uwf);
|
|
173
175
|
|
|
@@ -185,8 +187,290 @@ describe("cmdThreadList status filter", () => {
|
|
|
185
187
|
|
|
186
188
|
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
187
189
|
|
|
190
|
+
// Default behavior (issue #147): only active threads (idle + running)
|
|
191
|
+
expect(result).toHaveLength(2);
|
|
192
|
+
expect(result.map((r) => r.thread).sort()).toEqual([thread1, thread2].sort());
|
|
193
|
+
|
|
194
|
+
// Clean up marker
|
|
195
|
+
await deleteMarker(tmpDir, thread2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("should return all threads when --all (showAll=true)", async () => {
|
|
199
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
200
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
201
|
+
|
|
202
|
+
const thread1 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
203
|
+
const thread2 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
204
|
+
const thread3 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
205
|
+
|
|
206
|
+
await markThreadRunning(tmpDir, thread2, workflowHash);
|
|
207
|
+
|
|
208
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
209
|
+
const index = loadAllThreads(uwfIdx.varStore);
|
|
210
|
+
const thread3Head = index[thread3]!.head;
|
|
211
|
+
if (thread3Head === undefined) throw new Error("thread3 head not found");
|
|
212
|
+
await completeThread(tmpDir, thread3, workflowHash, thread3Head);
|
|
213
|
+
|
|
214
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
215
|
+
|
|
188
216
|
expect(result).toHaveLength(3);
|
|
189
217
|
expect(result.map((r) => r.thread).sort()).toEqual([thread1, thread2, thread3].sort());
|
|
218
|
+
|
|
219
|
+
// Clean up marker
|
|
220
|
+
await deleteMarker(tmpDir, thread2);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── default behavior tests (issue #147) ───────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe("cmdThreadList default behavior (issue #147)", () => {
|
|
227
|
+
test("default returns only idle + running threads", async () => {
|
|
228
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
229
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
230
|
+
|
|
231
|
+
const threadA = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
|
|
232
|
+
const threadB = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
233
|
+
const threadC = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
234
|
+
const threadD = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
235
|
+
|
|
236
|
+
await markThreadRunning(tmpDir, threadB, workflowHash);
|
|
237
|
+
|
|
238
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
239
|
+
const index = loadAllThreads(uwfIdx.varStore);
|
|
240
|
+
const threadCHead = index[threadC]!.head;
|
|
241
|
+
if (threadCHead === undefined) throw new Error("threadC head not found");
|
|
242
|
+
await completeThread(tmpDir, threadC, workflowHash, threadCHead);
|
|
243
|
+
|
|
244
|
+
// Cancel threadD
|
|
245
|
+
const threadDHead = index[threadD]!.head;
|
|
246
|
+
if (threadDHead === undefined) throw new Error("threadD head not found");
|
|
247
|
+
const uwfCancel = await createUwfStore(tmpDir);
|
|
248
|
+
completeThreadInStore(uwfCancel.varStore, threadD, "cancelled");
|
|
249
|
+
|
|
250
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
251
|
+
|
|
252
|
+
expect(result).toHaveLength(2);
|
|
253
|
+
expect(result.map((r) => r.thread).sort()).toEqual([threadA, threadB].sort());
|
|
254
|
+
|
|
255
|
+
await deleteMarker(tmpDir, threadB);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("default excludes completed threads", async () => {
|
|
259
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
260
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
261
|
+
|
|
262
|
+
const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 6000);
|
|
263
|
+
const completedThreads: ThreadId[] = [];
|
|
264
|
+
for (let i = 0; i < 5; i++) {
|
|
265
|
+
const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (5 - i) * 1000);
|
|
266
|
+
completedThreads.push(t);
|
|
267
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
268
|
+
const index = loadAllThreads(uwfIdx.varStore);
|
|
269
|
+
const head = index[t]!.head;
|
|
270
|
+
if (head === undefined) throw new Error("head not found");
|
|
271
|
+
await completeThread(tmpDir, t, workflowHash, head);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
275
|
+
|
|
276
|
+
expect(result).toHaveLength(1);
|
|
277
|
+
expect(result[0]?.thread).toBe(idleThread);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("default excludes cancelled threads", async () => {
|
|
281
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
282
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
283
|
+
|
|
284
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
|
|
285
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
286
|
+
|
|
287
|
+
const cancelled: ThreadId[] = [];
|
|
288
|
+
for (let i = 0; i < 3; i++) {
|
|
289
|
+
const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (3 - i) * 1000);
|
|
290
|
+
cancelled.push(t);
|
|
291
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
292
|
+
completeThreadInStore(uwfIdx.varStore, t, "cancelled");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
296
|
+
|
|
297
|
+
expect(result).toHaveLength(1);
|
|
298
|
+
expect(result[0]?.thread).toBe(runningThread);
|
|
299
|
+
|
|
300
|
+
await deleteMarker(tmpDir, runningThread);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("--all (showAll=true) returns every status", async () => {
|
|
304
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
305
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
306
|
+
|
|
307
|
+
const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
|
|
308
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
309
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
310
|
+
|
|
311
|
+
const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
312
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
313
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
314
|
+
const ch = idx[completedThread]!.head;
|
|
315
|
+
if (ch === undefined) throw new Error("completedThread head not found");
|
|
316
|
+
await completeThread(tmpDir, completedThread, workflowHash, ch);
|
|
317
|
+
|
|
318
|
+
const cancelledThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
319
|
+
completeThreadInStore(uwfIdx.varStore, cancelledThread, "cancelled");
|
|
320
|
+
|
|
321
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
322
|
+
|
|
323
|
+
expect(result).toHaveLength(4);
|
|
324
|
+
expect(result.map((r) => r.thread).sort()).toEqual(
|
|
325
|
+
[idleThread, runningThread, completedThread, cancelledThread].sort(),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
await deleteMarker(tmpDir, runningThread);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("explicit --status overrides default (still returns just the filtered statuses)", async () => {
|
|
332
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
333
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
334
|
+
|
|
335
|
+
const _idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
336
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
337
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
338
|
+
|
|
339
|
+
const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
340
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
341
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
342
|
+
const ch = idx[completedThread]!.head;
|
|
343
|
+
if (ch === undefined) throw new Error("completedThread head not found");
|
|
344
|
+
await completeThread(tmpDir, completedThread, workflowHash, ch);
|
|
345
|
+
|
|
346
|
+
const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null);
|
|
347
|
+
|
|
348
|
+
expect(result).toHaveLength(1);
|
|
349
|
+
expect(result[0]?.thread).toBe(completedThread);
|
|
350
|
+
expect(result[0]?.status).toBe("end");
|
|
351
|
+
|
|
352
|
+
await deleteMarker(tmpDir, runningThread);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("--status active keeps working", async () => {
|
|
356
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
357
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
358
|
+
|
|
359
|
+
const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
360
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
361
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
362
|
+
|
|
363
|
+
const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
364
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
365
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
366
|
+
const ch = idx[completedThread]!.head;
|
|
367
|
+
if (ch === undefined) throw new Error("completedThread head not found");
|
|
368
|
+
await completeThread(tmpDir, completedThread, workflowHash, ch);
|
|
369
|
+
|
|
370
|
+
const result = await cmdThreadList(tmpDir, ["idle", "running"], null, null, null, null);
|
|
371
|
+
|
|
372
|
+
expect(result).toHaveLength(2);
|
|
373
|
+
expect(result.map((r) => r.thread).sort()).toEqual([idleThread, runningThread].sort());
|
|
374
|
+
|
|
375
|
+
await deleteMarker(tmpDir, runningThread);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("--status + --all — explicit status wins", async () => {
|
|
379
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
380
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
381
|
+
|
|
382
|
+
const _idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
383
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
384
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
385
|
+
|
|
386
|
+
const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
387
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
388
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
389
|
+
const ch = idx[completedThread]!.head;
|
|
390
|
+
if (ch === undefined) throw new Error("completedThread head not found");
|
|
391
|
+
await completeThread(tmpDir, completedThread, workflowHash, ch);
|
|
392
|
+
|
|
393
|
+
const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null, true);
|
|
394
|
+
|
|
395
|
+
expect(result).toHaveLength(1);
|
|
396
|
+
expect(result[0]?.thread).toBe(completedThread);
|
|
397
|
+
|
|
398
|
+
await deleteMarker(tmpDir, runningThread);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("default returns empty when no threads", async () => {
|
|
402
|
+
await makeUwfStore(tmpDir);
|
|
403
|
+
|
|
404
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
405
|
+
|
|
406
|
+
expect(result).toHaveLength(0);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("default + time range filter composes correctly", async () => {
|
|
410
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
411
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
412
|
+
|
|
413
|
+
const ts1 = Date.UTC(2026, 4, 20, 0, 0, 0);
|
|
414
|
+
const ts2 = Date.UTC(2026, 4, 21, 0, 0, 0);
|
|
415
|
+
const ts3 = Date.UTC(2026, 4, 22, 0, 0, 0);
|
|
416
|
+
const ts4 = Date.UTC(2026, 4, 23, 0, 0, 0);
|
|
417
|
+
const ts5 = Date.UTC(2026, 4, 24, 0, 0, 0);
|
|
418
|
+
|
|
419
|
+
const _t1 = await createTestThread(uwf, tmpDir, workflowHash, ts1);
|
|
420
|
+
const t2 = await createTestThread(uwf, tmpDir, workflowHash, ts2);
|
|
421
|
+
const t3 = await createTestThread(uwf, tmpDir, workflowHash, ts3);
|
|
422
|
+
const t4 = await createTestThread(uwf, tmpDir, workflowHash, ts4);
|
|
423
|
+
const _t5 = await createTestThread(uwf, tmpDir, workflowHash, ts5);
|
|
424
|
+
|
|
425
|
+
// Mark t3 running
|
|
426
|
+
await markThreadRunning(tmpDir, t3, workflowHash);
|
|
427
|
+
|
|
428
|
+
// Complete t4 (should be excluded by default)
|
|
429
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
430
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
431
|
+
const t4head = idx[t4]!.head;
|
|
432
|
+
if (t4head === undefined) throw new Error("t4 head not found");
|
|
433
|
+
await completeThread(tmpDir, t4, workflowHash, t4head);
|
|
434
|
+
|
|
435
|
+
// afterMs in middle of range to exclude _t1
|
|
436
|
+
const afterMs = Date.UTC(2026, 4, 20, 12, 0, 0);
|
|
437
|
+
const result = await cmdThreadList(tmpDir, null, afterMs, null, null, null);
|
|
438
|
+
|
|
439
|
+
// Expected: t2 (idle), t3 (running), _t5 (idle); excludes t4 (completed) and _t1 (filtered by time)
|
|
440
|
+
expect(result).toHaveLength(3);
|
|
441
|
+
const ids = result.map((r) => r.thread).sort();
|
|
442
|
+
expect(ids).toEqual([t2, t3, _t5].sort());
|
|
443
|
+
|
|
444
|
+
await deleteMarker(tmpDir, t3);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("default + pagination composes correctly", async () => {
|
|
448
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
449
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
450
|
+
|
|
451
|
+
// Create 10 idle threads + 5 completed threads
|
|
452
|
+
const idleThreads: ThreadId[] = [];
|
|
453
|
+
for (let i = 0; i < 10; i++) {
|
|
454
|
+
idleThreads.push(
|
|
455
|
+
await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (15 - i) * 1000),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
for (let i = 0; i < 5; i++) {
|
|
459
|
+
const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (5 - i) * 1000);
|
|
460
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
461
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
462
|
+
const head = idx[t]!.head;
|
|
463
|
+
if (head === undefined) throw new Error("head not found");
|
|
464
|
+
await completeThread(tmpDir, t, workflowHash, head);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const result = await cmdThreadList(tmpDir, null, null, null, 2, 3);
|
|
468
|
+
|
|
469
|
+
expect(result).toHaveLength(3);
|
|
470
|
+
// All results should be idle (default excludes completed)
|
|
471
|
+
for (const r of result) {
|
|
472
|
+
expect(r.status).toBe("idle");
|
|
473
|
+
}
|
|
190
474
|
});
|
|
191
475
|
});
|
|
192
476
|
|
|
@@ -382,11 +666,11 @@ describe("combined filters", () => {
|
|
|
382
666
|
await completeThread(tmpDir, thread, workflowHash, headHash);
|
|
383
667
|
}
|
|
384
668
|
|
|
385
|
-
const result = await cmdThreadList(tmpDir, ["
|
|
669
|
+
const result = await cmdThreadList(tmpDir, ["end"], null, null, 3, 5);
|
|
386
670
|
|
|
387
671
|
expect(result).toHaveLength(5);
|
|
388
672
|
for (const r of result) {
|
|
389
|
-
expect(r.status).toBe("
|
|
673
|
+
expect(r.status).toBe("end");
|
|
390
674
|
}
|
|
391
675
|
});
|
|
392
676
|
|
|
@@ -570,3 +854,145 @@ describe("ISO date parsing", () => {
|
|
|
570
854
|
expect(() => parseTimeInput("invalid", nowMs)).toThrow();
|
|
571
855
|
});
|
|
572
856
|
});
|
|
857
|
+
|
|
858
|
+
// ── corrupt thread resilience (#250) ──────────────────────────────────────────
|
|
859
|
+
|
|
860
|
+
describe("corrupt thread resilience (#250)", () => {
|
|
861
|
+
test("thread list returns corrupt entry when CAS node is missing", async () => {
|
|
862
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
863
|
+
|
|
864
|
+
// Create a valid thread
|
|
865
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
866
|
+
const now = Date.now();
|
|
867
|
+
const _validId = await createTestThread(uwf, tmpDir, workflowHash, now);
|
|
868
|
+
|
|
869
|
+
// Create another thread with a unique start node, then delete its workflow CAS to corrupt it
|
|
870
|
+
const corruptThreadId = generateUlid(now + 1000) as ThreadId;
|
|
871
|
+
const startPayload = {
|
|
872
|
+
workflow: workflowHash,
|
|
873
|
+
prompt: "corrupt thread prompt — unique to avoid CAS hash collision",
|
|
874
|
+
cwd: tmpDir,
|
|
875
|
+
};
|
|
876
|
+
const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
|
|
877
|
+
setThread(uwf.varStore, corruptThreadId, createThreadIndexEntry(headHash));
|
|
878
|
+
|
|
879
|
+
// Delete the workflow CAS node — start node still exists but workflow ref dangles
|
|
880
|
+
uwf.store.cas.delete(workflowHash);
|
|
881
|
+
|
|
882
|
+
// thread list should NOT throw — it should return both threads
|
|
883
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
884
|
+
|
|
885
|
+
// Both threads should appear (the valid one is now also corrupt since workflow is shared)
|
|
886
|
+
// In practice: both become corrupt because they share the same workflow CAS node
|
|
887
|
+
// This matches the real scenario from issue #250 — gc deleted a shared node
|
|
888
|
+
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
889
|
+
const corruptItems = result.filter((r) => r.status === "corrupt");
|
|
890
|
+
expect(corruptItems.length).toBeGreaterThanOrEqual(1);
|
|
891
|
+
for (const item of corruptItems) {
|
|
892
|
+
expect(item.statusDisplay).toBe("corrupt");
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("corrupt threads appear in default filter (without --all)", async () => {
|
|
897
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
898
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
899
|
+
|
|
900
|
+
// Create a thread then corrupt it
|
|
901
|
+
const corruptId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
|
|
902
|
+
const corruptEntry = loadAllThreads(uwf.varStore)[corruptId];
|
|
903
|
+
uwf.store.cas.delete(corruptEntry.head);
|
|
904
|
+
|
|
905
|
+
// Default filter (no --all, no --status) should include corrupt
|
|
906
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
907
|
+
expect(result).toHaveLength(1);
|
|
908
|
+
expect(result[0].status).toBe("corrupt");
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// ── orphan thread detection (#286) ────────────────────────────────────────────
|
|
913
|
+
|
|
914
|
+
describe("orphan thread detection (#286)", () => {
|
|
915
|
+
test("thread list includes workflowName when workflow is in registry", async () => {
|
|
916
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
917
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
918
|
+
|
|
919
|
+
// Register the workflow in registry
|
|
920
|
+
saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
|
|
921
|
+
|
|
922
|
+
const threadId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
|
|
923
|
+
|
|
924
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
925
|
+
expect(result).toHaveLength(1);
|
|
926
|
+
expect(result[0].thread).toBe(threadId);
|
|
927
|
+
expect(result[0].workflowName).toBe("test-workflow");
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("thread list returns workflowName: null for orphaned threads", async () => {
|
|
931
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
932
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
933
|
+
|
|
934
|
+
// Do NOT register the workflow — thread is orphaned
|
|
935
|
+
const threadId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
|
|
936
|
+
|
|
937
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
938
|
+
expect(result).toHaveLength(1);
|
|
939
|
+
expect(result[0].thread).toBe(threadId);
|
|
940
|
+
expect(result[0].workflowName).toBeNull();
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test("mixed registered and orphaned threads in the same list", async () => {
|
|
944
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
945
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
946
|
+
|
|
947
|
+
// Register the workflow
|
|
948
|
+
saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
|
|
949
|
+
|
|
950
|
+
// Create a thread using the registered workflow
|
|
951
|
+
const now = Date.now();
|
|
952
|
+
const registeredId = await createTestThread(uwf, tmpDir, workflowHash, now);
|
|
953
|
+
|
|
954
|
+
// Create a second workflow (different hash), not registered
|
|
955
|
+
const orphanWorkflowPayload = {
|
|
956
|
+
name: "orphan-workflow",
|
|
957
|
+
roles: {
|
|
958
|
+
role1: {
|
|
959
|
+
goal: "orphan goal",
|
|
960
|
+
outputSchema: { type: "object" as const, properties: {} },
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
graph: { start: "role1" },
|
|
964
|
+
conditions: {},
|
|
965
|
+
};
|
|
966
|
+
const orphanHash = await uwf.store.cas.put(uwf.schemas.workflow, orphanWorkflowPayload);
|
|
967
|
+
const orphanId = await createTestThread(uwf, tmpDir, orphanHash, now + 1000);
|
|
968
|
+
|
|
969
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
970
|
+
expect(result).toHaveLength(2);
|
|
971
|
+
|
|
972
|
+
// Sorted newest first, so orphan (later timestamp) comes first
|
|
973
|
+
const orphanItem = result.find((r) => r.thread === orphanId);
|
|
974
|
+
const registeredItem = result.find((r) => r.thread === registeredId);
|
|
975
|
+
|
|
976
|
+
expect(orphanItem).toBeDefined();
|
|
977
|
+
expect(orphanItem!.workflowName).toBeNull();
|
|
978
|
+
|
|
979
|
+
expect(registeredItem).toBeDefined();
|
|
980
|
+
expect(registeredItem!.workflowName).toBe("test-workflow");
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
test("corrupt threads have workflowName: null", async () => {
|
|
984
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
985
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
986
|
+
saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
|
|
987
|
+
|
|
988
|
+
// Create a thread then corrupt it by deleting its head CAS node
|
|
989
|
+
const corruptId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
|
|
990
|
+
const corruptEntry = loadAllThreads(uwf.varStore)[corruptId];
|
|
991
|
+
uwf.store.cas.delete(corruptEntry.head);
|
|
992
|
+
|
|
993
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
994
|
+
expect(result).toHaveLength(1);
|
|
995
|
+
expect(result[0].status).toBe("corrupt");
|
|
996
|
+
expect(result[0].workflowName).toBeNull();
|
|
997
|
+
});
|
|
998
|
+
});
|