@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
@@ -299,7 +299,10 @@ function runUwf(
299
299
 
300
300
  // ── Group 1: CLI argument validation ───────────────────────────────────────
301
301
 
302
- describe("uwf step ask - CLI argument validation", () => {
302
+ // Phase 3 (#380): `uwf step ask` is disabled until Phase 4 once the Sumeru
303
+ // broker exposes session-fork APIs. The legacy spawn-agent path was removed,
304
+ // so these tests are skipped until the Phase 4 broker fork primitive lands.
305
+ describe.skip("uwf step ask - CLI argument validation", () => {
303
306
  test("1.1 missing step-hash exits non-zero", async () => {
304
307
  const { casDir } = await setupAskFixture();
305
308
  const result = runUwf(["step", "ask"], casDir);
@@ -325,7 +328,7 @@ describe("uwf step ask - CLI argument validation", () => {
325
328
 
326
329
  // ── Group 2: CAS validation errors ────────────────────────────────────────
327
330
 
328
- describe("uwf step ask - CAS validation errors", () => {
331
+ describe.skip("uwf step ask - CAS validation errors", () => {
329
332
  test("2.1 non-existent CAS hash exits non-zero with 'not found'", async () => {
330
333
  const { casDir, mockAgentPath } = await setupAskFixture();
331
334
  const result = runUwf(
@@ -360,7 +363,7 @@ describe("uwf step ask - CAS validation errors", () => {
360
363
 
361
364
  // ── Group 3: Successful ask (core behavior) ───────────────────────────────
362
365
 
363
- describe("uwf step ask - successful ask (core)", () => {
366
+ describe.skip("uwf step ask - successful ask (core)", () => {
364
367
  test("3.1 stdout contains agent's response text", async () => {
365
368
  const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
366
369
  const result = runUwf(
@@ -437,7 +440,7 @@ describe("uwf step ask - successful ask (core)", () => {
437
440
 
438
441
  // ── Group 4: Fork cache semantics ─────────────────────────────────────────
439
442
 
440
- describe("uwf step ask - fork cache", { timeout: 15_000 }, () => {
443
+ describe.skip("uwf step ask - fork cache", { timeout: 15_000 }, () => {
441
444
  test("4.1 first ask creates a fork session and caches it", async () => {
442
445
  const { casDir, stepHash, mockAgentPath, forkSessionCapturePath } = await setupAskFixture();
443
446
 
@@ -545,7 +548,7 @@ describe("uwf step ask - fork cache", { timeout: 15_000 }, () => {
545
548
 
546
549
  // ── Group 5: Fallback (agent has no fork support) ─────────────────────────
547
550
 
548
- describe("uwf step ask - fallback path", () => {
551
+ describe.skip("uwf step ask - fallback path", () => {
549
552
  test("5.1 fallback agent (no fork support) still answers via stdout", async () => {
550
553
  // Use a fallback agent that ONLY supports `ask` mode without ever being asked
551
554
  // to fork. The CLI should detect missing fork support and inject context instead.
@@ -648,7 +651,7 @@ esac
648
651
 
649
652
  // ── Group 6: Agent resolution ─────────────────────────────────────────────
650
653
 
651
- describe("uwf step ask - agent resolution", () => {
654
+ describe.skip("uwf step ask - agent resolution", () => {
652
655
  test("6.1 without --agent flag, agent is resolved from step's agent field", async () => {
653
656
  // Step's agent field points at mockAgentPath by default.
654
657
  const { casDir, stepHash, modeCapturePath, promptCapturePath } = await setupAskFixture();
@@ -169,7 +169,7 @@ describe("cmdStepShow JSON serialization", () => {
169
169
  expect(jsonOutput).toContain("\\n");
170
170
 
171
171
  const parsed = JSON.parse(jsonOutput);
172
- expect(parsed.turns[0].toolCalls[0].args).toContain("\n");
172
+ expect(parsed.detail.turns[0].toolCalls[0].args).toContain("\n");
173
173
  });
174
174
 
175
175
  test("escapes tabs in tool call args", async () => {
@@ -239,7 +239,7 @@ describe("cmdStepShow JSON serialization", () => {
239
239
 
240
240
  expect(() => JSON.parse(jsonOutput)).not.toThrow();
241
241
  const parsed = JSON.parse(jsonOutput);
242
- expect(parsed.turns).toBeDefined();
242
+ expect(parsed.detail.turns).toBeDefined();
243
243
  });
244
244
 
245
245
  test("handles Unicode control characters", async () => {
@@ -291,7 +291,7 @@ describe("cmdStepShow JSON serialization", () => {
291
291
 
292
292
  expect(() => JSON.parse(jsonOutput)).not.toThrow();
293
293
  const parsed = JSON.parse(jsonOutput);
294
- expect(parsed.turns).toHaveLength(2);
294
+ expect(parsed.detail.turns).toHaveLength(2);
295
295
  });
296
296
 
297
297
  test("YAML output format is unaffected", async () => {
@@ -333,7 +333,7 @@ describe("cmdStepShow JSON serialization", () => {
333
333
 
334
334
  expect(() => JSON.parse(jsonOutput)).not.toThrow();
335
335
  const parsed = JSON.parse(jsonOutput);
336
- expect(parsed.turns).toBeDefined();
336
+ expect(parsed.detail.turns).toBeDefined();
337
337
  });
338
338
 
339
339
  test("handles large step with multiple tool calls", async () => {
@@ -369,6 +369,6 @@ describe("cmdStepShow JSON serialization", () => {
369
369
  expect(() => JSON.parse(jsonOutput)).not.toThrow();
370
370
 
371
371
  const parsed = JSON.parse(jsonOutput);
372
- expect(parsed.turns).toHaveLength(25);
372
+ expect(parsed.detail.turns).toHaveLength(25);
373
373
  });
374
374
  });
@@ -0,0 +1,236 @@
1
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { bootstrap, type Hash, type JSONSchema, putSchema } from "@ocas/core";
5
+ import { openStore } from "@ocas/fs";
6
+ import type { CasRef, StepNodePayload } from "@united-workforce/protocol";
7
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
8
+ import { cmdStepShow } from "../commands/step.js";
9
+ import { writeEnvelope } from "../format.js";
10
+ import { toStepDetailPayload } from "../output-mappers.js";
11
+ import { registerUwfSchemas } from "../schemas.js";
12
+ import { createUwfStore } from "../store.js";
13
+
14
+ /**
15
+ * Issue #403 — regression guard for the `step show` **text** path.
16
+ *
17
+ * PR #394 added the `--- Content ---` turn block (plus `Usage` / `Turns`) to
18
+ * `STEP_DETAIL_TEMPLATE` and the `toStepDetailPayload` mapper flattens
19
+ * `detail.turns` into a top-level `turns` array — but no test asserted that the
20
+ * rendered text actually contains the turn bodies. A stale build (e.g. the
21
+ * published `protocol@0.4.0`) or an accidental retarget to `detail.turns` would
22
+ * go undetected.
23
+ *
24
+ * This exercises the full path
25
+ * cmdStepShow → toStepDetailPayload → writeEnvelope(text) → renderEnvelopeText
26
+ * and asserts the rendered text contains `--- Content ---`, each turn's content
27
+ * substring, and the `Turns N` line. The sibling JSON contract lives in
28
+ * `step-show-json.test.ts`; the protocol-level template invariant lives in
29
+ * `packages/protocol/src/__tests__/output-templates-step-detail.test.ts`.
30
+ */
31
+
32
+ const TURN_SCHEMA: JSONSchema = {
33
+ title: "test-turn",
34
+ type: "object",
35
+ required: ["index", "role", "content"],
36
+ properties: {
37
+ index: { type: "integer" },
38
+ role: { type: "string", enum: ["assistant", "tool"] },
39
+ content: { type: "string" },
40
+ toolCalls: {
41
+ anyOf: [
42
+ {
43
+ type: "array",
44
+ items: {
45
+ type: "object",
46
+ required: ["name", "args"],
47
+ properties: {
48
+ name: { type: "string" },
49
+ args: { type: "string" },
50
+ },
51
+ additionalProperties: false,
52
+ },
53
+ },
54
+ { type: "null" },
55
+ ],
56
+ },
57
+ },
58
+ additionalProperties: false,
59
+ };
60
+
61
+ const DETAIL_SCHEMA: JSONSchema = {
62
+ title: "test-detail",
63
+ type: "object",
64
+ required: ["turns"],
65
+ properties: {
66
+ turns: {
67
+ type: "array",
68
+ items: { type: "string", format: "ocas_ref" },
69
+ },
70
+ },
71
+ additionalProperties: false,
72
+ };
73
+
74
+ type TestSetup = {
75
+ store: Awaited<ReturnType<typeof openStore>>;
76
+ schemas: Awaited<ReturnType<typeof registerUwfSchemas>>;
77
+ turnType: Hash;
78
+ detailType: Hash;
79
+ };
80
+
81
+ async function setupTest(casDir: string): Promise<TestSetup> {
82
+ const store = await openStore(casDir);
83
+ await bootstrap(store);
84
+ const schemas = await registerUwfSchemas(store);
85
+ const [turnType, detailType] = await Promise.all([
86
+ putSchema(store, TURN_SCHEMA),
87
+ putSchema(store, DETAIL_SCHEMA),
88
+ ]);
89
+ return { store, schemas, turnType, detailType };
90
+ }
91
+
92
+ async function createTestStep(
93
+ setup: TestSetup,
94
+ turnPayloads: Array<{
95
+ index: number;
96
+ role: string;
97
+ content: string;
98
+ toolCalls: Array<{ name: string; args: string }> | null;
99
+ }>,
100
+ usage: StepNodePayload["usage"],
101
+ ): Promise<CasRef> {
102
+ const { store, schemas, turnType, detailType } = setup;
103
+
104
+ const turnHashes: CasRef[] = [];
105
+ for (const payload of turnPayloads) {
106
+ turnHashes.push(await store.cas.put(turnType, payload));
107
+ }
108
+
109
+ const detailHash = await store.cas.put(detailType, { turns: turnHashes });
110
+ const startHash = await store.cas.put(schemas.startNode, {
111
+ workflow: "0000000000000" as CasRef,
112
+ prompt: "test prompt",
113
+ cwd: "/tmp",
114
+ });
115
+ const outputHash = await store.cas.put(schemas.text, { $status: "reviewed" });
116
+
117
+ const stepPayload: StepNodePayload = {
118
+ prev: null,
119
+ start: startHash,
120
+ role: "reviewer",
121
+ agent: "claude-code",
122
+ output: outputHash,
123
+ detail: detailHash,
124
+ edgePrompt: "",
125
+ startedAtMs: 1_000_000,
126
+ completedAtMs: 1_137_400,
127
+ assembledPrompt: null,
128
+ cwd: "/tmp",
129
+ usage,
130
+ previousAttempts: null,
131
+ };
132
+ return store.cas.put(schemas.stepNode, stepPayload);
133
+ }
134
+
135
+ /** Capture everything written to `process.stdout` while `fn` runs. */
136
+ async function captureStdout(fn: () => Promise<void>): Promise<string> {
137
+ const buf: string[] = [];
138
+ const spy = vi.spyOn(process.stdout, "write").mockImplementation(((
139
+ chunk: string | Uint8Array,
140
+ ): boolean => {
141
+ buf.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
142
+ return true;
143
+ }) as typeof process.stdout.write);
144
+ try {
145
+ await fn();
146
+ } finally {
147
+ spy.mockRestore();
148
+ }
149
+ return buf.join("");
150
+ }
151
+
152
+ describe("cmdStepShow text rendering (issue #403)", () => {
153
+ let testDir: string;
154
+ let casDir: string;
155
+ let originalEnv: string | undefined;
156
+
157
+ beforeEach(async () => {
158
+ testDir = await mkdtemp(join(tmpdir(), "uwf-step-show-text-"));
159
+ casDir = join(testDir, "cas");
160
+ await mkdir(casDir, { recursive: true });
161
+ originalEnv = process.env.OCAS_HOME;
162
+ process.env.OCAS_HOME = casDir;
163
+ });
164
+
165
+ afterEach(async () => {
166
+ await rm(testDir, { recursive: true, force: true });
167
+ if (originalEnv === undefined) {
168
+ delete process.env.OCAS_HOME;
169
+ } else {
170
+ process.env.OCAS_HOME = originalEnv;
171
+ }
172
+ vi.restoreAllMocks();
173
+ });
174
+
175
+ test("renders the --- Content --- block with each turn's role and content", async () => {
176
+ const setup = await setupTest(casDir);
177
+ const stepHash = await createTestStep(
178
+ setup,
179
+ [
180
+ { index: 0, role: "assistant", content: "first turn body", toolCalls: null },
181
+ { index: 1, role: "assistant", content: "second turn body", toolCalls: null },
182
+ ],
183
+ { turns: 9, inputTokens: 38612, outputTokens: 10584, duration: 137400 },
184
+ );
185
+
186
+ const detail = await cmdStepShow(testDir, stepHash);
187
+ const uwf = await createUwfStore(testDir);
188
+ const out = await captureStdout(async () =>
189
+ writeEnvelope(toStepDetailPayload(stepHash, detail), "step-detail", {
190
+ format: "text",
191
+ store: uwf.store,
192
+ schemas: uwf.schemas,
193
+ }),
194
+ );
195
+
196
+ // Metadata header
197
+ expect(out).toContain(`Step ${stepHash}`);
198
+ expect(out).toContain("Role reviewer");
199
+ expect(out).toContain("Agent claude-code");
200
+ expect(out).toContain("Status reviewed");
201
+ expect(out).toContain("Duration 137.4s");
202
+ expect(out).toContain("Usage 38612 in / 10584 out / 9 turns");
203
+
204
+ // Turn-content block — the headline regression assertion
205
+ expect(out).toContain("Turns 2");
206
+ expect(out).toContain("--- Content ---");
207
+ expect(out).toContain("[assistant] first turn body");
208
+ expect(out).toContain("[assistant] second turn body");
209
+
210
+ // No JSON envelope leakage and a single trailing newline
211
+ expect(out).not.toContain('"type"');
212
+ expect(out).not.toContain("undefined");
213
+ expect(out.endsWith("\n")).toBe(true);
214
+ expect(out.endsWith("\n\n")).toBe(false);
215
+ });
216
+
217
+ test("omits the Content block for a step with zero turns, without throwing", async () => {
218
+ const setup = await setupTest(casDir);
219
+ const stepHash = await createTestStep(setup, [], null);
220
+
221
+ const detail = await cmdStepShow(testDir, stepHash);
222
+ const uwf = await createUwfStore(testDir);
223
+ const out = await captureStdout(async () =>
224
+ writeEnvelope(toStepDetailPayload(stepHash, detail), "step-detail", {
225
+ format: "text",
226
+ store: uwf.store,
227
+ schemas: uwf.schemas,
228
+ }),
229
+ );
230
+
231
+ expect(out).toContain("Role reviewer");
232
+ expect(out).not.toContain("--- Content ---");
233
+ expect(out).not.toMatch(/^Turns\s/m);
234
+ expect(out).not.toContain("undefined");
235
+ });
236
+ });