@united-workforce/cli 0.2.1-rc.9 → 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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { generateUlid } from "@united-workforce/util";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
6
|
+
import { createMarker, getProcessStartTime, isMarkerValid, isThreadRunning, listRunningThreads, readMarker, } from "../background/index.js";
|
|
7
|
+
import { createUwfStore } from "../store.js";
|
|
8
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
async function makeUwfStore(storageRoot) {
|
|
10
|
+
const casDir = join(storageRoot, "cas");
|
|
11
|
+
await mkdir(casDir, { recursive: true });
|
|
12
|
+
process.env.OCAS_HOME = casDir;
|
|
13
|
+
return createUwfStore(storageRoot);
|
|
14
|
+
}
|
|
15
|
+
async function createTestWorkflow(uwf) {
|
|
16
|
+
const workflowPayload = {
|
|
17
|
+
name: "test-workflow",
|
|
18
|
+
roles: {
|
|
19
|
+
role1: {
|
|
20
|
+
goal: "test goal",
|
|
21
|
+
outputSchema: { type: "object", properties: {} },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
graph: { start: "role1" },
|
|
25
|
+
conditions: {},
|
|
26
|
+
};
|
|
27
|
+
return await uwf.store.cas.put(uwf.schemas.workflow, workflowPayload);
|
|
28
|
+
}
|
|
29
|
+
// ── test setup ────────────────────────────────────────────────────────────────
|
|
30
|
+
let tmpDir;
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
tmpDir = await mkdtemp(join(tmpdir(), "pid-recycling-test-"));
|
|
33
|
+
});
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
// ── Spec: thread-marker-process-identity ──────────────────────────────────────
|
|
38
|
+
describe("marker records process start time", () => {
|
|
39
|
+
test("createMarker stores processStartTime in marker file", async () => {
|
|
40
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
41
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
42
|
+
const threadId = generateUlid(Date.now());
|
|
43
|
+
const processStartTime = getProcessStartTime(process.pid);
|
|
44
|
+
await createMarker(tmpDir, {
|
|
45
|
+
thread: threadId,
|
|
46
|
+
workflow: workflowHash,
|
|
47
|
+
pid: process.pid,
|
|
48
|
+
startedAt: Date.now(),
|
|
49
|
+
processStartTime,
|
|
50
|
+
});
|
|
51
|
+
const marker = await readMarker(tmpDir, threadId);
|
|
52
|
+
expect(marker).not.toBeNull();
|
|
53
|
+
expect(marker.pid).toBe(process.pid);
|
|
54
|
+
expect(marker.processStartTime).toBe(processStartTime);
|
|
55
|
+
});
|
|
56
|
+
test("processStartTime is number on Linux when /proc is available", async () => {
|
|
57
|
+
const startTime = getProcessStartTime(process.pid);
|
|
58
|
+
// On Linux, this should be a number (clock ticks since boot)
|
|
59
|
+
// On non-Linux, it may be null
|
|
60
|
+
if (process.platform === "linux") {
|
|
61
|
+
expect(typeof startTime).toBe("number");
|
|
62
|
+
expect(startTime).toBeGreaterThan(0);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// On non-Linux, null is acceptable
|
|
66
|
+
expect(startTime === null || typeof startTime === "number").toBe(true);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
test("getProcessStartTime returns null for non-existent PID", () => {
|
|
70
|
+
// PID 99999999 is unlikely to exist
|
|
71
|
+
const startTime = getProcessStartTime(99999999);
|
|
72
|
+
expect(startTime).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
// ── Spec: thread-marker-valid-process-still-blocked ───────────────────────────
|
|
76
|
+
describe("valid marker still blocks execution", () => {
|
|
77
|
+
test("isMarkerValid returns true when PID alive and processStartTime matches", () => {
|
|
78
|
+
const processStartTime = getProcessStartTime(process.pid);
|
|
79
|
+
const marker = {
|
|
80
|
+
thread: "test-thread",
|
|
81
|
+
workflow: "test-workflow",
|
|
82
|
+
pid: process.pid,
|
|
83
|
+
startedAt: Date.now(),
|
|
84
|
+
processStartTime,
|
|
85
|
+
};
|
|
86
|
+
const valid = isMarkerValid(marker);
|
|
87
|
+
expect(valid).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
test("isThreadRunning returns marker when PID alive and processStartTime matches", async () => {
|
|
90
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
91
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
92
|
+
const threadId = generateUlid(Date.now());
|
|
93
|
+
const processStartTime = getProcessStartTime(process.pid);
|
|
94
|
+
await createMarker(tmpDir, {
|
|
95
|
+
thread: threadId,
|
|
96
|
+
workflow: workflowHash,
|
|
97
|
+
pid: process.pid,
|
|
98
|
+
startedAt: Date.now(),
|
|
99
|
+
processStartTime,
|
|
100
|
+
});
|
|
101
|
+
const result = await isThreadRunning(tmpDir, threadId);
|
|
102
|
+
expect(result).not.toBeNull();
|
|
103
|
+
expect(result.pid).toBe(process.pid);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
// ── Spec: thread-exec-stale-marker-recovery ───────────────────────────────────
|
|
107
|
+
describe("stale marker recovery on exec", () => {
|
|
108
|
+
test("isMarkerValid returns false when processStartTime does not match", () => {
|
|
109
|
+
// Create a marker with a mismatched processStartTime
|
|
110
|
+
const marker = {
|
|
111
|
+
thread: "test-thread",
|
|
112
|
+
workflow: "test-workflow",
|
|
113
|
+
pid: process.pid, // PID is alive (it's our own process)
|
|
114
|
+
startedAt: Date.now(),
|
|
115
|
+
processStartTime: 1, // Deliberately wrong start time
|
|
116
|
+
};
|
|
117
|
+
const valid = isMarkerValid(marker);
|
|
118
|
+
expect(valid).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
test("isThreadRunning deletes stale marker and returns null when processStartTime mismatches", async () => {
|
|
121
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
122
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
123
|
+
const threadId = generateUlid(Date.now());
|
|
124
|
+
// Write a marker with a deliberately wrong processStartTime
|
|
125
|
+
await createMarker(tmpDir, {
|
|
126
|
+
thread: threadId,
|
|
127
|
+
workflow: workflowHash,
|
|
128
|
+
pid: process.pid, // alive process
|
|
129
|
+
startedAt: Date.now(),
|
|
130
|
+
processStartTime: 1, // wrong start time - simulates PID recycling
|
|
131
|
+
});
|
|
132
|
+
// isThreadRunning should detect the stale marker and clean it up
|
|
133
|
+
const result = await isThreadRunning(tmpDir, threadId);
|
|
134
|
+
expect(result).toBeNull();
|
|
135
|
+
// Verify marker file was deleted
|
|
136
|
+
const markerAfter = await readMarker(tmpDir, threadId);
|
|
137
|
+
expect(markerAfter).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
test("isMarkerValid returns false when PID is not alive (regardless of processStartTime)", () => {
|
|
140
|
+
const marker = {
|
|
141
|
+
thread: "test-thread",
|
|
142
|
+
workflow: "test-workflow",
|
|
143
|
+
pid: 99999999, // non-existent PID
|
|
144
|
+
startedAt: Date.now(),
|
|
145
|
+
processStartTime: 12345,
|
|
146
|
+
};
|
|
147
|
+
const valid = isMarkerValid(marker);
|
|
148
|
+
expect(valid).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
// ── Spec: thread-list-stale-marker-cleanup ────────────────────────────────────
|
|
152
|
+
describe("thread list filters stale markers", () => {
|
|
153
|
+
test("listRunningThreads excludes threads with mismatched processStartTime", async () => {
|
|
154
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
155
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
156
|
+
// T1: stale marker (PID alive, but wrong processStartTime)
|
|
157
|
+
const threadId1 = generateUlid(Date.now());
|
|
158
|
+
await createMarker(tmpDir, {
|
|
159
|
+
thread: threadId1,
|
|
160
|
+
workflow: workflowHash,
|
|
161
|
+
pid: process.pid,
|
|
162
|
+
startedAt: Date.now(),
|
|
163
|
+
processStartTime: 1, // wrong — simulates PID recycling
|
|
164
|
+
});
|
|
165
|
+
// T2: valid marker (PID alive, correct processStartTime)
|
|
166
|
+
const threadId2 = generateUlid(Date.now() + 1);
|
|
167
|
+
const correctStartTime = getProcessStartTime(process.pid);
|
|
168
|
+
await createMarker(tmpDir, {
|
|
169
|
+
thread: threadId2,
|
|
170
|
+
workflow: workflowHash,
|
|
171
|
+
pid: process.pid,
|
|
172
|
+
startedAt: Date.now(),
|
|
173
|
+
processStartTime: correctStartTime,
|
|
174
|
+
});
|
|
175
|
+
const running = await listRunningThreads(tmpDir);
|
|
176
|
+
// Only T2 should be listed
|
|
177
|
+
expect(running.length).toBe(1);
|
|
178
|
+
expect(running[0].thread).toBe(threadId2);
|
|
179
|
+
// T1's marker should have been deleted
|
|
180
|
+
const markerT1 = await readMarker(tmpDir, threadId1);
|
|
181
|
+
expect(markerT1).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
test("listRunningThreads deletes marker when PID is dead", async () => {
|
|
184
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
185
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
186
|
+
const threadId = generateUlid(Date.now());
|
|
187
|
+
// Marker with a non-existent PID
|
|
188
|
+
await createMarker(tmpDir, {
|
|
189
|
+
thread: threadId,
|
|
190
|
+
workflow: workflowHash,
|
|
191
|
+
pid: 99999999,
|
|
192
|
+
startedAt: Date.now(),
|
|
193
|
+
processStartTime: 12345,
|
|
194
|
+
});
|
|
195
|
+
const running = await listRunningThreads(tmpDir);
|
|
196
|
+
expect(running.length).toBe(0);
|
|
197
|
+
// Marker should be deleted
|
|
198
|
+
const markerAfter = await readMarker(tmpDir, threadId);
|
|
199
|
+
expect(markerAfter).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
// ── Spec: thread-stop-validates-process-identity ──────────────────────────────
|
|
203
|
+
describe("thread stop validates process identity", () => {
|
|
204
|
+
test("isMarkerValid returns false for recycled PID (PID alive, wrong start time)", () => {
|
|
205
|
+
// Simulate: marker says processStartTime=100, but actual process started at a different time
|
|
206
|
+
const marker = {
|
|
207
|
+
thread: "test-thread",
|
|
208
|
+
workflow: "test-workflow",
|
|
209
|
+
pid: process.pid, // alive
|
|
210
|
+
startedAt: Date.now(),
|
|
211
|
+
processStartTime: 1, // wrong — this PID was recycled
|
|
212
|
+
};
|
|
213
|
+
expect(isMarkerValid(marker)).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
// ── Spec: thread-cancel-validates-process-identity ────────────────────────────
|
|
217
|
+
describe("thread cancel validates process identity", () => {
|
|
218
|
+
test("isMarkerValid correctly identifies stale markers for cancel scenario", () => {
|
|
219
|
+
const marker = {
|
|
220
|
+
thread: "test-thread",
|
|
221
|
+
workflow: "test-workflow",
|
|
222
|
+
pid: process.pid, // alive
|
|
223
|
+
startedAt: Date.now(),
|
|
224
|
+
processStartTime: 1, // wrong — this is a recycled PID
|
|
225
|
+
};
|
|
226
|
+
expect(isMarkerValid(marker)).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
// ── Legacy marker compatibility ───────────────────────────────────────────────
|
|
230
|
+
describe("backward compatibility with old markers", () => {
|
|
231
|
+
test("marker without processStartTime field is treated as stale when PID alive", async () => {
|
|
232
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
233
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
234
|
+
const threadId = generateUlid(Date.now());
|
|
235
|
+
// Simulate an old-format marker (no processStartTime field)
|
|
236
|
+
const runningDir = join(tmpDir, "running");
|
|
237
|
+
await mkdir(runningDir, { recursive: true });
|
|
238
|
+
const markerPath = join(runningDir, `${threadId}.json`);
|
|
239
|
+
const oldMarker = {
|
|
240
|
+
thread: threadId,
|
|
241
|
+
workflow: workflowHash,
|
|
242
|
+
pid: process.pid,
|
|
243
|
+
startedAt: Date.now(),
|
|
244
|
+
// No processStartTime field
|
|
245
|
+
};
|
|
246
|
+
await writeFile(markerPath, JSON.stringify(oldMarker, null, 2), "utf8");
|
|
247
|
+
// Reading the marker should work (null processStartTime)
|
|
248
|
+
const marker = await readMarker(tmpDir, threadId);
|
|
249
|
+
expect(marker).not.toBeNull();
|
|
250
|
+
expect(marker.processStartTime).toBeNull();
|
|
251
|
+
// isMarkerValid should still accept it gracefully (null means can't verify — fallback to PID check only)
|
|
252
|
+
// But since we can't verify identity, we treat it as potentially stale
|
|
253
|
+
// The spec says: on non-Linux, processStartTime is null — same behavior
|
|
254
|
+
// When processStartTime is null in marker AND we can't read /proc (or it's null from getProcessStartTime too),
|
|
255
|
+
// we fall back to the PID-alive-only check for backward compat
|
|
256
|
+
const valid = isMarkerValid(marker);
|
|
257
|
+
// With null processStartTime in the marker, we can't verify identity,
|
|
258
|
+
// but the PID IS alive. For backward compat, this should be treated as valid
|
|
259
|
+
// (the new getProcessStartTime returns a real number on Linux, null if unavailable)
|
|
260
|
+
if (process.platform === "linux") {
|
|
261
|
+
// On Linux where we CAN get the actual start time but the marker has null,
|
|
262
|
+
// we cannot confirm identity — treat as potentially stale
|
|
263
|
+
// However for backward compat during transition, null in marker = skip identity check
|
|
264
|
+
expect(valid).toBe(true);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
expect(valid).toBe(true);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
//# sourceMappingURL=pid-recycling.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pid-recycling.test.js","sourceRoot":"","sources":["../../src/__tests__/pid-recycling.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { describe, expect, test } from "vitest";
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
import { generateCliReference } from "@united-workforce/util";
|
|
6
7
|
import { cmdPromptAdapterDeveloping, cmdPromptBootstrap, cmdPromptList, cmdPromptUsage, cmdPromptWorkflowAuthoring, } from "../commands/prompt.js";
|
|
7
8
|
describe("prompt commands", () => {
|
|
8
9
|
test("prompt list returns prompt names (no bootstrap)", () => {
|
|
@@ -31,6 +32,22 @@ describe("prompt commands", () => {
|
|
|
31
32
|
expect(result).not.toContain("Adapter Developing Reference");
|
|
32
33
|
expect(result.length).toBeGreaterThan(500);
|
|
33
34
|
});
|
|
35
|
+
test("prompt usage describes .workflows/ auto-discovery", () => {
|
|
36
|
+
const result = cmdPromptUsage();
|
|
37
|
+
expect(result).toContain(".workflows/");
|
|
38
|
+
expect(result).toContain("uwf thread start solve-issue");
|
|
39
|
+
expect(result.toLowerCase()).toContain("auto-discover");
|
|
40
|
+
expect(result.toLowerCase()).toContain("recommended");
|
|
41
|
+
});
|
|
42
|
+
test("prompt cli-reference describes .workflows/ auto-discovery", () => {
|
|
43
|
+
const ref = generateCliReference();
|
|
44
|
+
expect(ref).toContain(".workflows/");
|
|
45
|
+
expect(ref.toLowerCase()).toContain("cwd upward");
|
|
46
|
+
expect(ref).toContain("workflow list");
|
|
47
|
+
expect(ref).toMatch(/CAS hash/i);
|
|
48
|
+
expect(ref).toMatch(/file path/i);
|
|
49
|
+
expect(ref).toMatch(/registry/i);
|
|
50
|
+
});
|
|
34
51
|
test("prompt workflow-authoring returns non-empty markdown string with frontmatter", () => {
|
|
35
52
|
const result = cmdPromptWorkflowAuthoring();
|
|
36
53
|
expect(typeof result).toBe("string");
|
|
@@ -44,6 +61,36 @@ describe("prompt commands", () => {
|
|
|
44
61
|
expect(result).toContain("version:");
|
|
45
62
|
expect(result.length).toBeGreaterThan(500);
|
|
46
63
|
});
|
|
64
|
+
test("prompt workflow-authoring documents .workflows/ Placement section", () => {
|
|
65
|
+
const result = cmdPromptWorkflowAuthoring();
|
|
66
|
+
expect(result).toContain("## Placement");
|
|
67
|
+
expect(result).toContain(".workflows/");
|
|
68
|
+
expect(result).toContain("solve-issue.yaml");
|
|
69
|
+
expect(result.toLowerCase()).toContain("auto-discover");
|
|
70
|
+
expect(result.toLowerCase()).toContain("no workflow add");
|
|
71
|
+
// Placement must appear before Self-Testing
|
|
72
|
+
expect(result.indexOf("## Placement")).toBeLessThan(result.indexOf("## Self-Testing"));
|
|
73
|
+
});
|
|
74
|
+
test("prompt workflow-authoring mentions .workflow/ as legacy fallback", () => {
|
|
75
|
+
const result = cmdPromptWorkflowAuthoring();
|
|
76
|
+
expect(result).toContain(".workflow/");
|
|
77
|
+
expect(result.toLowerCase()).toContain("legacy");
|
|
78
|
+
});
|
|
79
|
+
test("prompt workflow-authoring documents Liquid filters with join example", () => {
|
|
80
|
+
const result = cmdPromptWorkflowAuthoring();
|
|
81
|
+
expect(result).toContain("| join");
|
|
82
|
+
expect(result.toLowerCase()).toContain("filter");
|
|
83
|
+
});
|
|
84
|
+
test("prompt workflow-authoring documents Liquid loops with for example", () => {
|
|
85
|
+
const result = cmdPromptWorkflowAuthoring();
|
|
86
|
+
expect(result).toContain("{% for");
|
|
87
|
+
expect(result).toContain("{% endfor %}");
|
|
88
|
+
});
|
|
89
|
+
test("prompt workflow-authoring uses Liquid terminology with no Mustache remnants", () => {
|
|
90
|
+
const result = cmdPromptWorkflowAuthoring();
|
|
91
|
+
expect(result.toLowerCase()).not.toContain("mustache");
|
|
92
|
+
expect(result.toLowerCase()).toContain("liquid");
|
|
93
|
+
});
|
|
47
94
|
test("prompt adapter-developing returns non-empty markdown string with frontmatter", () => {
|
|
48
95
|
const result = cmdPromptAdapterDeveloping();
|
|
49
96
|
expect(typeof result).toBe("string");
|
|
@@ -96,4 +143,278 @@ describe("prompt commands", () => {
|
|
|
96
143
|
expect(output).not.toContain("usage-reference");
|
|
97
144
|
});
|
|
98
145
|
});
|
|
146
|
+
describe("prompt adapter-developing — issue #214 v0.4 contract", () => {
|
|
147
|
+
const text = cmdPromptAdapterDeveloping();
|
|
148
|
+
const lower = text.toLowerCase();
|
|
149
|
+
// ── Item 1 — AgentOptions includes fork and cleanup ─────────────────
|
|
150
|
+
test("AgentOptions documents fork field with AgentForkFn | null", () => {
|
|
151
|
+
expect(text).toContain("AgentOptions");
|
|
152
|
+
expect(text).toMatch(/fork\s*:\s*AgentForkFn\s*\|\s*null/);
|
|
153
|
+
expect(text).toContain("AgentForkFn");
|
|
154
|
+
});
|
|
155
|
+
test("AgentOptions documents cleanup field with AgentCleanupFn | null", () => {
|
|
156
|
+
expect(text).toMatch(/cleanup\s*:\s*AgentCleanupFn\s*\|\s*null/);
|
|
157
|
+
expect(text).toContain("AgentCleanupFn");
|
|
158
|
+
});
|
|
159
|
+
test("explains that fork=null is acceptable for adapters that do not implement step ask", () => {
|
|
160
|
+
expect(lower).toMatch(/fork.*null.*(do(es)? not|no).*step ask|step ask.*fork.*null/);
|
|
161
|
+
});
|
|
162
|
+
test("explains that cleanup runs after the agent completes (success or failure)", () => {
|
|
163
|
+
expect(lower).toMatch(/cleanup.*(after|completes|invoked).*(release|i\/?o|resources|subprocess)/);
|
|
164
|
+
});
|
|
165
|
+
// ── Item 2 — Public helpers table is complete ───────────────────────
|
|
166
|
+
test("helpers table lists buildRolePrompt", () => {
|
|
167
|
+
expect(text).toContain("buildRolePrompt");
|
|
168
|
+
});
|
|
169
|
+
test("helpers table lists buildContinuationPrompt", () => {
|
|
170
|
+
expect(text).toContain("buildContinuationPrompt");
|
|
171
|
+
});
|
|
172
|
+
test("helpers table lists buildThreadProgress", () => {
|
|
173
|
+
expect(text).toContain("buildThreadProgress");
|
|
174
|
+
});
|
|
175
|
+
test("helpers table lists buildOutputFormatInstruction", () => {
|
|
176
|
+
expect(text).toContain("buildOutputFormatInstruction");
|
|
177
|
+
});
|
|
178
|
+
test("helpers table lists buildSuspendOutput", () => {
|
|
179
|
+
expect(text).toContain("buildSuspendOutput");
|
|
180
|
+
});
|
|
181
|
+
test("helpers table lists buildFrontmatterRetryPrompt", () => {
|
|
182
|
+
expect(text).toContain("buildFrontmatterRetryPrompt");
|
|
183
|
+
});
|
|
184
|
+
test("helpers table lists session-cache helpers", () => {
|
|
185
|
+
expect(text).toContain("getCachedSessionId");
|
|
186
|
+
expect(text).toContain("setCachedSessionId");
|
|
187
|
+
expect(text).toContain("getAskSessionId");
|
|
188
|
+
expect(text).toContain("setAskSessionId");
|
|
189
|
+
});
|
|
190
|
+
// ── Item 3 — $SUSPEND coroutine yield ───────────────────────────────
|
|
191
|
+
test("documents $SUSPEND as coroutine yield with reason", () => {
|
|
192
|
+
expect(text).toContain("$SUSPEND");
|
|
193
|
+
expect(lower).toContain("coroutine");
|
|
194
|
+
expect(lower).toMatch(/reason/);
|
|
195
|
+
});
|
|
196
|
+
test("documents buildSuspendOutput helper to emit a $SUSPEND output", () => {
|
|
197
|
+
expect(text).toContain("buildSuspendOutput");
|
|
198
|
+
expect(text).toMatch(/buildSuspendOutput\s*\(/);
|
|
199
|
+
});
|
|
200
|
+
test("documents trySuspendFastPath round-trip and SUSPEND_OUTPUT_SCHEMA", () => {
|
|
201
|
+
expect(text).toContain("trySuspendFastPath");
|
|
202
|
+
expect(text).toMatch(/SUSPEND_OUTPUT_SCHEMA|suspendOutput/);
|
|
203
|
+
});
|
|
204
|
+
test("explains engine intercepts $SUSPEND before the moderator", () => {
|
|
205
|
+
expect(lower).toMatch(/intercept.*moderator|before the moderator|engine.*suspend/);
|
|
206
|
+
expect(lower).toMatch(/(thread|state).*suspend/);
|
|
207
|
+
});
|
|
208
|
+
test("notes that $SUSPEND is reserved and may be emitted by any role regardless of declared output", () => {
|
|
209
|
+
expect(lower).toMatch(/(any role|every role|regardless).*\$?suspend|reserved/);
|
|
210
|
+
});
|
|
211
|
+
// ── Item 4 — step ask adapter contract ──────────────────────────────
|
|
212
|
+
test("documents step ask --mode fork CLI contract for adapters", () => {
|
|
213
|
+
expect(text).toContain("--mode fork");
|
|
214
|
+
expect(text).toContain("--session");
|
|
215
|
+
expect(lower).toMatch(/fork.*(stdout|prints|return).*session/);
|
|
216
|
+
});
|
|
217
|
+
test("documents step ask --mode ask CLI contract", () => {
|
|
218
|
+
expect(text).toContain("--mode ask");
|
|
219
|
+
expect(text).toContain("--prompt");
|
|
220
|
+
});
|
|
221
|
+
test("explains that fork: null adapters do not need to handle --mode fork/ask", () => {
|
|
222
|
+
expect(lower).toMatch(/fork\s*:\s*null.*(do(es)? not|no|not required).*(--mode|step ask)/);
|
|
223
|
+
});
|
|
224
|
+
test("documents the per-stepHash ask-session cache key", () => {
|
|
225
|
+
expect(text).toContain("getAskSessionId");
|
|
226
|
+
expect(lower).toMatch(/(<step ?hash>|stephash).*:ask|ask.*cache|forked.*session.*step/);
|
|
227
|
+
});
|
|
228
|
+
// ── Item 5 — Adapter-owned LLM config ───────────────────────────────
|
|
229
|
+
test("explains engine config.yaml is LLM-free (no providers/models)", () => {
|
|
230
|
+
expect(lower).toMatch(/engine.*(config|llm-free|llm free)/);
|
|
231
|
+
expect(lower).toMatch(/no.*(provider|model|api[- ]?key)/);
|
|
232
|
+
});
|
|
233
|
+
test("shows the adapter-owned config path convention ~/.uwf/agents/<name>.yaml", () => {
|
|
234
|
+
expect(text).toMatch(/~\/?\.uwf\/agents\/.+\.yaml/);
|
|
235
|
+
});
|
|
236
|
+
test("shows a concrete example with provider.baseUrl, provider.apiKey, model", () => {
|
|
237
|
+
expect(text).toContain("baseUrl");
|
|
238
|
+
expect(text).toContain("apiKey");
|
|
239
|
+
expect(text).toMatch(/^\s*model\s*:/m);
|
|
240
|
+
});
|
|
241
|
+
test("references storageRoot from AgentContext as the way to resolve the adapter config path", () => {
|
|
242
|
+
expect(text).toContain("storageRoot");
|
|
243
|
+
expect(lower).toMatch(/(storageroot|ctx\.storageroot).*(agents\/|config|yaml)/);
|
|
244
|
+
});
|
|
245
|
+
// ── Item 6 — previousAttempts and $status: error ────────────────────
|
|
246
|
+
test("documents the failed-step retry path with $status: error", () => {
|
|
247
|
+
expect(text).toMatch(/\$status\s*:\s*["']?error["']?/);
|
|
248
|
+
expect(text).toContain("ErrorOutputPayload");
|
|
249
|
+
});
|
|
250
|
+
test("documents previousAttempts as CAS refs to prior failed StepNodes", () => {
|
|
251
|
+
expect(text).toContain("previousAttempts");
|
|
252
|
+
expect(lower).toMatch(/previousattempts.*(failed|prior|retry).*step/);
|
|
253
|
+
});
|
|
254
|
+
test("explains thread head is NOT advanced on isError=true", () => {
|
|
255
|
+
expect(lower).toMatch(/(head|thread).*not.*advance|advance.*not|isError.*true/);
|
|
256
|
+
});
|
|
257
|
+
test("documents the @uwf/thread-failed variable for tracking failed attempts across runs", () => {
|
|
258
|
+
expect(text).toContain("@uwf/thread-failed/");
|
|
259
|
+
});
|
|
260
|
+
test("explains MAX_FRONTMATTER_RETRIES (2) before persisting the error step", () => {
|
|
261
|
+
expect(text).toMatch(/2\s*(retries?|attempts?|frontmatter)/i);
|
|
262
|
+
});
|
|
263
|
+
// ── Item 7 — Realistic run() skeleton ───────────────────────────────
|
|
264
|
+
test("Quick Start run() builds prompt via helpers (not empty comments)", () => {
|
|
265
|
+
expect(text).toMatch(/buildRolePrompt|buildContinuationPrompt|buildThreadProgress/);
|
|
266
|
+
});
|
|
267
|
+
test("Quick Start run() returns all 5 AgentRunResult fields", () => {
|
|
268
|
+
expect(text).toContain("assembledPrompt");
|
|
269
|
+
expect(text).toContain("usage");
|
|
270
|
+
expect(text).toContain("detailHash");
|
|
271
|
+
expect(text).toContain("sessionId");
|
|
272
|
+
});
|
|
273
|
+
test("documents Usage type fields turns/inputTokens/outputTokens/duration", () => {
|
|
274
|
+
expect(text).toContain("inputTokens");
|
|
275
|
+
expect(text).toContain("outputTokens");
|
|
276
|
+
expect(text).toMatch(/turns/);
|
|
277
|
+
expect(text).toContain("duration");
|
|
278
|
+
});
|
|
279
|
+
test("Quick Start example does NOT contain the placeholder stub `// 1. Build your prompt from ctx`", () => {
|
|
280
|
+
expect(text).not.toMatch(/\/\/\s*1\.\s*Build your prompt from ctx\b/);
|
|
281
|
+
});
|
|
282
|
+
// ── Item 8 — isFirstVisit ───────────────────────────────────────────
|
|
283
|
+
test("explains isFirstVisit semantics", () => {
|
|
284
|
+
expect(text).toContain("isFirstVisit");
|
|
285
|
+
expect(lower).toMatch(/isfirstvisit.*(true|false).*(role|appeared|run|history)/);
|
|
286
|
+
});
|
|
287
|
+
test("explains the first-visit / re-entry branching pattern", () => {
|
|
288
|
+
expect(lower).toMatch(/(first[- ]?visit|isfirstvisit)[\s\S]*(re-?entry|resume)/);
|
|
289
|
+
});
|
|
290
|
+
// ── Item 9 — Fast path jargon explained ─────────────────────────────
|
|
291
|
+
test("introduces frontmatter extraction concept before the symbol name", () => {
|
|
292
|
+
const idxConcept = lower.search(/frontmatter extraction|extract.*frontmatter|parse.*frontmatter/);
|
|
293
|
+
const idxSymbol = text.indexOf("tryFrontmatterFastPath");
|
|
294
|
+
if (idxSymbol !== -1) {
|
|
295
|
+
expect(idxConcept).toBeGreaterThanOrEqual(0);
|
|
296
|
+
expect(idxConcept).toBeLessThan(idxSymbol);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
test("does not use the bare term 'fast path' without an explanation in the surrounding 200 chars", () => {
|
|
300
|
+
const re = /fast[- ]?path/gi;
|
|
301
|
+
let m = re.exec(text);
|
|
302
|
+
while (m !== null) {
|
|
303
|
+
const window = text.slice(Math.max(0, m.index - 200), m.index + 200).toLowerCase();
|
|
304
|
+
expect(window).toMatch(/extract|parse|attempt|try|interpret/);
|
|
305
|
+
m = re.exec(text);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
// ── Item 10 — No undefined schema variables ─────────────────────────
|
|
309
|
+
test("does not reference an undefined `textSchema` in the code samples", () => {
|
|
310
|
+
const idx = text.indexOf("textSchema");
|
|
311
|
+
if (idx !== -1) {
|
|
312
|
+
const window = text.slice(Math.max(0, idx - 200), idx + 200);
|
|
313
|
+
expect(window).toMatch(/registerAgentSchemas|schemas\.text|putSchema|TEXT_SCHEMA/);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
test("does not reference an undefined `detailSchema` in the code samples", () => {
|
|
317
|
+
const idx = text.indexOf("detailSchema");
|
|
318
|
+
if (idx !== -1) {
|
|
319
|
+
const window = text.slice(Math.max(0, idx - 200), idx + 200);
|
|
320
|
+
expect(window).toMatch(/registerAgentSchemas|schemas|putSchema/);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
test("Storing Session Detail section uses real APIs (storeBuiltinDetail / storeClaudeCodeDetail or store.cas.put with a registered schema)", () => {
|
|
324
|
+
expect(text).toMatch(/store\.cas\.put|storeBuiltinDetail|storeClaudeCodeDetail|registerAgentSchemas/);
|
|
325
|
+
});
|
|
326
|
+
// ── Cross-cutting structural tests ──────────────────────────────────
|
|
327
|
+
test("AdapterOutput JSON envelope (not just step hash) is documented as the stdout contract", () => {
|
|
328
|
+
expect(text).toContain("AdapterOutput");
|
|
329
|
+
expect(lower).toMatch(/json.*stdout|stdout.*json/);
|
|
330
|
+
expect(text).toContain("isError");
|
|
331
|
+
expect(text).toContain("errorMessage");
|
|
332
|
+
});
|
|
333
|
+
test("documents AgentContext storageRoot and casDir fields", () => {
|
|
334
|
+
expect(text).toContain("storageRoot");
|
|
335
|
+
expect(text).toContain("casDir");
|
|
336
|
+
});
|
|
337
|
+
test("documents UWF_HOME / OCAS_HOME env propagation from CLI to adapter", () => {
|
|
338
|
+
expect(text).toContain("UWF_HOME");
|
|
339
|
+
expect(text).toContain("OCAS_HOME");
|
|
340
|
+
});
|
|
341
|
+
test("Existing Adapters table still lists hermes, builtin, claude-code", () => {
|
|
342
|
+
expect(text).toContain("uwf-hermes");
|
|
343
|
+
expect(text).toContain("uwf-builtin");
|
|
344
|
+
expect(text).toContain("uwf-claude-code");
|
|
345
|
+
});
|
|
346
|
+
test("Checklist now includes fork, cleanup, $SUSPEND, and adapter-owned LLM config items", () => {
|
|
347
|
+
const checklistIdx = text.search(/##\s+Checklist/);
|
|
348
|
+
expect(checklistIdx).toBeGreaterThan(-1);
|
|
349
|
+
const checklist = text.slice(checklistIdx);
|
|
350
|
+
expect(checklist).toContain("fork");
|
|
351
|
+
expect(checklist).toContain("cleanup");
|
|
352
|
+
expect(checklist).toContain("$SUSPEND");
|
|
353
|
+
expect(checklist.toLowerCase()).toMatch(/llm config|agents\/.+\.yaml|adapter-owned/);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
describe("prompt workflow-authoring — issue #226 edge location field", () => {
|
|
357
|
+
const text = cmdPromptWorkflowAuthoring();
|
|
358
|
+
const lower = text.toLowerCase();
|
|
359
|
+
// ── Group 1 — Field documentation ───────────────────────────────────
|
|
360
|
+
test("documents the location field on graph edges", () => {
|
|
361
|
+
expect(text).toMatch(/^\s*\|\s*`?location`?\s*\|/m);
|
|
362
|
+
expect(text).toMatch(/location[\s\S]{0,200}(working directory|cwd)/i);
|
|
363
|
+
});
|
|
364
|
+
test("documents location as optional with null fallback", () => {
|
|
365
|
+
expect(lower).toMatch(/location[\s\S]{0,300}(null|omitted|optional|default|fall(s| ?back))/i);
|
|
366
|
+
expect(lower).toMatch(/(thread.*cwd|start.*cwd|creation cwd|thread['']?s cwd)/);
|
|
367
|
+
});
|
|
368
|
+
test("documents Liquid template support for location", () => {
|
|
369
|
+
expect(text).toMatch(/location[\s\S]{0,400}\{\{\s*[a-zA-Z_]\w*\s*\}\}/);
|
|
370
|
+
expect(lower).toMatch(/location[\s\S]{0,300}liquid/);
|
|
371
|
+
});
|
|
372
|
+
// ── Group 2 — Inheritance chain ─────────────────────────────────────
|
|
373
|
+
test("documents the cwd inheritance chain end-to-end", () => {
|
|
374
|
+
expect(text).toContain("--cwd");
|
|
375
|
+
expect(text).toMatch(/StartNodePayload\.cwd|start(\.|node\.)?cwd|thread start cwd/i);
|
|
376
|
+
expect(text).toMatch(/Target\.location|edge\s+location|location\s+(field|override)/i);
|
|
377
|
+
expect(text).toMatch(/StepRecord\.cwd|StepNodePayload\.cwd|step(\.|node\.)?cwd|step.*cwd/i);
|
|
378
|
+
const flagIdx = text.indexOf("--cwd");
|
|
379
|
+
const startIdx = text.search(/StartNodePayload\.cwd|start(\.|node\.)?cwd|thread start cwd/i);
|
|
380
|
+
const locIdx = text.search(/Target\.location|edge\s+location|location\s+(field|override)/i);
|
|
381
|
+
const stepIdx = text.search(/StepRecord\.cwd|StepNodePayload\.cwd|step(\.|node\.)?cwd/i);
|
|
382
|
+
expect(flagIdx).toBeGreaterThanOrEqual(0);
|
|
383
|
+
expect(startIdx).toBeGreaterThan(flagIdx);
|
|
384
|
+
expect(locIdx).toBeGreaterThan(startIdx);
|
|
385
|
+
expect(stepIdx).toBeGreaterThan(locIdx);
|
|
386
|
+
});
|
|
387
|
+
test("explains location override is per-step (not per-thread)", () => {
|
|
388
|
+
expect(lower).toMatch(/(each|per).?step|override.*per.*step|step['']?s (working|cwd)/);
|
|
389
|
+
});
|
|
390
|
+
// ── Group 3 — Realistic cross-cwd example ───────────────────────────
|
|
391
|
+
test("includes a YAML example showing location on an edge", () => {
|
|
392
|
+
const yamlBlocks = text.match(/```yaml[\s\S]*?```/g) ?? [];
|
|
393
|
+
const hasLocationEdge = yamlBlocks.some((b) => /graph\s*:/.test(b) && /^\s*location\s*:/m.test(b));
|
|
394
|
+
expect(hasLocationEdge).toBe(true);
|
|
395
|
+
});
|
|
396
|
+
test("example demonstrates cross-cwd execution with a Liquid-templated path", () => {
|
|
397
|
+
const yamlBlocks = text.match(/```yaml[\s\S]*?```/g) ?? [];
|
|
398
|
+
const hasCrossCwdExample = yamlBlocks.some((b) => /location\s*:\s*['"]?\{\{\s*[a-zA-Z_]\w*\s*\}\}/m.test(b));
|
|
399
|
+
expect(hasCrossCwdExample).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
test("example narrates a realistic scenario", () => {
|
|
402
|
+
expect(lower).toMatch(/(clone|checkout|dispatch|cross[- ]repo|different (repo|directory|working directory|cwd))/);
|
|
403
|
+
});
|
|
404
|
+
// ── Group 4 — Structural placement ──────────────────────────────────
|
|
405
|
+
test("location documentation appears under the Graph Routing section", () => {
|
|
406
|
+
const graphIdx = text.indexOf("## Graph Routing");
|
|
407
|
+
expect(graphIdx).toBeGreaterThanOrEqual(0);
|
|
408
|
+
const after = text.slice(graphIdx);
|
|
409
|
+
const localLocIdx = after.search(/\blocation\b/i);
|
|
410
|
+
expect(localLocIdx).toBeGreaterThanOrEqual(0);
|
|
411
|
+
const nextHeadingIdx = after.slice(1).search(/\n## /);
|
|
412
|
+
expect(localLocIdx).toBeLessThan(nextHeadingIdx === -1 ? after.length : nextHeadingIdx + 1);
|
|
413
|
+
});
|
|
414
|
+
test("Target field table still includes role and prompt alongside location", () => {
|
|
415
|
+
expect(text).toMatch(/\|\s*`?role`?\s*\|/m);
|
|
416
|
+
expect(text).toMatch(/\|\s*`?prompt`?\s*\|/m);
|
|
417
|
+
expect(text).toMatch(/\|\s*`?location`?\s*\|/m);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
99
420
|
//# sourceMappingURL=prompt.test.js.map
|