@united-workforce/cli 0.6.1 → 0.8.1
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 +120 -5
- package/dist/.build-fingerprint +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.js +9 -2
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -1
- package/dist/__tests__/broker-prompt.test.d.ts +10 -0
- package/dist/__tests__/broker-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/broker-prompt.test.js +129 -0
- package/dist/__tests__/broker-prompt.test.js.map +1 -0
- package/dist/__tests__/broker-step-active-turns.test.d.ts +20 -0
- package/dist/__tests__/broker-step-active-turns.test.d.ts.map +1 -0
- package/dist/__tests__/broker-step-active-turns.test.js +428 -0
- package/dist/__tests__/broker-step-active-turns.test.js.map +1 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts +13 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts.map +1 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.js +429 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.js.map +1 -0
- package/dist/__tests__/config.test.js +33 -37
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/e2e-broker-step-suspend.test.d.ts +18 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.js +313 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.js.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts +13 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.js +278 -0
- package/dist/__tests__/e2e-broker-step.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts +28 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js +322 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js.map +1 -0
- package/dist/__tests__/log-tag-validity.test.d.ts +2 -0
- package/dist/__tests__/log-tag-validity.test.d.ts.map +1 -0
- package/dist/__tests__/log-tag-validity.test.js +110 -0
- package/dist/__tests__/log-tag-validity.test.js.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.js +35 -23
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.js +5 -2
- package/dist/__tests__/setup-no-llm.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -6
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/step-show-json.test.js +5 -5
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-show-text.test.d.ts +2 -0
- package/dist/__tests__/step-show-text.test.d.ts.map +1 -0
- package/dist/__tests__/step-show-text.test.js +192 -0
- package/dist/__tests__/step-show-text.test.js.map +1 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.d.ts +21 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.js +356 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.js.map +1 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.d.ts +21 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.js +476 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.js.map +1 -0
- package/dist/__tests__/step-turns.test.d.ts +24 -0
- package/dist/__tests__/step-turns.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns.test.js +646 -0
- package/dist/__tests__/step-turns.test.js.map +1 -0
- package/dist/__tests__/store-turn-chain.test.d.ts +2 -0
- package/dist/__tests__/store-turn-chain.test.d.ts.map +1 -0
- package/dist/__tests__/store-turn-chain.test.js +341 -0
- package/dist/__tests__/store-turn-chain.test.js.map +1 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js +3 -3
- package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -1
- package/dist/__tests__/thread-list-limit-offset.test.d.ts +24 -0
- package/dist/__tests__/thread-list-limit-offset.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-limit-offset.test.js +254 -0
- package/dist/__tests__/thread-list-limit-offset.test.js.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +7 -2
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -1
- package/dist/__tests__/thread-poke.test.js +6 -6
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +2 -2
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-suspend-step.test.js +1 -1
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread.test.js +28 -14
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/cli.js +910 -344
- package/dist/cli.js.map +1 -1
- package/dist/commands/broker-step.d.ts +117 -0
- package/dist/commands/broker-step.d.ts.map +1 -0
- package/dist/commands/broker-step.js +654 -0
- package/dist/commands/broker-step.js.map +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +2 -23
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +43 -51
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +6 -4
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +24 -27
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +54 -6
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +484 -134
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +4 -0
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +77 -151
- package/dist/commands/thread.js.map +1 -1
- package/dist/output-mappers.d.ts +8 -0
- package/dist/output-mappers.d.ts.map +1 -1
- package/dist/output-mappers.js +72 -18
- package/dist/output-mappers.js.map +1 -1
- package/dist/schemas.d.ts +3 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +17 -3
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +147 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +254 -1
- package/dist/store.js.map +1 -1
- package/dist/text-renderers.d.ts.map +1 -1
- package/dist/text-renderers.js +27 -2
- package/dist/text-renderers.js.map +1 -1
- package/package.json +7 -5
- package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
- package/src/__tests__/broker-prompt.test.ts +142 -0
- package/src/__tests__/broker-step-active-turns.test.ts +509 -0
- package/src/__tests__/broker-step-turn-chain-phase2.test.ts +525 -0
- package/src/__tests__/config.test.ts +35 -39
- package/src/__tests__/e2e-broker-step-suspend.test.ts +351 -0
- package/src/__tests__/e2e-broker-step.test.ts +320 -0
- package/src/__tests__/e2e-mock-agent.test.ts +1 -1
- package/src/__tests__/e2e-thread-resume-timeout-suspend.test.ts +360 -0
- package/src/__tests__/log-tag-validity.test.ts +124 -0
- package/src/__tests__/setup-agent-discovery.test.ts +35 -23
- package/src/__tests__/setup-no-llm.test.ts +5 -2
- package/src/__tests__/step-ask.test.ts +9 -6
- package/src/__tests__/step-show-json.test.ts +5 -5
- package/src/__tests__/step-show-text.test.ts +236 -0
- package/src/__tests__/step-turns-cli-subprocess.test.ts +411 -0
- package/src/__tests__/step-turns-panorama-phase3.test.ts +579 -0
- package/src/__tests__/step-turns.test.ts +734 -0
- package/src/__tests__/store-turn-chain.test.ts +386 -0
- package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
- package/src/__tests__/thread-list-limit-offset.test.ts +305 -0
- package/src/__tests__/thread-list-template-ms-date.test.ts +7 -2
- package/src/__tests__/thread-poke.test.ts +6 -6
- package/src/__tests__/thread-resume.test.ts +2 -2
- package/src/__tests__/thread-suspend-step.test.ts +1 -1
- package/src/__tests__/thread.test.ts +29 -15
- package/src/cli.ts +1056 -483
- package/src/commands/broker-step.ts +913 -0
- package/src/commands/config.ts +2 -24
- package/src/commands/prompt.ts +43 -51
- package/src/commands/setup.ts +25 -29
- package/src/commands/step.ts +645 -176
- package/src/commands/thread.ts +87 -192
- package/src/output-mappers.ts +99 -21
- package/src/schemas.ts +32 -2
- package/src/store.ts +297 -2
- package/src/text-renderers.ts +35 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/adapter-json-roundtrip.test.js +0 -160
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.d.ts +0 -2
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.js +0 -79
- package/dist/__tests__/spawn-agent-json.test.js.map +0 -1
- package/src/__tests__/adapter-json-roundtrip.test.ts +0 -193
- package/src/__tests__/spawn-agent-json.test.ts +0 -100
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #423 — CLI subprocess integration test for `uwf step turns`.
|
|
3
|
+
*
|
|
4
|
+
* This test exercises the full CLI invocation path via subprocess (`execFileSync`),
|
|
5
|
+
* catching regressions that function-level tests cannot detect:
|
|
6
|
+
* - Argument parsing (yargs configuration)
|
|
7
|
+
* - Output formatting at the command boundary
|
|
8
|
+
* - Environment variable handling (UWF_HOME, OCAS_HOME)
|
|
9
|
+
* - Exit code behavior
|
|
10
|
+
*
|
|
11
|
+
* The test covers a RECURRING ROLE scenario (developer → reviewer → developer)
|
|
12
|
+
* which was the root cause of #412 and is the most likely to regress.
|
|
13
|
+
*
|
|
14
|
+
* Existing function-level tests (step-turns.test.ts, step-turns-panorama-phase3.test.ts)
|
|
15
|
+
* verify `cmdStepTurns` directly — this test does NOT duplicate those; it only
|
|
16
|
+
* adds the missing subprocess/CLI-level coverage.
|
|
17
|
+
*
|
|
18
|
+
* Follows the pattern established in thread-start-cwd-cli.test.ts.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { putSchema } from "@ocas/core";
|
|
27
|
+
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
28
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
29
|
+
import {
|
|
30
|
+
createUwfStore,
|
|
31
|
+
setActiveTurnHead,
|
|
32
|
+
setThread,
|
|
33
|
+
type UwfStore,
|
|
34
|
+
writeStepStart,
|
|
35
|
+
writeTurnNode,
|
|
36
|
+
} from "../store.js";
|
|
37
|
+
|
|
38
|
+
// ── test setup ───────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const THREAD_ID = "06FD9WEG5BH7C8JPD04X4184E4" as ThreadId;
|
|
41
|
+
|
|
42
|
+
const DETAIL_SCHEMA = {
|
|
43
|
+
title: "broker-detail-cli-test",
|
|
44
|
+
type: "object" as const,
|
|
45
|
+
required: ["sessionId", "duration", "turnCount"],
|
|
46
|
+
properties: {
|
|
47
|
+
sessionId: { type: "string" as const },
|
|
48
|
+
duration: { type: "integer" as const },
|
|
49
|
+
turnCount: { type: "integer" as const },
|
|
50
|
+
},
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let tmpDir: string;
|
|
55
|
+
let storageRoot: string;
|
|
56
|
+
let casDir: string;
|
|
57
|
+
let savedUwfHome: string | undefined;
|
|
58
|
+
let savedOcasHome: string | undefined;
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
savedUwfHome = process.env.UWF_HOME;
|
|
62
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
63
|
+
|
|
64
|
+
tmpDir = join(tmpdir(), `uwf-step-turns-cli-${Date.now()}`);
|
|
65
|
+
storageRoot = join(tmpDir, "storage");
|
|
66
|
+
casDir = join(tmpDir, "cas");
|
|
67
|
+
await mkdir(storageRoot, { recursive: true });
|
|
68
|
+
await mkdir(casDir, { recursive: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(async () => {
|
|
72
|
+
if (savedUwfHome === undefined) delete process.env.UWF_HOME;
|
|
73
|
+
else process.env.UWF_HOME = savedUwfHome;
|
|
74
|
+
if (savedOcasHome === undefined) delete process.env.OCAS_HOME;
|
|
75
|
+
else process.env.OCAS_HOME = savedOcasHome;
|
|
76
|
+
|
|
77
|
+
if (tmpDir) {
|
|
78
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Seed a thread with a recurring role pattern: developer → reviewer → developer.
|
|
84
|
+
* This scenario exercises the #412 fix (owner-based segmentation) and ensures
|
|
85
|
+
* the CLI correctly renders separate segments for the same role.
|
|
86
|
+
*
|
|
87
|
+
* Returns the thread ID for CLI invocation.
|
|
88
|
+
*/
|
|
89
|
+
async function seedRecurringRoleThread(uwf: UwfStore): Promise<ThreadId> {
|
|
90
|
+
// Create workflow and start node
|
|
91
|
+
const workflowHash = uwf.store.cas.put(uwf.schemas.workflow, {
|
|
92
|
+
version: 1,
|
|
93
|
+
name: "recurring-role-test",
|
|
94
|
+
description: "test workflow for recurring role CLI test",
|
|
95
|
+
roles: {},
|
|
96
|
+
graph: {},
|
|
97
|
+
}) as CasRef;
|
|
98
|
+
const startHash = uwf.store.cas.put(uwf.schemas.startNode, {
|
|
99
|
+
workflow: workflowHash,
|
|
100
|
+
prompt: "implement feature X",
|
|
101
|
+
cwd: "/tmp/test",
|
|
102
|
+
}) as CasRef;
|
|
103
|
+
|
|
104
|
+
const detailSchemaHash = putSchema(uwf.store, DETAIL_SCHEMA);
|
|
105
|
+
const outputHash = uwf.store.cas.put(uwf.schemas.text, "output") as CasRef;
|
|
106
|
+
|
|
107
|
+
// Step 1: developer round 1 — 2 turns
|
|
108
|
+
const stepStart1 = writeStepStart(uwf, {
|
|
109
|
+
role: "developer",
|
|
110
|
+
edgePrompt: "Implement feature X",
|
|
111
|
+
stepIndex: 0,
|
|
112
|
+
prev: null,
|
|
113
|
+
start: startHash,
|
|
114
|
+
startedAtMs: 1000,
|
|
115
|
+
cwd: "/tmp/test",
|
|
116
|
+
});
|
|
117
|
+
const t1 = writeTurnNode(uwf, {
|
|
118
|
+
role: "assistant",
|
|
119
|
+
content: "DEV_R1_TURN_1: Reading requirements...",
|
|
120
|
+
prev: null,
|
|
121
|
+
owner: stepStart1,
|
|
122
|
+
});
|
|
123
|
+
const t2 = writeTurnNode(uwf, {
|
|
124
|
+
role: "assistant",
|
|
125
|
+
content: "DEV_R1_TURN_2: Implementation complete.",
|
|
126
|
+
prev: t1,
|
|
127
|
+
owner: stepStart1,
|
|
128
|
+
});
|
|
129
|
+
const detail1 = uwf.store.cas.put(detailSchemaHash, {
|
|
130
|
+
sessionId: "ses_dev_r1",
|
|
131
|
+
duration: 5,
|
|
132
|
+
turnCount: 2,
|
|
133
|
+
}) as CasRef;
|
|
134
|
+
const stepNode1 = uwf.store.cas.put(uwf.schemas.stepNode, {
|
|
135
|
+
start: startHash,
|
|
136
|
+
prev: null,
|
|
137
|
+
role: "developer",
|
|
138
|
+
output: outputHash,
|
|
139
|
+
detail: detail1,
|
|
140
|
+
agent: "test-agent",
|
|
141
|
+
edgePrompt: "Implement feature X",
|
|
142
|
+
startedAtMs: 1000,
|
|
143
|
+
completedAtMs: 2000,
|
|
144
|
+
cwd: "/tmp/test",
|
|
145
|
+
assembledPrompt: null,
|
|
146
|
+
usage: null,
|
|
147
|
+
previousAttempts: null,
|
|
148
|
+
}) as CasRef;
|
|
149
|
+
|
|
150
|
+
// Step 2: reviewer — 2 turns
|
|
151
|
+
const stepStart2 = writeStepStart(uwf, {
|
|
152
|
+
role: "reviewer",
|
|
153
|
+
edgePrompt: "Review the implementation",
|
|
154
|
+
stepIndex: 1,
|
|
155
|
+
prev: stepStart1,
|
|
156
|
+
start: startHash,
|
|
157
|
+
startedAtMs: 3000,
|
|
158
|
+
cwd: "/tmp/test",
|
|
159
|
+
});
|
|
160
|
+
const t3 = writeTurnNode(uwf, {
|
|
161
|
+
role: "assistant",
|
|
162
|
+
content: "REV_TURN_1: Reviewing code quality...",
|
|
163
|
+
prev: t2,
|
|
164
|
+
owner: stepStart2,
|
|
165
|
+
});
|
|
166
|
+
const t4 = writeTurnNode(uwf, {
|
|
167
|
+
role: "assistant",
|
|
168
|
+
content: "REV_TURN_2: Found issues, requesting changes.",
|
|
169
|
+
prev: t3,
|
|
170
|
+
owner: stepStart2,
|
|
171
|
+
});
|
|
172
|
+
const detail2 = uwf.store.cas.put(detailSchemaHash, {
|
|
173
|
+
sessionId: "ses_rev",
|
|
174
|
+
duration: 4,
|
|
175
|
+
turnCount: 2,
|
|
176
|
+
}) as CasRef;
|
|
177
|
+
const stepNode2 = uwf.store.cas.put(uwf.schemas.stepNode, {
|
|
178
|
+
start: startHash,
|
|
179
|
+
prev: stepNode1,
|
|
180
|
+
role: "reviewer",
|
|
181
|
+
output: outputHash,
|
|
182
|
+
detail: detail2,
|
|
183
|
+
agent: "test-agent",
|
|
184
|
+
edgePrompt: "Review the implementation",
|
|
185
|
+
startedAtMs: 3000,
|
|
186
|
+
completedAtMs: 4000,
|
|
187
|
+
cwd: "/tmp/test",
|
|
188
|
+
assembledPrompt: null,
|
|
189
|
+
usage: null,
|
|
190
|
+
previousAttempts: null,
|
|
191
|
+
}) as CasRef;
|
|
192
|
+
|
|
193
|
+
// Step 3: developer round 2 — 3 turns
|
|
194
|
+
const stepStart3 = writeStepStart(uwf, {
|
|
195
|
+
role: "developer",
|
|
196
|
+
edgePrompt: "Address reviewer feedback",
|
|
197
|
+
stepIndex: 2,
|
|
198
|
+
prev: stepStart2,
|
|
199
|
+
start: startHash,
|
|
200
|
+
startedAtMs: 5000,
|
|
201
|
+
cwd: "/tmp/test",
|
|
202
|
+
});
|
|
203
|
+
const t5 = writeTurnNode(uwf, {
|
|
204
|
+
role: "assistant",
|
|
205
|
+
content: "DEV_R2_TURN_1: Reading feedback...",
|
|
206
|
+
prev: t4,
|
|
207
|
+
owner: stepStart3,
|
|
208
|
+
});
|
|
209
|
+
const t6 = writeTurnNode(uwf, {
|
|
210
|
+
role: "assistant",
|
|
211
|
+
content: "DEV_R2_TURN_2: Making requested changes...",
|
|
212
|
+
prev: t5,
|
|
213
|
+
owner: stepStart3,
|
|
214
|
+
});
|
|
215
|
+
const t7 = writeTurnNode(uwf, {
|
|
216
|
+
role: "assistant",
|
|
217
|
+
content: "DEV_R2_TURN_3: Changes complete, ready for re-review.",
|
|
218
|
+
prev: t6,
|
|
219
|
+
owner: stepStart3,
|
|
220
|
+
});
|
|
221
|
+
const detail3 = uwf.store.cas.put(detailSchemaHash, {
|
|
222
|
+
sessionId: "ses_dev_r2",
|
|
223
|
+
duration: 6,
|
|
224
|
+
turnCount: 3,
|
|
225
|
+
}) as CasRef;
|
|
226
|
+
const stepNode3 = uwf.store.cas.put(uwf.schemas.stepNode, {
|
|
227
|
+
start: startHash,
|
|
228
|
+
prev: stepNode2,
|
|
229
|
+
role: "developer",
|
|
230
|
+
output: outputHash,
|
|
231
|
+
detail: detail3,
|
|
232
|
+
agent: "test-agent",
|
|
233
|
+
edgePrompt: "Address reviewer feedback",
|
|
234
|
+
startedAtMs: 5000,
|
|
235
|
+
completedAtMs: 6000,
|
|
236
|
+
cwd: "/tmp/test",
|
|
237
|
+
assembledPrompt: null,
|
|
238
|
+
usage: null,
|
|
239
|
+
previousAttempts: null,
|
|
240
|
+
}) as CasRef;
|
|
241
|
+
|
|
242
|
+
// Set thread state — all steps completed
|
|
243
|
+
setThread(uwf.varStore, THREAD_ID, {
|
|
244
|
+
head: stepNode3,
|
|
245
|
+
status: "idle",
|
|
246
|
+
suspendedRole: null,
|
|
247
|
+
suspendMessage: null,
|
|
248
|
+
completedAt: null,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Set active-turn-head to the last turn
|
|
252
|
+
setActiveTurnHead(uwf.store, THREAD_ID, t7);
|
|
253
|
+
|
|
254
|
+
return THREAD_ID;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── spec: step-turns-cli-subprocess-recurring-role.md ────────────────────────
|
|
258
|
+
|
|
259
|
+
describe("uwf step turns CLI subprocess integration (#423)", () => {
|
|
260
|
+
test("recurring role scenario (developer→reviewer→developer) via subprocess", async () => {
|
|
261
|
+
// Setup: seed the store with recurring role thread
|
|
262
|
+
process.env.OCAS_HOME = casDir;
|
|
263
|
+
const uwf = await createUwfStore(storageRoot);
|
|
264
|
+
const threadId = await seedRecurringRoleThread(uwf);
|
|
265
|
+
|
|
266
|
+
// Build paths to CLI binary
|
|
267
|
+
const pkgRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
268
|
+
const uwfBin = join(pkgRoot, "dist", "cli.js");
|
|
269
|
+
|
|
270
|
+
// Invoke CLI via subprocess
|
|
271
|
+
const output = execFileSync(process.execPath, [uwfBin, "step", "turns", threadId], {
|
|
272
|
+
env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir },
|
|
273
|
+
encoding: "utf8",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Exit code 0 is implicit — execFileSync throws on non-zero exit
|
|
277
|
+
|
|
278
|
+
// Verify: 3 step groups in chronological order
|
|
279
|
+
const groups = output.match(/## (developer|reviewer)/g);
|
|
280
|
+
expect(groups).toEqual(["## developer", "## reviewer", "## developer"]);
|
|
281
|
+
|
|
282
|
+
// Verify: Both developer segments present (NOT collapsed)
|
|
283
|
+
const firstDevIdx = output.indexOf("## developer");
|
|
284
|
+
const reviewerIdx = output.indexOf("## reviewer");
|
|
285
|
+
const secondDevIdx = output.indexOf("## developer", reviewerIdx + 1);
|
|
286
|
+
expect(firstDevIdx).toBeLessThan(reviewerIdx);
|
|
287
|
+
expect(reviewerIdx).toBeLessThan(secondDevIdx);
|
|
288
|
+
|
|
289
|
+
// Verify: Each segment's turns are correctly attributed (no cross-segment leakage)
|
|
290
|
+
const devR1Section = output.slice(firstDevIdx, reviewerIdx);
|
|
291
|
+
const revSection = output.slice(reviewerIdx, secondDevIdx);
|
|
292
|
+
const devR2Section = output.slice(secondDevIdx);
|
|
293
|
+
|
|
294
|
+
// Developer round 1: 2 turns
|
|
295
|
+
expect(devR1Section).toContain("DEV_R1_TURN_1");
|
|
296
|
+
expect(devR1Section).toContain("DEV_R1_TURN_2");
|
|
297
|
+
expect(devR1Section).not.toContain("REV_TURN");
|
|
298
|
+
expect(devR1Section).not.toContain("DEV_R2_TURN");
|
|
299
|
+
|
|
300
|
+
// Reviewer: 2 turns
|
|
301
|
+
expect(revSection).toContain("REV_TURN_1");
|
|
302
|
+
expect(revSection).toContain("REV_TURN_2");
|
|
303
|
+
expect(revSection).not.toContain("DEV_R1_TURN");
|
|
304
|
+
expect(revSection).not.toContain("DEV_R2_TURN");
|
|
305
|
+
|
|
306
|
+
// Developer round 2: 3 turns
|
|
307
|
+
expect(devR2Section).toContain("DEV_R2_TURN_1");
|
|
308
|
+
expect(devR2Section).toContain("DEV_R2_TURN_2");
|
|
309
|
+
expect(devR2Section).toContain("DEV_R2_TURN_3");
|
|
310
|
+
expect(devR2Section).not.toContain("DEV_R1_TURN");
|
|
311
|
+
expect(devR2Section).not.toContain("REV_TURN");
|
|
312
|
+
|
|
313
|
+
// Verify: All 7 turns rendered with global numbering (Turn 1 through Turn 7)
|
|
314
|
+
expect(output).toContain("## Turn 1");
|
|
315
|
+
expect(output).toContain("## Turn 2");
|
|
316
|
+
expect(output).toContain("## Turn 3");
|
|
317
|
+
expect(output).toContain("## Turn 4");
|
|
318
|
+
expect(output).toContain("## Turn 5");
|
|
319
|
+
expect(output).toContain("## Turn 6");
|
|
320
|
+
expect(output).toContain("## Turn 7");
|
|
321
|
+
|
|
322
|
+
// Verify: All steps marked as completed (✓)
|
|
323
|
+
const checkmarks = output.match(/✓/g);
|
|
324
|
+
expect(checkmarks).toHaveLength(3);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("CLI accepts --role filter via subprocess", async () => {
|
|
328
|
+
// Setup
|
|
329
|
+
process.env.OCAS_HOME = casDir;
|
|
330
|
+
const uwf = await createUwfStore(storageRoot);
|
|
331
|
+
const threadId = await seedRecurringRoleThread(uwf);
|
|
332
|
+
|
|
333
|
+
const pkgRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
334
|
+
const uwfBin = join(pkgRoot, "dist", "cli.js");
|
|
335
|
+
|
|
336
|
+
// Invoke CLI with --role developer
|
|
337
|
+
const output = execFileSync(
|
|
338
|
+
process.execPath,
|
|
339
|
+
[uwfBin, "step", "turns", threadId, "--role", "developer"],
|
|
340
|
+
{
|
|
341
|
+
env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir },
|
|
342
|
+
encoding: "utf8",
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Verify: Only developer groups present
|
|
347
|
+
const groups = output.match(/## (developer|reviewer)/g);
|
|
348
|
+
expect(groups).toEqual(["## developer", "## developer"]);
|
|
349
|
+
|
|
350
|
+
// Verify: No reviewer turns
|
|
351
|
+
expect(output).not.toContain("REV_TURN");
|
|
352
|
+
|
|
353
|
+
// Verify: Both developer segment turns present
|
|
354
|
+
expect(output).toContain("DEV_R1_TURN");
|
|
355
|
+
expect(output).toContain("DEV_R2_TURN");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("CLI accepts --limit and --offset pagination via subprocess", async () => {
|
|
359
|
+
// Setup
|
|
360
|
+
process.env.OCAS_HOME = casDir;
|
|
361
|
+
const uwf = await createUwfStore(storageRoot);
|
|
362
|
+
const threadId = await seedRecurringRoleThread(uwf);
|
|
363
|
+
|
|
364
|
+
const pkgRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
365
|
+
const uwfBin = join(pkgRoot, "dist", "cli.js");
|
|
366
|
+
|
|
367
|
+
// Invoke CLI with --offset 2 --limit 3 (turns 3, 4, 5)
|
|
368
|
+
const output = execFileSync(
|
|
369
|
+
process.execPath,
|
|
370
|
+
[uwfBin, "step", "turns", threadId, "--offset", "2", "--limit", "3"],
|
|
371
|
+
{
|
|
372
|
+
env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir },
|
|
373
|
+
encoding: "utf8",
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// Verify: Global indices Turn 3, Turn 4, Turn 5 (1-based display)
|
|
378
|
+
expect(output).toContain("## Turn 3");
|
|
379
|
+
expect(output).toContain("## Turn 4");
|
|
380
|
+
expect(output).toContain("## Turn 5");
|
|
381
|
+
|
|
382
|
+
// Verify: Turns 1, 2 are skipped
|
|
383
|
+
expect(output).not.toContain("## Turn 1");
|
|
384
|
+
expect(output).not.toContain("## Turn 2");
|
|
385
|
+
|
|
386
|
+
// Verify: Turns 6, 7 are beyond the limit
|
|
387
|
+
expect(output).not.toContain("## Turn 6");
|
|
388
|
+
expect(output).not.toContain("## Turn 7");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("CLI exits with error for invalid thread ID", async () => {
|
|
392
|
+
const pkgRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
393
|
+
const uwfBin = join(pkgRoot, "dist", "cli.js");
|
|
394
|
+
|
|
395
|
+
// Invoke CLI with a non-existent thread ID
|
|
396
|
+
const invalidThreadId = "06FDINVALIDTHREADID000000";
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
execFileSync(process.execPath, [uwfBin, "step", "turns", invalidThreadId], {
|
|
400
|
+
env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir },
|
|
401
|
+
encoding: "utf8",
|
|
402
|
+
});
|
|
403
|
+
// If we get here, the command didn't fail as expected
|
|
404
|
+
expect.fail("Expected CLI to exit with non-zero code for invalid thread ID");
|
|
405
|
+
} catch (err) {
|
|
406
|
+
// execFileSync throws on non-zero exit — this is expected
|
|
407
|
+
const error = err as { status: number; stderr?: Buffer };
|
|
408
|
+
expect(error.status).not.toBe(0);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
});
|