@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.
Files changed (167) hide show
  1. package/README.md +120 -5
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__tests__/agent-resolution-llm-free.test.js +9 -2
  4. package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -1
  5. package/dist/__tests__/broker-prompt.test.d.ts +10 -0
  6. package/dist/__tests__/broker-prompt.test.d.ts.map +1 -0
  7. package/dist/__tests__/broker-prompt.test.js +129 -0
  8. package/dist/__tests__/broker-prompt.test.js.map +1 -0
  9. package/dist/__tests__/broker-step-active-turns.test.d.ts +20 -0
  10. package/dist/__tests__/broker-step-active-turns.test.d.ts.map +1 -0
  11. package/dist/__tests__/broker-step-active-turns.test.js +428 -0
  12. package/dist/__tests__/broker-step-active-turns.test.js.map +1 -0
  13. package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts +13 -0
  14. package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts.map +1 -0
  15. package/dist/__tests__/broker-step-turn-chain-phase2.test.js +429 -0
  16. package/dist/__tests__/broker-step-turn-chain-phase2.test.js.map +1 -0
  17. package/dist/__tests__/config.test.js +33 -37
  18. package/dist/__tests__/config.test.js.map +1 -1
  19. package/dist/__tests__/e2e-broker-step-suspend.test.d.ts +18 -0
  20. package/dist/__tests__/e2e-broker-step-suspend.test.d.ts.map +1 -0
  21. package/dist/__tests__/e2e-broker-step-suspend.test.js +313 -0
  22. package/dist/__tests__/e2e-broker-step-suspend.test.js.map +1 -0
  23. package/dist/__tests__/e2e-broker-step.test.d.ts +13 -0
  24. package/dist/__tests__/e2e-broker-step.test.d.ts.map +1 -0
  25. package/dist/__tests__/e2e-broker-step.test.js +278 -0
  26. package/dist/__tests__/e2e-broker-step.test.js.map +1 -0
  27. package/dist/__tests__/e2e-mock-agent.test.js +1 -1
  28. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  29. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts +28 -0
  30. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts.map +1 -0
  31. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js +322 -0
  32. package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js.map +1 -0
  33. package/dist/__tests__/log-tag-validity.test.d.ts +2 -0
  34. package/dist/__tests__/log-tag-validity.test.d.ts.map +1 -0
  35. package/dist/__tests__/log-tag-validity.test.js +110 -0
  36. package/dist/__tests__/log-tag-validity.test.js.map +1 -0
  37. package/dist/__tests__/setup-agent-discovery.test.js +35 -23
  38. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
  39. package/dist/__tests__/setup-no-llm.test.js +5 -2
  40. package/dist/__tests__/setup-no-llm.test.js.map +1 -1
  41. package/dist/__tests__/step-ask.test.js +9 -6
  42. package/dist/__tests__/step-ask.test.js.map +1 -1
  43. package/dist/__tests__/step-show-json.test.js +5 -5
  44. package/dist/__tests__/step-show-json.test.js.map +1 -1
  45. package/dist/__tests__/step-show-text.test.d.ts +2 -0
  46. package/dist/__tests__/step-show-text.test.d.ts.map +1 -0
  47. package/dist/__tests__/step-show-text.test.js +192 -0
  48. package/dist/__tests__/step-show-text.test.js.map +1 -0
  49. package/dist/__tests__/step-turns-cli-subprocess.test.d.ts +21 -0
  50. package/dist/__tests__/step-turns-cli-subprocess.test.d.ts.map +1 -0
  51. package/dist/__tests__/step-turns-cli-subprocess.test.js +356 -0
  52. package/dist/__tests__/step-turns-cli-subprocess.test.js.map +1 -0
  53. package/dist/__tests__/step-turns-panorama-phase3.test.d.ts +21 -0
  54. package/dist/__tests__/step-turns-panorama-phase3.test.d.ts.map +1 -0
  55. package/dist/__tests__/step-turns-panorama-phase3.test.js +476 -0
  56. package/dist/__tests__/step-turns-panorama-phase3.test.js.map +1 -0
  57. package/dist/__tests__/step-turns.test.d.ts +24 -0
  58. package/dist/__tests__/step-turns.test.d.ts.map +1 -0
  59. package/dist/__tests__/step-turns.test.js +646 -0
  60. package/dist/__tests__/step-turns.test.js.map +1 -0
  61. package/dist/__tests__/store-turn-chain.test.d.ts +2 -0
  62. package/dist/__tests__/store-turn-chain.test.d.ts.map +1 -0
  63. package/dist/__tests__/store-turn-chain.test.js +341 -0
  64. package/dist/__tests__/store-turn-chain.test.js.map +1 -0
  65. package/dist/__tests__/thread-agent-failure-suspended.test.js +3 -3
  66. package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -1
  67. package/dist/__tests__/thread-list-limit-offset.test.d.ts +24 -0
  68. package/dist/__tests__/thread-list-limit-offset.test.d.ts.map +1 -0
  69. package/dist/__tests__/thread-list-limit-offset.test.js +254 -0
  70. package/dist/__tests__/thread-list-limit-offset.test.js.map +1 -0
  71. package/dist/__tests__/thread-list-template-ms-date.test.js +7 -2
  72. package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -1
  73. package/dist/__tests__/thread-poke.test.js +6 -6
  74. package/dist/__tests__/thread-poke.test.js.map +1 -1
  75. package/dist/__tests__/thread-resume.test.js +2 -2
  76. package/dist/__tests__/thread-resume.test.js.map +1 -1
  77. package/dist/__tests__/thread-suspend-step.test.js +1 -1
  78. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  79. package/dist/__tests__/thread.test.js +28 -14
  80. package/dist/__tests__/thread.test.js.map +1 -1
  81. package/dist/cli.js +910 -344
  82. package/dist/cli.js.map +1 -1
  83. package/dist/commands/broker-step.d.ts +117 -0
  84. package/dist/commands/broker-step.d.ts.map +1 -0
  85. package/dist/commands/broker-step.js +654 -0
  86. package/dist/commands/broker-step.js.map +1 -0
  87. package/dist/commands/config.d.ts.map +1 -1
  88. package/dist/commands/config.js +2 -23
  89. package/dist/commands/config.js.map +1 -1
  90. package/dist/commands/prompt.d.ts.map +1 -1
  91. package/dist/commands/prompt.js +43 -51
  92. package/dist/commands/prompt.js.map +1 -1
  93. package/dist/commands/setup.d.ts +6 -4
  94. package/dist/commands/setup.d.ts.map +1 -1
  95. package/dist/commands/setup.js +24 -27
  96. package/dist/commands/setup.js.map +1 -1
  97. package/dist/commands/step.d.ts +54 -6
  98. package/dist/commands/step.d.ts.map +1 -1
  99. package/dist/commands/step.js +484 -134
  100. package/dist/commands/step.js.map +1 -1
  101. package/dist/commands/thread.d.ts +4 -0
  102. package/dist/commands/thread.d.ts.map +1 -1
  103. package/dist/commands/thread.js +77 -151
  104. package/dist/commands/thread.js.map +1 -1
  105. package/dist/output-mappers.d.ts +8 -0
  106. package/dist/output-mappers.d.ts.map +1 -1
  107. package/dist/output-mappers.js +72 -18
  108. package/dist/output-mappers.js.map +1 -1
  109. package/dist/schemas.d.ts +3 -0
  110. package/dist/schemas.d.ts.map +1 -1
  111. package/dist/schemas.js +17 -3
  112. package/dist/schemas.js.map +1 -1
  113. package/dist/store.d.ts +147 -1
  114. package/dist/store.d.ts.map +1 -1
  115. package/dist/store.js +254 -1
  116. package/dist/store.js.map +1 -1
  117. package/dist/text-renderers.d.ts.map +1 -1
  118. package/dist/text-renderers.js +27 -2
  119. package/dist/text-renderers.js.map +1 -1
  120. package/package.json +7 -5
  121. package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
  122. package/src/__tests__/broker-prompt.test.ts +142 -0
  123. package/src/__tests__/broker-step-active-turns.test.ts +509 -0
  124. package/src/__tests__/broker-step-turn-chain-phase2.test.ts +525 -0
  125. package/src/__tests__/config.test.ts +35 -39
  126. package/src/__tests__/e2e-broker-step-suspend.test.ts +351 -0
  127. package/src/__tests__/e2e-broker-step.test.ts +320 -0
  128. package/src/__tests__/e2e-mock-agent.test.ts +1 -1
  129. package/src/__tests__/e2e-thread-resume-timeout-suspend.test.ts +360 -0
  130. package/src/__tests__/log-tag-validity.test.ts +124 -0
  131. package/src/__tests__/setup-agent-discovery.test.ts +35 -23
  132. package/src/__tests__/setup-no-llm.test.ts +5 -2
  133. package/src/__tests__/step-ask.test.ts +9 -6
  134. package/src/__tests__/step-show-json.test.ts +5 -5
  135. package/src/__tests__/step-show-text.test.ts +236 -0
  136. package/src/__tests__/step-turns-cli-subprocess.test.ts +411 -0
  137. package/src/__tests__/step-turns-panorama-phase3.test.ts +579 -0
  138. package/src/__tests__/step-turns.test.ts +734 -0
  139. package/src/__tests__/store-turn-chain.test.ts +386 -0
  140. package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
  141. package/src/__tests__/thread-list-limit-offset.test.ts +305 -0
  142. package/src/__tests__/thread-list-template-ms-date.test.ts +7 -2
  143. package/src/__tests__/thread-poke.test.ts +6 -6
  144. package/src/__tests__/thread-resume.test.ts +2 -2
  145. package/src/__tests__/thread-suspend-step.test.ts +1 -1
  146. package/src/__tests__/thread.test.ts +29 -15
  147. package/src/cli.ts +1056 -483
  148. package/src/commands/broker-step.ts +913 -0
  149. package/src/commands/config.ts +2 -24
  150. package/src/commands/prompt.ts +43 -51
  151. package/src/commands/setup.ts +25 -29
  152. package/src/commands/step.ts +645 -176
  153. package/src/commands/thread.ts +87 -192
  154. package/src/output-mappers.ts +99 -21
  155. package/src/schemas.ts +32 -2
  156. package/src/store.ts +297 -2
  157. package/src/text-renderers.ts +35 -2
  158. package/dist/__tests__/adapter-json-roundtrip.test.d.ts +0 -2
  159. package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +0 -1
  160. package/dist/__tests__/adapter-json-roundtrip.test.js +0 -160
  161. package/dist/__tests__/adapter-json-roundtrip.test.js.map +0 -1
  162. package/dist/__tests__/spawn-agent-json.test.d.ts +0 -2
  163. package/dist/__tests__/spawn-agent-json.test.d.ts.map +0 -1
  164. package/dist/__tests__/spawn-agent-json.test.js +0 -79
  165. package/dist/__tests__/spawn-agent-json.test.js.map +0 -1
  166. package/src/__tests__/adapter-json-roundtrip.test.ts +0 -193
  167. 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
+ });