@united-workforce/cli 0.4.0 → 0.6.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__/config-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/config-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/config-text-renderer.test.js +137 -0
- package/dist/__tests__/config-text-renderer.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__/issue-180-workflow-ref-removed.test.js +1 -1
- 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-agent-failure-suspended.test.d.ts +2 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.d.ts.map +1 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js +332 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -0
- 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-join.test.d.ts +2 -0
- package/dist/__tests__/thread-join.test.d.ts.map +1 -0
- package/dist/__tests__/thread-join.test.js +77 -0
- package/dist/__tests__/thread-join.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 +15 -2
- 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-paths.test.d.ts +2 -0
- package/dist/__tests__/workflow-paths.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-paths.test.js +261 -0
- package/dist/__tests__/workflow-paths.test.js.map +1 -0
- 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 +76 -36
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +81 -3
- 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 +12 -0
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +226 -9
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +2 -2
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +26 -10
- 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/store.d.ts +11 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +20 -1
- package/dist/store.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 +12 -11
- package/src/__tests__/adapter-json-roundtrip.test.ts +15 -6
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/config-text-renderer.test.ts +156 -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__/issue-180-workflow-ref-removed.test.ts +1 -1
- 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-agent-failure-suspended.test.ts +406 -0
- 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-join.test.ts +103 -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 +14 -2
- 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-paths.test.ts +337 -0
- 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 +111 -35
- package/src/commands/config.ts +85 -3
- package/src/commands/prompt.ts +42 -29
- package/src/commands/setup.ts +57 -7
- package/src/commands/thread.ts +280 -9
- package/src/commands/workflow.ts +32 -11
- 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 +255 -0
- package/src/schemas.ts +39 -3
- package/src/store.ts +25 -1
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +33 -12
- package/LICENSE +0 -21
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdir, 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 { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
createMarker,
|
|
9
|
+
deleteMarker,
|
|
10
|
+
getProcessStartTime,
|
|
11
|
+
isThreadRunning,
|
|
12
|
+
} from "../background/index.js";
|
|
13
|
+
import { cmdThreadJoin } from "../commands/thread.js";
|
|
14
|
+
import { makeUwfStore, seedThread } from "./thread-test-helpers.js";
|
|
15
|
+
|
|
16
|
+
describe("cmdThreadJoin", () => {
|
|
17
|
+
let storageRoot: string;
|
|
18
|
+
let savedOcasHome: string | undefined;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
22
|
+
storageRoot = join(
|
|
23
|
+
tmpdir(),
|
|
24
|
+
`uwf-test-join-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
25
|
+
);
|
|
26
|
+
await mkdir(storageRoot, { recursive: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
if (savedOcasHome !== undefined) {
|
|
31
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
32
|
+
} else {
|
|
33
|
+
delete process.env.OCAS_HOME;
|
|
34
|
+
}
|
|
35
|
+
await rm(storageRoot, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("throws when thread does not exist", async () => {
|
|
39
|
+
await makeUwfStore(storageRoot);
|
|
40
|
+
const threadId = "01JF0000000000NOTEXIST0" as ThreadId;
|
|
41
|
+
await expect(cmdThreadJoin(storageRoot, threadId, null)).rejects.toThrow(
|
|
42
|
+
/thread not found|process\.exit/,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("times out when thread keeps running", async () => {
|
|
47
|
+
const threadId = "01JF0000000000TESTJOIN03" as ThreadId;
|
|
48
|
+
await makeUwfStore(storageRoot);
|
|
49
|
+
// Seed a thread so existence check passes
|
|
50
|
+
const uwf = await makeUwfStore(storageRoot);
|
|
51
|
+
const head = (await uwf.store.cas.put(uwf.schemas.text, "join-timeout-test")) as CasRef;
|
|
52
|
+
await seedThread(storageRoot, threadId, createThreadIndexEntry(head));
|
|
53
|
+
|
|
54
|
+
// Create a running marker with our PID (it will stay alive)
|
|
55
|
+
await createMarker(storageRoot, {
|
|
56
|
+
thread: threadId,
|
|
57
|
+
workflow: "AAAAAAAAAAAAA" as CasRef,
|
|
58
|
+
pid: process.pid,
|
|
59
|
+
startedAt: Date.now(),
|
|
60
|
+
processStartTime: getProcessStartTime(process.pid),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Timeout after 100ms — should fail because marker never disappears
|
|
64
|
+
await expect(cmdThreadJoin(storageRoot, threadId, 100)).rejects.toThrow(
|
|
65
|
+
/join timed out|process\.exit/,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Cleanup
|
|
69
|
+
await deleteMarker(storageRoot, threadId);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("poll loop exits when marker is removed", async () => {
|
|
73
|
+
const threadId = "01JF0000000000TESTJOIN04" as ThreadId;
|
|
74
|
+
const uwf = await makeUwfStore(storageRoot);
|
|
75
|
+
const head = (await uwf.store.cas.put(uwf.schemas.text, "join-poll-test")) as CasRef;
|
|
76
|
+
await seedThread(storageRoot, threadId, createThreadIndexEntry(head));
|
|
77
|
+
|
|
78
|
+
// Create a running marker
|
|
79
|
+
await createMarker(storageRoot, {
|
|
80
|
+
thread: threadId,
|
|
81
|
+
workflow: "AAAAAAAAAAAAA" as CasRef,
|
|
82
|
+
pid: process.pid,
|
|
83
|
+
startedAt: Date.now(),
|
|
84
|
+
processStartTime: getProcessStartTime(process.pid),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Confirm marker is valid
|
|
88
|
+
expect(await isThreadRunning(storageRoot, threadId)).not.toBeNull();
|
|
89
|
+
|
|
90
|
+
// Remove it after a short delay — simulates background worker finishing
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
deleteMarker(storageRoot, threadId);
|
|
93
|
+
}, 300);
|
|
94
|
+
|
|
95
|
+
// cmdThreadJoin will poll and wait. It will exit the poll loop after marker
|
|
96
|
+
// disappears, then try to resolve workflow from head. Our simple text node
|
|
97
|
+
// won't resolve, so it will fail — but the key test is that the poll loop
|
|
98
|
+
// DID exit (it didn't time out). We use a generous timeout to prove this.
|
|
99
|
+
await expect(cmdThreadJoin(storageRoot, threadId, 5000)).rejects.toThrow(
|
|
100
|
+
/failed to resolve workflow|process\.exit/,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -28,12 +28,19 @@ const OUTPUT_SCHEMA = {
|
|
|
28
28
|
const THREAD_ID = "01POKESTEPTEST00000000" as ThreadId;
|
|
29
29
|
|
|
30
30
|
let tmpDir: string;
|
|
31
|
+
let savedOcasHome: string | undefined;
|
|
31
32
|
|
|
32
33
|
beforeEach(async () => {
|
|
34
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
33
35
|
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-poke-test-"));
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
afterEach(async () => {
|
|
39
|
+
if (savedOcasHome === undefined) {
|
|
40
|
+
delete process.env.OCAS_HOME;
|
|
41
|
+
} else {
|
|
42
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
43
|
+
}
|
|
37
44
|
await rm(tmpDir, { recursive: true, force: true });
|
|
38
45
|
});
|
|
39
46
|
|
|
@@ -271,8 +278,10 @@ function runUwf(
|
|
|
271
278
|
casDir: string,
|
|
272
279
|
): { stdout: string; stderr: string; status: number } {
|
|
273
280
|
const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
|
|
281
|
+
// Tests parse stdout as bare JSON; default --format text would break that.
|
|
282
|
+
const formatArgs = args.includes("--format") ? args : ["--format", "raw-json", ...args];
|
|
274
283
|
try {
|
|
275
|
-
const stdout = execFileSync(process.execPath, [cliPath, ...
|
|
284
|
+
const stdout = execFileSync(process.execPath, [cliPath, ...formatArgs], {
|
|
276
285
|
encoding: "utf8",
|
|
277
286
|
stdio: ["ignore", "pipe", "pipe"],
|
|
278
287
|
env: {
|
|
@@ -529,7 +538,7 @@ describe("uwf thread poke - edge cases", () => {
|
|
|
529
538
|
expect(cliOutput.suspendMessage).toBeNull();
|
|
530
539
|
});
|
|
531
540
|
|
|
532
|
-
test("6.2 agent failure leaves thread head unchanged", async () => {
|
|
541
|
+
test("6.2 agent failure leaves thread head unchanged and suspends thread", async () => {
|
|
533
542
|
const { casDir, oldStepHash, failingAgentPath } = await setupThread();
|
|
534
543
|
const result = runUwf(
|
|
535
544
|
["thread", "poke", THREAD_ID, "-p", "redo", "--agent", failingAgentPath],
|
|
@@ -541,5 +550,8 @@ describe("uwf thread poke - edge cases", () => {
|
|
|
541
550
|
const uwf = await createUwfStore(tmpDir);
|
|
542
551
|
const entry = getThread(uwf.varStore, THREAD_ID);
|
|
543
552
|
expect(entry?.head).toBe(oldStepHash);
|
|
553
|
+
expect(entry?.status).toBe("suspended");
|
|
554
|
+
expect(entry?.suspendedRole).toBe("worker");
|
|
555
|
+
expect(entry?.suspendMessage).toContain("agent command failed");
|
|
544
556
|
});
|
|
545
557
|
});
|
|
@@ -1,13 +1,11 @@
|
|
|
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 { bootstrap, putSchema, type Store } from "@ocas/core";
|
|
5
5
|
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
7
7
|
import { cmdThreadRead, THREAD_READ_DEFAULT_QUOTA } from "../commands/thread.js";
|
|
8
|
-
import
|
|
9
|
-
import { createUwfStore } from "../store.js";
|
|
10
|
-
import { seedThreads } from "./thread-test-helpers.js";
|
|
8
|
+
import { makeUwfStore, seedThreads } from "./thread-test-helpers.js";
|
|
11
9
|
|
|
12
10
|
// ── schemas used in tests ────────────────────────────────────────────────────
|
|
13
11
|
|
|
@@ -49,13 +47,6 @@ const DETAIL_SCHEMA = {
|
|
|
49
47
|
|
|
50
48
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
51
49
|
|
|
52
|
-
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
|
|
53
|
-
const casDir = join(storageRoot, "cas");
|
|
54
|
-
await mkdir(casDir, { recursive: true });
|
|
55
|
-
process.env.OCAS_HOME = casDir;
|
|
56
|
-
return createUwfStore(storageRoot);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
50
|
async function registerDetailSchemas(store: Store) {
|
|
60
51
|
await bootstrap(store);
|
|
61
52
|
const [turn, detail] = await Promise.all([
|
|
@@ -68,12 +59,19 @@ async function registerDetailSchemas(store: Store) {
|
|
|
68
59
|
// ── fixture ───────────────────────────────────────────────────────────────────
|
|
69
60
|
|
|
70
61
|
let tmpDir: string;
|
|
62
|
+
let savedOcasHome: string | undefined;
|
|
71
63
|
|
|
72
64
|
beforeEach(async () => {
|
|
65
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
73
66
|
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-test-"));
|
|
74
67
|
});
|
|
75
68
|
|
|
76
69
|
afterEach(async () => {
|
|
70
|
+
if (savedOcasHome === undefined) {
|
|
71
|
+
delete process.env.OCAS_HOME;
|
|
72
|
+
} else {
|
|
73
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
74
|
+
}
|
|
77
75
|
await rm(tmpDir, { recursive: true, force: true });
|
|
78
76
|
});
|
|
79
77
|
|