@united-workforce/cli 0.4.0 → 0.5.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 +30 -3
- package/dist/.build-fingerprint +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +16 -6
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/concurrency.test.d.ts +2 -0
- package/dist/__tests__/concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/concurrency.test.js +196 -0
- package/dist/__tests__/concurrency.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +23 -7
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/format-text-default.test.d.ts +2 -0
- package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-default.test.js +43 -0
- package/dist/__tests__/format-text-default.test.js.map +1 -0
- package/dist/__tests__/format-text-registry.test.d.ts +2 -0
- package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-registry.test.js +158 -0
- package/dist/__tests__/format-text-registry.test.js.map +1 -0
- package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/log-text-renderer.test.js +265 -0
- package/dist/__tests__/log-text-renderer.test.js.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +9 -7
- package/dist/__tests__/pid-recycling.test.js.map +1 -1
- package/dist/__tests__/prompt.test.js +46 -4
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +8 -0
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/solve-issue-tea-worktree.test.js +3 -1
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -1
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +19 -17
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +19 -13
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +10 -8
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
- package/dist/__tests__/thread-poke.test.js +11 -1
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +11 -1
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +5 -2
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-test-helpers.d.ts +7 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
- package/dist/__tests__/thread-test-helpers.js +13 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -1
- package/dist/__tests__/thread.test.js +11 -9
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +56 -2
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.js +10 -7
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -1
- package/dist/__tests__/workflow-resolution.test.js +10 -7
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.js +10 -7
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-validate.test.js +75 -55
- package/dist/__tests__/workflow-validate.test.js.map +1 -1
- package/dist/__tests__/write-envelope.test.d.ts +2 -0
- package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
- package/dist/__tests__/write-envelope.test.js +201 -0
- package/dist/__tests__/write-envelope.test.js.map +1 -0
- package/dist/cli.js +58 -35
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +12 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +42 -29
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +9 -4
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +51 -7
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +44 -2
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +1 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +2 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/concurrency/concurrency.d.ts +34 -0
- package/dist/concurrency/concurrency.d.ts.map +1 -0
- package/dist/concurrency/concurrency.js +216 -0
- package/dist/concurrency/concurrency.js.map +1 -0
- package/dist/concurrency/index.d.ts +3 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +2 -0
- package/dist/concurrency/index.js.map +1 -0
- package/dist/concurrency/types.d.ts +19 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +2 -0
- package/dist/concurrency/types.js.map +1 -0
- package/dist/format.d.ts +69 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +198 -1
- package/dist/format.js.map +1 -1
- package/dist/output-mappers.d.ts +122 -0
- package/dist/output-mappers.d.ts.map +1 -0
- package/dist/output-mappers.js +134 -0
- package/dist/output-mappers.js.map +1 -0
- package/dist/schemas.d.ts +4 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +31 -4
- package/dist/schemas.js.map +1 -1
- package/dist/text-renderers.d.ts +30 -0
- package/dist/text-renderers.d.ts.map +1 -0
- package/dist/text-renderers.js +251 -0
- package/dist/text-renderers.js.map +1 -0
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +28 -11
- package/dist/validate-semantic.js.map +1 -1
- package/examples/brainstorm.yaml +130 -0
- package/examples/debate.yaml +169 -0
- package/examples/socratic-questioning.yaml +112 -0
- package/package.json +5 -4
- package/src/__tests__/adapter-json-roundtrip.test.ts +15 -6
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/e2e-mock-agent.test.ts +45 -7
- package/src/__tests__/format-text-default.test.ts +49 -0
- package/src/__tests__/format-text-registry.test.ts +173 -0
- package/src/__tests__/log-text-renderer.test.ts +294 -0
- package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
- package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
- package/src/__tests__/pid-recycling.test.ts +9 -8
- package/src/__tests__/prompt.test.ts +48 -4
- package/src/__tests__/resolve-head-hash.test.ts +7 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +3 -1
- package/src/__tests__/step-ask.test.ts +8 -1
- package/src/__tests__/store-unified-threads.test.ts +21 -18
- package/src/__tests__/thread-cancel-status.test.ts +21 -14
- package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
- package/src/__tests__/thread-list-filters.test.ts +9 -9
- package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
- package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
- package/src/__tests__/thread-poke.test.ts +10 -1
- package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
- package/src/__tests__/thread-resume.test.ts +10 -1
- package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
- package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
- package/src/__tests__/thread-suspend-step.test.ts +5 -2
- package/src/__tests__/thread-test-helpers.ts +15 -1
- package/src/__tests__/thread.test.ts +10 -10
- package/src/__tests__/validate-semantic.test.ts +59 -2
- package/src/__tests__/workflow-list-recursive.test.ts +9 -9
- package/src/__tests__/workflow-resolution.test.ts +9 -8
- package/src/__tests__/workflow-show-resolution.test.ts +9 -8
- package/src/__tests__/workflow-validate.test.ts +78 -56
- package/src/__tests__/write-envelope.test.ts +257 -0
- package/src/cli.ts +92 -35
- package/src/commands/config.ts +11 -0
- package/src/commands/prompt.ts +42 -29
- package/src/commands/setup.ts +57 -7
- package/src/commands/thread.ts +48 -2
- package/src/commands/workflow.ts +3 -7
- package/src/concurrency/concurrency.ts +245 -0
- package/src/concurrency/index.ts +10 -0
- package/src/concurrency/types.ts +19 -0
- package/src/format.ts +282 -2
- package/src/output-mappers.ts +254 -0
- package/src/schemas.ts +39 -3
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +33 -12
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
5
|
-
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
6
6
|
import {
|
|
7
7
|
completeThread,
|
|
8
|
-
createUwfStore,
|
|
8
|
+
type createUwfStore,
|
|
9
9
|
getThread,
|
|
10
10
|
loadActiveThreads,
|
|
11
11
|
loadHistoryThreads,
|
|
12
12
|
setThread,
|
|
13
13
|
} from "../store.js";
|
|
14
|
-
|
|
15
|
-
async function makeUwfStore(storageRoot: string) {
|
|
16
|
-
const casDir = join(storageRoot, "cas");
|
|
17
|
-
await mkdir(casDir, { recursive: true });
|
|
18
|
-
process.env.OCAS_HOME = casDir;
|
|
19
|
-
return createUwfStore(storageRoot);
|
|
20
|
-
}
|
|
14
|
+
import { makeUwfStore } from "./thread-test-helpers.js";
|
|
21
15
|
|
|
22
16
|
async function seedThreadHead(
|
|
23
17
|
uwf: Awaited<ReturnType<typeof createUwfStore>>,
|
|
@@ -26,9 +20,25 @@ async function seedThreadHead(
|
|
|
26
20
|
return (await uwf.store.cas.put(uwf.schemas.text, label)) as CasRef;
|
|
27
21
|
}
|
|
28
22
|
|
|
23
|
+
let tmpDir: string;
|
|
24
|
+
let savedOcasHome: string | undefined;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
28
|
+
tmpDir = await mkdtemp(join(tmpdir(), "uwf-store-test-"));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
if (savedOcasHome === undefined) {
|
|
33
|
+
delete process.env.OCAS_HOME;
|
|
34
|
+
} else {
|
|
35
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
36
|
+
}
|
|
37
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
29
40
|
describe("unified thread storage", () => {
|
|
30
41
|
test("loadActiveThreads excludes completed threads", async () => {
|
|
31
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-active-test-"));
|
|
32
42
|
const uwf = await makeUwfStore(tmpDir);
|
|
33
43
|
|
|
34
44
|
const threadId1 = "01JTEST000000000000ACTIVE1" as ThreadId;
|
|
@@ -59,7 +69,6 @@ describe("unified thread storage", () => {
|
|
|
59
69
|
});
|
|
60
70
|
|
|
61
71
|
test("loadActiveThreads excludes cancelled threads", async () => {
|
|
62
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-active-test-"));
|
|
63
72
|
const uwf = await makeUwfStore(tmpDir);
|
|
64
73
|
|
|
65
74
|
const threadId1 = "01JTEST000000000000ACTIVE3" as ThreadId;
|
|
@@ -90,7 +99,6 @@ describe("unified thread storage", () => {
|
|
|
90
99
|
});
|
|
91
100
|
|
|
92
101
|
test("loadHistoryThreads only returns completed and cancelled", async () => {
|
|
93
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-history-test-"));
|
|
94
102
|
const uwf = await makeUwfStore(tmpDir);
|
|
95
103
|
|
|
96
104
|
const threadId1 = "01JTEST000000000000HISTOR1" as ThreadId;
|
|
@@ -132,7 +140,6 @@ describe("unified thread storage", () => {
|
|
|
132
140
|
});
|
|
133
141
|
|
|
134
142
|
test("completeThread marks thread as completed", async () => {
|
|
135
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-complete-test-"));
|
|
136
143
|
const uwf = await makeUwfStore(tmpDir);
|
|
137
144
|
const threadId = "01JTEST000000000000COMPLE1" as ThreadId;
|
|
138
145
|
const head = await seedThreadHead(uwf, "active-head");
|
|
@@ -155,7 +162,6 @@ describe("unified thread storage", () => {
|
|
|
155
162
|
});
|
|
156
163
|
|
|
157
164
|
test("completeThread marks thread as cancelled", async () => {
|
|
158
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-complete-test-"));
|
|
159
165
|
const uwf = await makeUwfStore(tmpDir);
|
|
160
166
|
const threadId = "01JTEST000000000000COMPLE2" as ThreadId;
|
|
161
167
|
const head = await seedThreadHead(uwf, "active-head");
|
|
@@ -178,7 +184,6 @@ describe("unified thread storage", () => {
|
|
|
178
184
|
});
|
|
179
185
|
|
|
180
186
|
test("completeThread clears suspend metadata", async () => {
|
|
181
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-complete-test-"));
|
|
182
187
|
const uwf = await makeUwfStore(tmpDir);
|
|
183
188
|
const threadId = "01JTEST000000000000COMPLE3" as ThreadId;
|
|
184
189
|
const head = await seedThreadHead(uwf, "suspended-head");
|
|
@@ -201,7 +206,6 @@ describe("unified thread storage", () => {
|
|
|
201
206
|
});
|
|
202
207
|
|
|
203
208
|
test("completeThread handles non-existent thread gracefully", async () => {
|
|
204
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-complete-test-"));
|
|
205
209
|
const uwf = await makeUwfStore(tmpDir);
|
|
206
210
|
const threadId = "01JTEST000000000000NOEXIST" as ThreadId;
|
|
207
211
|
|
|
@@ -213,7 +217,6 @@ describe("unified thread storage", () => {
|
|
|
213
217
|
});
|
|
214
218
|
|
|
215
219
|
test("status and completedAt tags are persisted and loaded", async () => {
|
|
216
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-tags-test-"));
|
|
217
220
|
const uwf = await makeUwfStore(tmpDir);
|
|
218
221
|
const threadId = "01JTEST000000000000TAGTEST" as ThreadId;
|
|
219
222
|
const head = await seedThreadHead(uwf, "test-head");
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
5
|
-
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
6
6
|
import {
|
|
7
7
|
completeThread,
|
|
8
|
-
createUwfStore,
|
|
8
|
+
type createUwfStore,
|
|
9
9
|
getThread,
|
|
10
10
|
loadHistoryThreads,
|
|
11
11
|
setThread,
|
|
12
12
|
} from "../store.js";
|
|
13
|
-
|
|
14
|
-
async function makeUwfStore(storageRoot: string) {
|
|
15
|
-
const casDir = join(storageRoot, "cas");
|
|
16
|
-
await mkdir(casDir, { recursive: true });
|
|
17
|
-
process.env.OCAS_HOME = casDir;
|
|
18
|
-
return createUwfStore(storageRoot);
|
|
19
|
-
}
|
|
13
|
+
import { makeUwfStore } from "./thread-test-helpers.js";
|
|
20
14
|
|
|
21
15
|
async function seedHistoryHead(
|
|
22
16
|
uwf: Awaited<ReturnType<typeof createUwfStore>>,
|
|
@@ -25,9 +19,25 @@ async function seedHistoryHead(
|
|
|
25
19
|
return (await uwf.store.cas.put(uwf.schemas.text, label)) as CasRef;
|
|
26
20
|
}
|
|
27
21
|
|
|
22
|
+
let tmpDir: string;
|
|
23
|
+
let savedOcasHome: string | undefined;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
27
|
+
tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
if (savedOcasHome === undefined) {
|
|
32
|
+
delete process.env.OCAS_HOME;
|
|
33
|
+
} else {
|
|
34
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
35
|
+
}
|
|
36
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
28
39
|
describe("thread cancel status", () => {
|
|
29
40
|
test("cancelled thread has status 'cancelled'", async () => {
|
|
30
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
|
31
41
|
const threadId = "01JTEST000000000000CANCEL1" as ThreadId;
|
|
32
42
|
const uwf = await makeUwfStore(tmpDir);
|
|
33
43
|
const head = await seedHistoryHead(uwf, "cancelled-head");
|
|
@@ -48,7 +58,6 @@ describe("thread cancel status", () => {
|
|
|
48
58
|
});
|
|
49
59
|
|
|
50
60
|
test("completed thread has status 'completed'", async () => {
|
|
51
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
|
52
61
|
const threadId = "01JTEST000000000000CANCEL2" as ThreadId;
|
|
53
62
|
const uwf = await makeUwfStore(tmpDir);
|
|
54
63
|
const head = await seedHistoryHead(uwf, "completed-head");
|
|
@@ -69,7 +78,6 @@ describe("thread cancel status", () => {
|
|
|
69
78
|
});
|
|
70
79
|
|
|
71
80
|
test("loadHistoryThreads returns completed and cancelled", async () => {
|
|
72
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
|
73
81
|
const uwf = await makeUwfStore(tmpDir);
|
|
74
82
|
const head1 = await seedHistoryHead(uwf, "head1");
|
|
75
83
|
const head2 = await seedHistoryHead(uwf, "head2");
|
|
@@ -103,7 +111,6 @@ describe("thread cancel status", () => {
|
|
|
103
111
|
});
|
|
104
112
|
|
|
105
113
|
test("mixed completed and cancelled entries preserve distinct statuses", async () => {
|
|
106
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "uwf-cancel-test-"));
|
|
107
114
|
const uwf = await makeUwfStore(tmpDir);
|
|
108
115
|
const head1 = await seedHistoryHead(uwf, "head1");
|
|
109
116
|
const head2 = await seedHistoryHead(uwf, "head2");
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { formatOutput, getTextRenderer, TEXT_RENDERERS } from "../format.js";
|
|
3
|
+
import { renderThreadCancel } from "../text-renderers.js";
|
|
4
|
+
|
|
5
|
+
describe("thread cancel — text renderer registration", () => {
|
|
6
|
+
test("TEXT_RENDERERS contains 'thread cancel'", () => {
|
|
7
|
+
expect(getTextRenderer("thread cancel")).toBeDefined();
|
|
8
|
+
expect(typeof getTextRenderer("thread cancel")).toBe("function");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("TEXT_RENDERERS['thread cancel'] is the same reference as renderThreadCancel", () => {
|
|
12
|
+
expect(TEXT_RENDERERS["thread cancel"]).toBe(renderThreadCancel);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("renderThreadCancel is exported from text-renderers.ts", () => {
|
|
16
|
+
expect(typeof renderThreadCancel).toBe("function");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("renderThreadCancel — output shape", () => {
|
|
21
|
+
test("returns a string for full payload", () => {
|
|
22
|
+
const out = renderThreadCancel({
|
|
23
|
+
thread: "01JTEST000000000000CANCEL1",
|
|
24
|
+
cancelled: true,
|
|
25
|
+
});
|
|
26
|
+
expect(typeof out).toBe("string");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("includes the cancelled thread's ULID", () => {
|
|
30
|
+
const out = renderThreadCancel({
|
|
31
|
+
thread: "01JTEST000000000000CANCEL1",
|
|
32
|
+
cancelled: true,
|
|
33
|
+
});
|
|
34
|
+
expect(out).toContain("01JTEST000000000000CANCEL1");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("indicates cancelled status (Status: cancelled OR Cancelled: yes)", () => {
|
|
38
|
+
const out = renderThreadCancel({
|
|
39
|
+
thread: "01JTEST000000000000CANCEL1",
|
|
40
|
+
cancelled: true,
|
|
41
|
+
});
|
|
42
|
+
// accept either rendering style
|
|
43
|
+
const lower = out.toLowerCase();
|
|
44
|
+
const hasCancelMarker = lower.includes("cancelled") || lower.includes("yes");
|
|
45
|
+
expect(hasCancelMarker).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("does NOT begin with '{' or '[' (not raw JSON)", () => {
|
|
49
|
+
const out = renderThreadCancel({
|
|
50
|
+
thread: "01JTEST000000000000CANCEL1",
|
|
51
|
+
cancelled: true,
|
|
52
|
+
});
|
|
53
|
+
const trimmed = out.trimStart();
|
|
54
|
+
expect(trimmed.startsWith("{")).toBe(false);
|
|
55
|
+
expect(trimmed.startsWith("[")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("does NOT contain literal 'undefined'", () => {
|
|
59
|
+
const out = renderThreadCancel({
|
|
60
|
+
thread: "01JTEST000000000000CANCEL1",
|
|
61
|
+
cancelled: true,
|
|
62
|
+
});
|
|
63
|
+
expect(out).not.toContain("undefined");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("renderThreadCancel — partial / missing data", () => {
|
|
68
|
+
test("missing 'cancelled' field — returns string, no throw, no 'undefined'", () => {
|
|
69
|
+
const out = renderThreadCancel({ thread: "01JTEST000000000000CANCEL1" });
|
|
70
|
+
expect(typeof out).toBe("string");
|
|
71
|
+
expect(out).not.toContain("undefined");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("missing 'thread' field — returns string, no throw, no 'undefined'", () => {
|
|
75
|
+
const out = renderThreadCancel({ cancelled: true });
|
|
76
|
+
expect(typeof out).toBe("string");
|
|
77
|
+
expect(out).not.toContain("undefined");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("empty object — returns string, no throw, no 'undefined'", () => {
|
|
81
|
+
const out = renderThreadCancel({});
|
|
82
|
+
expect(typeof out).toBe("string");
|
|
83
|
+
expect(out).not.toContain("undefined");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("null payload — returns string, no throw", () => {
|
|
87
|
+
expect(() => renderThreadCancel(null)).not.toThrow();
|
|
88
|
+
const out = renderThreadCancel(null);
|
|
89
|
+
expect(typeof out).toBe("string");
|
|
90
|
+
expect(out).not.toContain("undefined");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("non-object payload (string) — returns string, no throw", () => {
|
|
94
|
+
expect(() => renderThreadCancel("oops")).not.toThrow();
|
|
95
|
+
const out = renderThreadCancel("oops");
|
|
96
|
+
expect(typeof out).toBe("string");
|
|
97
|
+
expect(out).not.toContain("undefined");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("formatOutput integration — thread cancel", () => {
|
|
102
|
+
test("formatOutput(data, 'text', 'thread cancel') uses renderer", () => {
|
|
103
|
+
const data = { thread: "01JTEST000000000000CANCEL1", cancelled: true };
|
|
104
|
+
const out = formatOutput(data, "text", "thread cancel");
|
|
105
|
+
expect(typeof out).toBe("string");
|
|
106
|
+
expect(out).not.toContain("undefined");
|
|
107
|
+
expect(out.trimStart().startsWith("{")).toBe(false);
|
|
108
|
+
expect(out).toContain("01JTEST000000000000CANCEL1");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("formatOutput(data, 'json', 'thread cancel') still emits parseable JSON", () => {
|
|
112
|
+
const data = { thread: "01JTEST000000000000CANCEL1", cancelled: true };
|
|
113
|
+
const out = formatOutput(data, "json", "thread cancel");
|
|
114
|
+
const parsed = JSON.parse(out);
|
|
115
|
+
expect(parsed).toEqual(data);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("formatOutput(data, 'yaml', 'thread cancel') still emits YAML", () => {
|
|
119
|
+
const data = { thread: "01JTEST000000000000CANCEL1", cancelled: true };
|
|
120
|
+
const out = formatOutput(data, "yaml", "thread cancel");
|
|
121
|
+
expect(typeof out).toBe("string");
|
|
122
|
+
expect(out).toContain("thread:");
|
|
123
|
+
expect(out).toContain("cancelled:");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
@@ -16,17 +16,10 @@ import {
|
|
|
16
16
|
saveWorkflowRegistry,
|
|
17
17
|
setThread,
|
|
18
18
|
} from "../store.js";
|
|
19
|
+
import { makeUwfStore } from "./thread-test-helpers.js";
|
|
19
20
|
|
|
20
21
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
21
22
|
|
|
22
|
-
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
|
|
23
|
-
const casDir = join(storageRoot, "cas");
|
|
24
|
-
await mkdir(casDir, { recursive: true });
|
|
25
|
-
// Set OCAS_HOME to use the test's CAS directory
|
|
26
|
-
process.env.OCAS_HOME = casDir;
|
|
27
|
-
return createUwfStore(storageRoot);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
23
|
async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
|
|
31
24
|
const workflowPayload = {
|
|
32
25
|
name: "test-workflow",
|
|
@@ -84,12 +77,19 @@ async function completeThread(
|
|
|
84
77
|
// ── test setup ────────────────────────────────────────────────────────────────
|
|
85
78
|
|
|
86
79
|
let tmpDir: string;
|
|
80
|
+
let savedOcasHome: string | undefined;
|
|
87
81
|
|
|
88
82
|
beforeEach(async () => {
|
|
83
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
89
84
|
tmpDir = await mkdtemp(join(tmpdir(), "thread-list-filters-test-"));
|
|
90
85
|
});
|
|
91
86
|
|
|
92
87
|
afterEach(async () => {
|
|
88
|
+
if (savedOcasHome === undefined) {
|
|
89
|
+
delete process.env.OCAS_HOME;
|
|
90
|
+
} else {
|
|
91
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
92
|
+
}
|
|
93
93
|
await rm(tmpDir, { recursive: true, force: true });
|
|
94
94
|
});
|
|
95
95
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { OUTPUT_TEMPLATES } from "@united-workforce/protocol";
|
|
2
|
+
import { Liquid } from "liquidjs";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Issue #351 — `uwf thread list --format text` rendered the `STARTED` column
|
|
7
|
+
* as `58414-12-06` because `THREAD_LIST_TEMPLATE` piped `item.startedAt` (Unix
|
|
8
|
+
* **ms** per `THREAD_LIST_OUTPUT_SCHEMA`) directly into LiquidJS's `| date`
|
|
9
|
+
* filter, which expects Unix **seconds**.
|
|
10
|
+
*
|
|
11
|
+
* This integration test renders the template against a known ms timestamp
|
|
12
|
+
* and asserts the year falls within the realistic 20xx range, confirming
|
|
13
|
+
* the ms→s conversion is in place at the protocol layer.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function makeEngine(): Liquid {
|
|
17
|
+
return new Liquid({ cache: false, strictFilters: false, strictVariables: false });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", () => {
|
|
21
|
+
test("renders item.startedAt=1781229932779 as a 2026 calendar date (not 58414)", async () => {
|
|
22
|
+
const engine = makeEngine();
|
|
23
|
+
const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], {
|
|
24
|
+
items: [
|
|
25
|
+
{
|
|
26
|
+
threadId: "01K5HMKZQB7VDA8E2K9P3R5XBC",
|
|
27
|
+
workflowHash: "WF1234567890A",
|
|
28
|
+
workflowName: null,
|
|
29
|
+
status: "idle",
|
|
30
|
+
currentRole: "planner",
|
|
31
|
+
startedAt: 1781229932779,
|
|
32
|
+
completedAt: null,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(out).not.toContain("58414");
|
|
38
|
+
expect(out).toMatch(/\b20\d{2}-\d{2}-\d{2}\b/);
|
|
39
|
+
// The STARTED cell must NOT begin with a 5-digit year.
|
|
40
|
+
expect(out).not.toMatch(/\b\d{5}-\d{2}-\d{2}\b/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("renders `-` for items with startedAt=null (null guard preserved)", async () => {
|
|
44
|
+
const engine = makeEngine();
|
|
45
|
+
const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], {
|
|
46
|
+
items: [
|
|
47
|
+
{
|
|
48
|
+
threadId: "01K5HMKZQB7VDA8E2K9P3R5XBC",
|
|
49
|
+
workflowHash: "WF1234567890A",
|
|
50
|
+
workflowName: null,
|
|
51
|
+
status: "idle",
|
|
52
|
+
currentRole: "planner",
|
|
53
|
+
startedAt: null,
|
|
54
|
+
completedAt: null,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(out).not.toContain("58414");
|
|
60
|
+
expect(out).not.toContain("Invalid Date");
|
|
61
|
+
expect(out).not.toContain("1970-01-01");
|
|
62
|
+
// Last token of the row is the rendered STARTED cell — must be `-`.
|
|
63
|
+
const dataRow = out
|
|
64
|
+
.split("\n")
|
|
65
|
+
.find((line: string) => line.includes("01K5HMKZQB7VDA8E2K9P3R5XBC"));
|
|
66
|
+
expect(dataRow).toBeDefined();
|
|
67
|
+
expect(dataRow?.trimEnd().endsWith("-")).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("renders multiple ms timestamps across years 2020–2030 with correct year prefix", async () => {
|
|
71
|
+
const engine = makeEngine();
|
|
72
|
+
const items = [
|
|
73
|
+
{
|
|
74
|
+
threadId: "ID1",
|
|
75
|
+
workflowHash: "WF",
|
|
76
|
+
workflowName: null,
|
|
77
|
+
status: "idle",
|
|
78
|
+
currentRole: null,
|
|
79
|
+
startedAt: Date.UTC(2020, 0, 1, 0, 0, 0),
|
|
80
|
+
completedAt: null,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
threadId: "ID2",
|
|
84
|
+
workflowHash: "WF",
|
|
85
|
+
workflowName: null,
|
|
86
|
+
status: "idle",
|
|
87
|
+
currentRole: null,
|
|
88
|
+
startedAt: Date.UTC(2026, 5, 12, 5, 25, 0),
|
|
89
|
+
completedAt: null,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
threadId: "ID3",
|
|
93
|
+
workflowHash: "WF",
|
|
94
|
+
workflowName: null,
|
|
95
|
+
status: "idle",
|
|
96
|
+
currentRole: null,
|
|
97
|
+
startedAt: Date.UTC(2030, 11, 31, 23, 59, 0),
|
|
98
|
+
completedAt: null,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], { items });
|
|
103
|
+
|
|
104
|
+
expect(out).toContain("2020-");
|
|
105
|
+
expect(out).toContain("2026-");
|
|
106
|
+
expect(out).toContain("2030-");
|
|
107
|
+
expect(out).not.toContain("58414");
|
|
108
|
+
expect(out).not.toMatch(/\b\d{5}-\d{2}-\d{2}\b/);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
5
|
+
import { createThreadIndexEntry } from "@united-workforce/protocol";
|
|
6
|
+
import { generateUlid } from "@united-workforce/util";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
8
|
+
import { cmdThreadList } from "../commands/thread.js";
|
|
9
|
+
import type { UwfStore } from "../store.js";
|
|
10
|
+
import { completeThread as completeThreadInStore, setThread } from "../store.js";
|
|
11
|
+
import { makeUwfStore } from "./thread-test-helpers.js";
|
|
12
|
+
|
|
13
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
|
|
16
|
+
const workflowPayload = {
|
|
17
|
+
name: "test-workflow",
|
|
18
|
+
roles: {
|
|
19
|
+
role1: {
|
|
20
|
+
goal: "test goal",
|
|
21
|
+
outputSchema: { type: "object" as const, properties: {} },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
graph: { start: "role1" },
|
|
25
|
+
conditions: {},
|
|
26
|
+
};
|
|
27
|
+
return await uwf.store.cas.put(uwf.schemas.workflow, workflowPayload);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function createTestThread(
|
|
31
|
+
uwf: UwfStore,
|
|
32
|
+
storageRoot: string,
|
|
33
|
+
workflowHash: CasRef,
|
|
34
|
+
timestamp: number,
|
|
35
|
+
): Promise<ThreadId> {
|
|
36
|
+
const threadId = generateUlid(timestamp) as ThreadId;
|
|
37
|
+
const startPayload = {
|
|
38
|
+
workflow: workflowHash,
|
|
39
|
+
prompt: "test prompt",
|
|
40
|
+
cwd: storageRoot,
|
|
41
|
+
};
|
|
42
|
+
const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
|
|
43
|
+
setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash));
|
|
44
|
+
return threadId;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── test setup ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
let tmpDir: string;
|
|
50
|
+
let savedOcasHome: string | undefined;
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
54
|
+
tmpDir = await mkdtemp(join(tmpdir(), "thread-list-workflow-corrupt-test-"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
if (savedOcasHome === undefined) {
|
|
59
|
+
delete process.env.OCAS_HOME;
|
|
60
|
+
} else {
|
|
61
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
62
|
+
}
|
|
63
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── issue #326: loadWorkflowPayload throws instead of process.exit ───────────
|
|
67
|
+
|
|
68
|
+
describe("loadWorkflowPayload throws on error (#326)", () => {
|
|
69
|
+
test("active thread with missing workflow CAS node appears as corrupt", async () => {
|
|
70
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
71
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
|
|
74
|
+
// Create a valid thread
|
|
75
|
+
const validId = await createTestThread(uwf, tmpDir, workflowHash, now);
|
|
76
|
+
|
|
77
|
+
// Create a thread with a different workflow, then delete it
|
|
78
|
+
const otherWorkflowPayload = {
|
|
79
|
+
name: "other-workflow",
|
|
80
|
+
roles: {
|
|
81
|
+
role1: {
|
|
82
|
+
goal: "other goal",
|
|
83
|
+
outputSchema: { type: "object" as const, properties: {} },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
graph: { start: "role1" },
|
|
87
|
+
conditions: {},
|
|
88
|
+
};
|
|
89
|
+
const otherWorkflowHash = await uwf.store.cas.put(uwf.schemas.workflow, otherWorkflowPayload);
|
|
90
|
+
const corruptId = await createTestThread(uwf, tmpDir, otherWorkflowHash, now + 1000);
|
|
91
|
+
|
|
92
|
+
// Delete the other workflow CAS node — start node still exists but workflow ref dangles
|
|
93
|
+
uwf.store.cas.delete(otherWorkflowHash);
|
|
94
|
+
|
|
95
|
+
// thread list should NOT crash — corrupt thread appears with status: "corrupt"
|
|
96
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
97
|
+
|
|
98
|
+
expect(result.length).toBe(2);
|
|
99
|
+
|
|
100
|
+
const validItem = result.find((r) => r.thread === validId);
|
|
101
|
+
expect(validItem).toBeDefined();
|
|
102
|
+
expect(validItem!.status).toBe("idle");
|
|
103
|
+
|
|
104
|
+
const corruptItem = result.find((r) => r.thread === corruptId);
|
|
105
|
+
expect(corruptItem).toBeDefined();
|
|
106
|
+
expect(corruptItem!.status).toBe("corrupt");
|
|
107
|
+
expect(corruptItem!.statusDisplay).toBe("corrupt");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("active thread with wrong-type workflow CAS node appears as corrupt", async () => {
|
|
111
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
|
|
114
|
+
// Create a valid workflow and thread
|
|
115
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
116
|
+
const validId = await createTestThread(uwf, tmpDir, workflowHash, now);
|
|
117
|
+
|
|
118
|
+
// Create a non-workflow CAS node (text type) and use its hash as a workflow ref
|
|
119
|
+
const wrongTypeHash = await uwf.store.cas.put(uwf.schemas.text, "not a workflow");
|
|
120
|
+
|
|
121
|
+
// Create a thread whose start node points to the wrong-type CAS node
|
|
122
|
+
const corruptId = generateUlid(now + 1000) as ThreadId;
|
|
123
|
+
const startPayload = {
|
|
124
|
+
workflow: wrongTypeHash,
|
|
125
|
+
prompt: "corrupt thread with wrong type workflow",
|
|
126
|
+
cwd: tmpDir,
|
|
127
|
+
};
|
|
128
|
+
const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
|
|
129
|
+
setThread(uwf.varStore, corruptId, createThreadIndexEntry(headHash));
|
|
130
|
+
|
|
131
|
+
// thread list should NOT crash — wrong-type thread appears as corrupt
|
|
132
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
133
|
+
|
|
134
|
+
expect(result.length).toBe(2);
|
|
135
|
+
|
|
136
|
+
const validItem = result.find((r) => r.thread === validId);
|
|
137
|
+
expect(validItem).toBeDefined();
|
|
138
|
+
expect(validItem!.status).toBe("idle");
|
|
139
|
+
|
|
140
|
+
const corruptItem = result.find((r) => r.thread === corruptId);
|
|
141
|
+
expect(corruptItem).toBeDefined();
|
|
142
|
+
expect(corruptItem!.status).toBe("corrupt");
|
|
143
|
+
expect(corruptItem!.statusDisplay).toBe("corrupt");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("completed thread with missing workflow CAS node retains stored status with --all", async () => {
|
|
147
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
|
|
150
|
+
// Create two separate workflows so we can corrupt one without affecting the other
|
|
151
|
+
const activeWorkflowHash = await createTestWorkflow(uwf);
|
|
152
|
+
const completedWorkflowPayload = {
|
|
153
|
+
name: "completed-workflow",
|
|
154
|
+
roles: {
|
|
155
|
+
role1: {
|
|
156
|
+
goal: "completed goal",
|
|
157
|
+
outputSchema: { type: "object" as const, properties: {} },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
graph: { start: "role1" },
|
|
161
|
+
conditions: {},
|
|
162
|
+
};
|
|
163
|
+
const completedWorkflowHash = await uwf.store.cas.put(
|
|
164
|
+
uwf.schemas.workflow,
|
|
165
|
+
completedWorkflowPayload,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Create a valid active thread
|
|
169
|
+
const activeId = await createTestThread(uwf, tmpDir, activeWorkflowHash, now);
|
|
170
|
+
|
|
171
|
+
// Create a thread and complete it
|
|
172
|
+
const completedId = await createTestThread(uwf, tmpDir, completedWorkflowHash, now + 1000);
|
|
173
|
+
completeThreadInStore(uwf.varStore, completedId, "end");
|
|
174
|
+
|
|
175
|
+
// Delete only the completed thread's workflow CAS node
|
|
176
|
+
uwf.store.cas.delete(completedWorkflowHash);
|
|
177
|
+
|
|
178
|
+
// thread list --all should NOT crash
|
|
179
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
180
|
+
|
|
181
|
+
expect(result.length).toBe(2);
|
|
182
|
+
|
|
183
|
+
// Active thread is still valid (its workflow exists)
|
|
184
|
+
const activeItem = result.find((r) => r.thread === activeId);
|
|
185
|
+
expect(activeItem).toBeDefined();
|
|
186
|
+
expect(activeItem!.status).toBe("idle");
|
|
187
|
+
|
|
188
|
+
// Completed thread retains its stored status — collectCompletedThreads only calls
|
|
189
|
+
// resolveWorkflowFromHead (returns ref from start node) and never loads the workflow CAS node
|
|
190
|
+
const completedItem = result.find((r) => r.thread === completedId);
|
|
191
|
+
expect(completedItem).toBeDefined();
|
|
192
|
+
expect(completedItem!.status).toBe("end");
|
|
193
|
+
expect(completedItem!.statusDisplay).toBe("end");
|
|
194
|
+
// workflowName is null because the workflow ref won't match a registry entry
|
|
195
|
+
// (the deleted workflow was never registered)
|
|
196
|
+
expect(completedItem!.workflowName).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
});
|