@united-workforce/cli 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +30 -3
  2. package/dist/.build-fingerprint +1 -0
  3. package/dist/__tests__/adapter-json-roundtrip.test.js +16 -6
  4. package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
  5. package/dist/__tests__/concurrency.test.d.ts +2 -0
  6. package/dist/__tests__/concurrency.test.d.ts.map +1 -0
  7. package/dist/__tests__/concurrency.test.js +196 -0
  8. package/dist/__tests__/concurrency.test.js.map +1 -0
  9. package/dist/__tests__/config-text-renderer.test.d.ts +2 -0
  10. package/dist/__tests__/config-text-renderer.test.d.ts.map +1 -0
  11. package/dist/__tests__/config-text-renderer.test.js +137 -0
  12. package/dist/__tests__/config-text-renderer.test.js.map +1 -0
  13. package/dist/__tests__/e2e-mock-agent.test.js +23 -7
  14. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  15. package/dist/__tests__/format-text-default.test.d.ts +2 -0
  16. package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
  17. package/dist/__tests__/format-text-default.test.js +43 -0
  18. package/dist/__tests__/format-text-default.test.js.map +1 -0
  19. package/dist/__tests__/format-text-registry.test.d.ts +2 -0
  20. package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
  21. package/dist/__tests__/format-text-registry.test.js +158 -0
  22. package/dist/__tests__/format-text-registry.test.js.map +1 -0
  23. package/dist/__tests__/issue-180-workflow-ref-removed.test.js +1 -1
  24. package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
  25. package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
  26. package/dist/__tests__/log-text-renderer.test.js +265 -0
  27. package/dist/__tests__/log-text-renderer.test.js.map +1 -0
  28. package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
  29. package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
  30. package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
  31. package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
  32. package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
  33. package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
  34. package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
  35. package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
  36. package/dist/__tests__/pid-recycling.test.js +9 -7
  37. package/dist/__tests__/pid-recycling.test.js.map +1 -1
  38. package/dist/__tests__/prompt.test.js +46 -4
  39. package/dist/__tests__/prompt.test.js.map +1 -1
  40. package/dist/__tests__/resolve-head-hash.test.js +8 -0
  41. package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
  42. package/dist/__tests__/solve-issue-tea-worktree.test.js +3 -1
  43. package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
  44. package/dist/__tests__/step-ask.test.js +9 -1
  45. package/dist/__tests__/step-ask.test.js.map +1 -1
  46. package/dist/__tests__/store-unified-threads.test.js +19 -17
  47. package/dist/__tests__/store-unified-threads.test.js.map +1 -1
  48. package/dist/__tests__/thread-agent-failure-suspended.test.d.ts +2 -0
  49. package/dist/__tests__/thread-agent-failure-suspended.test.d.ts.map +1 -0
  50. package/dist/__tests__/thread-agent-failure-suspended.test.js +332 -0
  51. package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -0
  52. package/dist/__tests__/thread-cancel-status.test.js +19 -13
  53. package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
  54. package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
  55. package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
  56. package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
  57. package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
  58. package/dist/__tests__/thread-join.test.d.ts +2 -0
  59. package/dist/__tests__/thread-join.test.d.ts.map +1 -0
  60. package/dist/__tests__/thread-join.test.js +77 -0
  61. package/dist/__tests__/thread-join.test.js.map +1 -0
  62. package/dist/__tests__/thread-list-filters.test.js +10 -8
  63. package/dist/__tests__/thread-list-filters.test.js.map +1 -1
  64. package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
  65. package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
  66. package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
  67. package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
  68. package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
  69. package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
  70. package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
  71. package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
  72. package/dist/__tests__/thread-poke.test.js +15 -2
  73. package/dist/__tests__/thread-poke.test.js.map +1 -1
  74. package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
  75. package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
  76. package/dist/__tests__/thread-resume.test.js +11 -1
  77. package/dist/__tests__/thread-resume.test.js.map +1 -1
  78. package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
  79. package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
  80. package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
  81. package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
  82. package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
  83. package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
  84. package/dist/__tests__/thread-suspend-step.test.js +5 -2
  85. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  86. package/dist/__tests__/thread-test-helpers.d.ts +7 -0
  87. package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
  88. package/dist/__tests__/thread-test-helpers.js +13 -0
  89. package/dist/__tests__/thread-test-helpers.js.map +1 -1
  90. package/dist/__tests__/thread.test.js +11 -9
  91. package/dist/__tests__/thread.test.js.map +1 -1
  92. package/dist/__tests__/validate-semantic.test.js +56 -2
  93. package/dist/__tests__/validate-semantic.test.js.map +1 -1
  94. package/dist/__tests__/workflow-list-recursive.test.js +10 -7
  95. package/dist/__tests__/workflow-list-recursive.test.js.map +1 -1
  96. package/dist/__tests__/workflow-paths.test.d.ts +2 -0
  97. package/dist/__tests__/workflow-paths.test.d.ts.map +1 -0
  98. package/dist/__tests__/workflow-paths.test.js +261 -0
  99. package/dist/__tests__/workflow-paths.test.js.map +1 -0
  100. package/dist/__tests__/workflow-resolution.test.js +10 -7
  101. package/dist/__tests__/workflow-resolution.test.js.map +1 -1
  102. package/dist/__tests__/workflow-show-resolution.test.js +10 -7
  103. package/dist/__tests__/workflow-show-resolution.test.js.map +1 -1
  104. package/dist/__tests__/workflow-validate.test.js +75 -55
  105. package/dist/__tests__/workflow-validate.test.js.map +1 -1
  106. package/dist/__tests__/write-envelope.test.d.ts +2 -0
  107. package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
  108. package/dist/__tests__/write-envelope.test.js +201 -0
  109. package/dist/__tests__/write-envelope.test.js.map +1 -0
  110. package/dist/cli.js +76 -36
  111. package/dist/cli.js.map +1 -1
  112. package/dist/commands/config.d.ts +5 -0
  113. package/dist/commands/config.d.ts.map +1 -1
  114. package/dist/commands/config.js +81 -3
  115. package/dist/commands/config.js.map +1 -1
  116. package/dist/commands/prompt.d.ts.map +1 -1
  117. package/dist/commands/prompt.js +42 -29
  118. package/dist/commands/prompt.js.map +1 -1
  119. package/dist/commands/setup.d.ts +9 -4
  120. package/dist/commands/setup.d.ts.map +1 -1
  121. package/dist/commands/setup.js +51 -7
  122. package/dist/commands/setup.js.map +1 -1
  123. package/dist/commands/thread.d.ts +12 -0
  124. package/dist/commands/thread.d.ts.map +1 -1
  125. package/dist/commands/thread.js +226 -9
  126. package/dist/commands/thread.js.map +1 -1
  127. package/dist/commands/workflow.d.ts +2 -2
  128. package/dist/commands/workflow.d.ts.map +1 -1
  129. package/dist/commands/workflow.js +26 -10
  130. package/dist/commands/workflow.js.map +1 -1
  131. package/dist/concurrency/concurrency.d.ts +34 -0
  132. package/dist/concurrency/concurrency.d.ts.map +1 -0
  133. package/dist/concurrency/concurrency.js +216 -0
  134. package/dist/concurrency/concurrency.js.map +1 -0
  135. package/dist/concurrency/index.d.ts +3 -0
  136. package/dist/concurrency/index.d.ts.map +1 -0
  137. package/dist/concurrency/index.js +2 -0
  138. package/dist/concurrency/index.js.map +1 -0
  139. package/dist/concurrency/types.d.ts +19 -0
  140. package/dist/concurrency/types.d.ts.map +1 -0
  141. package/dist/concurrency/types.js +2 -0
  142. package/dist/concurrency/types.js.map +1 -0
  143. package/dist/format.d.ts +69 -2
  144. package/dist/format.d.ts.map +1 -1
  145. package/dist/format.js +198 -1
  146. package/dist/format.js.map +1 -1
  147. package/dist/output-mappers.d.ts +122 -0
  148. package/dist/output-mappers.d.ts.map +1 -0
  149. package/dist/output-mappers.js +134 -0
  150. package/dist/output-mappers.js.map +1 -0
  151. package/dist/schemas.d.ts +4 -1
  152. package/dist/schemas.d.ts.map +1 -1
  153. package/dist/schemas.js +31 -4
  154. package/dist/schemas.js.map +1 -1
  155. package/dist/store.d.ts +11 -0
  156. package/dist/store.d.ts.map +1 -1
  157. package/dist/store.js +20 -1
  158. package/dist/store.js.map +1 -1
  159. package/dist/text-renderers.d.ts +30 -0
  160. package/dist/text-renderers.d.ts.map +1 -0
  161. package/dist/text-renderers.js +251 -0
  162. package/dist/text-renderers.js.map +1 -0
  163. package/dist/validate-semantic.d.ts.map +1 -1
  164. package/dist/validate-semantic.js +28 -11
  165. package/dist/validate-semantic.js.map +1 -1
  166. package/examples/brainstorm.yaml +130 -0
  167. package/examples/debate.yaml +169 -0
  168. package/examples/socratic-questioning.yaml +112 -0
  169. package/package.json +12 -11
  170. package/src/__tests__/adapter-json-roundtrip.test.ts +15 -6
  171. package/src/__tests__/concurrency.test.ts +266 -0
  172. package/src/__tests__/config-text-renderer.test.ts +156 -0
  173. package/src/__tests__/e2e-mock-agent.test.ts +45 -7
  174. package/src/__tests__/format-text-default.test.ts +49 -0
  175. package/src/__tests__/format-text-registry.test.ts +173 -0
  176. package/src/__tests__/issue-180-workflow-ref-removed.test.ts +1 -1
  177. package/src/__tests__/log-text-renderer.test.ts +294 -0
  178. package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
  179. package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
  180. package/src/__tests__/pid-recycling.test.ts +9 -8
  181. package/src/__tests__/prompt.test.ts +48 -4
  182. package/src/__tests__/resolve-head-hash.test.ts +7 -0
  183. package/src/__tests__/solve-issue-tea-worktree.test.ts +3 -1
  184. package/src/__tests__/step-ask.test.ts +8 -1
  185. package/src/__tests__/store-unified-threads.test.ts +21 -18
  186. package/src/__tests__/thread-agent-failure-suspended.test.ts +406 -0
  187. package/src/__tests__/thread-cancel-status.test.ts +21 -14
  188. package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
  189. package/src/__tests__/thread-join.test.ts +103 -0
  190. package/src/__tests__/thread-list-filters.test.ts +9 -9
  191. package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
  192. package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
  193. package/src/__tests__/thread-poke.test.ts +14 -2
  194. package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
  195. package/src/__tests__/thread-resume.test.ts +10 -1
  196. package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
  197. package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
  198. package/src/__tests__/thread-suspend-step.test.ts +5 -2
  199. package/src/__tests__/thread-test-helpers.ts +15 -1
  200. package/src/__tests__/thread.test.ts +10 -10
  201. package/src/__tests__/validate-semantic.test.ts +59 -2
  202. package/src/__tests__/workflow-list-recursive.test.ts +9 -9
  203. package/src/__tests__/workflow-paths.test.ts +337 -0
  204. package/src/__tests__/workflow-resolution.test.ts +9 -8
  205. package/src/__tests__/workflow-show-resolution.test.ts +9 -8
  206. package/src/__tests__/workflow-validate.test.ts +78 -56
  207. package/src/__tests__/write-envelope.test.ts +257 -0
  208. package/src/cli.ts +111 -35
  209. package/src/commands/config.ts +85 -3
  210. package/src/commands/prompt.ts +42 -29
  211. package/src/commands/setup.ts +57 -7
  212. package/src/commands/thread.ts +280 -9
  213. package/src/commands/workflow.ts +32 -11
  214. package/src/concurrency/concurrency.ts +245 -0
  215. package/src/concurrency/index.ts +10 -0
  216. package/src/concurrency/types.ts +19 -0
  217. package/src/format.ts +282 -2
  218. package/src/output-mappers.ts +255 -0
  219. package/src/schemas.ts +39 -3
  220. package/src/store.ts +25 -1
  221. package/src/text-renderers.ts +355 -0
  222. package/src/validate-semantic.ts +33 -12
  223. package/LICENSE +0 -21
@@ -0,0 +1,294 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { formatOutput, getTextRenderer, TEXT_RENDERERS } from "../format.js";
3
+ import { renderLogList, renderLogShow } from "../text-renderers.js";
4
+
5
+ describe("log list — text renderer registration", () => {
6
+ test("TEXT_RENDERERS contains 'log list'", () => {
7
+ expect(getTextRenderer("log list")).toBeDefined();
8
+ expect(typeof getTextRenderer("log list")).toBe("function");
9
+ });
10
+
11
+ test("TEXT_RENDERERS['log list'] is the same reference as renderLogList", () => {
12
+ expect(TEXT_RENDERERS["log list"]).toBe(renderLogList);
13
+ });
14
+
15
+ test("renderLogList is exported from text-renderers.ts", () => {
16
+ expect(typeof renderLogList).toBe("function");
17
+ });
18
+ });
19
+
20
+ describe("log show — text renderer registration", () => {
21
+ test("TEXT_RENDERERS contains 'log show'", () => {
22
+ expect(getTextRenderer("log show")).toBeDefined();
23
+ expect(typeof getTextRenderer("log show")).toBe("function");
24
+ });
25
+
26
+ test("TEXT_RENDERERS['log show'] is the same reference as renderLogShow", () => {
27
+ expect(TEXT_RENDERERS["log show"]).toBe(renderLogShow);
28
+ });
29
+
30
+ test("renderLogShow is exported from text-renderers.ts", () => {
31
+ expect(typeof renderLogShow).toBe("function");
32
+ });
33
+ });
34
+
35
+ describe("renderLogList — output shape", () => {
36
+ test("returns a string for full payload", () => {
37
+ const out = renderLogList([
38
+ { name: "2026-06-10.jsonl", size: 4096, date: "2026-06-10" },
39
+ { name: "2026-06-11.jsonl", size: 8192, date: "2026-06-11" },
40
+ ]);
41
+ expect(typeof out).toBe("string");
42
+ });
43
+
44
+ test("includes file names, dates, and sizes for each row", () => {
45
+ const out = renderLogList([
46
+ { name: "2026-06-10.jsonl", size: 4096, date: "2026-06-10" },
47
+ { name: "2026-06-11.jsonl", size: 8192, date: "2026-06-11" },
48
+ ]);
49
+ expect(out).toContain("2026-06-10.jsonl");
50
+ expect(out).toContain("2026-06-10");
51
+ expect(out).toContain("2026-06-11.jsonl");
52
+ expect(out).toContain("2026-06-11");
53
+ });
54
+
55
+ test("includes a header line", () => {
56
+ const out = renderLogList([{ name: "2026-06-10.jsonl", size: 4096, date: "2026-06-10" }]);
57
+ const lines = out.split("\n");
58
+ expect(lines.length).toBeGreaterThanOrEqual(2);
59
+ // header should mention NAME or DATE or SIZE
60
+ const header = lines[0]?.toUpperCase() ?? "";
61
+ const hasHeader = header.includes("NAME") || header.includes("DATE") || header.includes("SIZE");
62
+ expect(hasHeader).toBe(true);
63
+ });
64
+
65
+ test("does NOT begin with '{' or '[' (not raw JSON)", () => {
66
+ const out = renderLogList([{ name: "2026-06-10.jsonl", size: 4096, date: "2026-06-10" }]);
67
+ const trimmed = out.trimStart();
68
+ expect(trimmed.startsWith("{")).toBe(false);
69
+ expect(trimmed.startsWith("[")).toBe(false);
70
+ });
71
+
72
+ test("does NOT contain literal 'undefined'", () => {
73
+ const out = renderLogList([{ name: "2026-06-10.jsonl", size: 4096, date: "2026-06-10" }]);
74
+ expect(out).not.toContain("undefined");
75
+ });
76
+
77
+ test("empty array — returns string, no 'undefined'", () => {
78
+ const out = renderLogList([]);
79
+ expect(typeof out).toBe("string");
80
+ expect(out).not.toContain("undefined");
81
+ });
82
+
83
+ test("null payload — returns string, does not throw", () => {
84
+ expect(() => renderLogList(null)).not.toThrow();
85
+ const out = renderLogList(null);
86
+ expect(typeof out).toBe("string");
87
+ expect(out).not.toContain("undefined");
88
+ });
89
+
90
+ test("non-array payload — returns string, does not throw", () => {
91
+ expect(() => renderLogList({ name: "x" })).not.toThrow();
92
+ const out = renderLogList({ name: "x" });
93
+ expect(typeof out).toBe("string");
94
+ expect(out).not.toContain("undefined");
95
+ });
96
+
97
+ test("partial item (missing size) — returns string, no 'undefined'", () => {
98
+ const out = renderLogList([{ name: "2026-06-10.jsonl", date: "2026-06-10" }]);
99
+ expect(typeof out).toBe("string");
100
+ expect(out).not.toContain("undefined");
101
+ });
102
+ });
103
+
104
+ describe("renderLogShow — output shape", () => {
105
+ test("returns a string for full payload", () => {
106
+ const out = renderLogShow([
107
+ {
108
+ ts: "2026-06-12T10:00:00.000Z",
109
+ pid: "12345",
110
+ tag: "4KNMR2PX",
111
+ msg: "Loading workflow...",
112
+ thread: "01JTEST000000000000THREAD1",
113
+ workflow: "WF1234567890A",
114
+ },
115
+ ]);
116
+ expect(typeof out).toBe("string");
117
+ });
118
+
119
+ test("includes ts, pid, tag, and msg for each entry", () => {
120
+ const out = renderLogShow([
121
+ {
122
+ ts: "2026-06-12T10:00:00.000Z",
123
+ pid: "12345",
124
+ tag: "4KNMR2PX",
125
+ msg: "Loading workflow...",
126
+ thread: null,
127
+ workflow: null,
128
+ },
129
+ ]);
130
+ expect(out).toContain("2026-06-12T10:00:00.000Z");
131
+ expect(out).toContain("12345");
132
+ expect(out).toContain("4KNMR2PX");
133
+ expect(out).toContain("Loading workflow...");
134
+ });
135
+
136
+ test("includes thread id when present", () => {
137
+ const out = renderLogShow([
138
+ {
139
+ ts: "2026-06-12T10:00:00.000Z",
140
+ pid: "12345",
141
+ tag: "4KNMR2PX",
142
+ msg: "Loading workflow...",
143
+ thread: "01JTEST000000000000THREAD1",
144
+ workflow: null,
145
+ },
146
+ ]);
147
+ expect(out).toContain("01JTEST000000000000THREAD1");
148
+ });
149
+
150
+ test("does NOT begin with '{' or '[' (not raw JSON)", () => {
151
+ const out = renderLogShow([
152
+ {
153
+ ts: "2026-06-12T10:00:00.000Z",
154
+ pid: "12345",
155
+ tag: "4KNMR2PX",
156
+ msg: "Loading workflow...",
157
+ thread: null,
158
+ workflow: null,
159
+ },
160
+ ]);
161
+ const trimmed = out.trimStart();
162
+ expect(trimmed.startsWith("{")).toBe(false);
163
+ expect(trimmed.startsWith("[")).toBe(false);
164
+ });
165
+
166
+ test("does NOT contain literal 'undefined'", () => {
167
+ const out = renderLogShow([
168
+ {
169
+ ts: "2026-06-12T10:00:00.000Z",
170
+ pid: "12345",
171
+ tag: "4KNMR2PX",
172
+ msg: "Loading workflow...",
173
+ thread: null,
174
+ workflow: null,
175
+ },
176
+ ]);
177
+ expect(out).not.toContain("undefined");
178
+ });
179
+
180
+ test("empty array — returns string, no 'undefined'", () => {
181
+ const out = renderLogShow([]);
182
+ expect(typeof out).toBe("string");
183
+ expect(out).not.toContain("undefined");
184
+ });
185
+
186
+ test("null payload — returns string, does not throw", () => {
187
+ expect(() => renderLogShow(null)).not.toThrow();
188
+ const out = renderLogShow(null);
189
+ expect(typeof out).toBe("string");
190
+ expect(out).not.toContain("undefined");
191
+ });
192
+
193
+ test("non-array payload — returns string, does not throw", () => {
194
+ expect(() => renderLogShow({ ts: "x" })).not.toThrow();
195
+ const out = renderLogShow({ ts: "x" });
196
+ expect(typeof out).toBe("string");
197
+ expect(out).not.toContain("undefined");
198
+ });
199
+
200
+ test("partial entry (missing thread) — returns string, no 'undefined'", () => {
201
+ const out = renderLogShow([
202
+ {
203
+ ts: "2026-06-12T10:00:00.000Z",
204
+ pid: "12345",
205
+ tag: "4KNMR2PX",
206
+ msg: "hello",
207
+ },
208
+ ]);
209
+ expect(typeof out).toBe("string");
210
+ expect(out).not.toContain("undefined");
211
+ });
212
+ });
213
+
214
+ describe("formatOutput integration — log list", () => {
215
+ test("formatOutput(data, 'text', 'log list') uses renderer", () => {
216
+ const data = [{ name: "2026-06-10.jsonl", size: 4096, date: "2026-06-10" }];
217
+ const out = formatOutput(data, "text", "log list");
218
+ expect(typeof out).toBe("string");
219
+ expect(out).not.toContain("undefined");
220
+ expect(out.trimStart().startsWith("{")).toBe(false);
221
+ expect(out.trimStart().startsWith("[")).toBe(false);
222
+ expect(out).toContain("2026-06-10.jsonl");
223
+ });
224
+
225
+ test("formatOutput(data, 'json', 'log list') still emits parseable JSON", () => {
226
+ const data = [{ name: "2026-06-10.jsonl", size: 4096, date: "2026-06-10" }];
227
+ const out = formatOutput(data, "json", "log list");
228
+ const parsed = JSON.parse(out);
229
+ expect(parsed).toEqual(data);
230
+ });
231
+
232
+ test("formatOutput(data, 'yaml', 'log list') still emits YAML", () => {
233
+ const data = [{ name: "2026-06-10.jsonl", size: 4096, date: "2026-06-10" }];
234
+ const out = formatOutput(data, "yaml", "log list");
235
+ expect(typeof out).toBe("string");
236
+ expect(out).toContain("name:");
237
+ expect(out).toContain("2026-06-10.jsonl");
238
+ });
239
+ });
240
+
241
+ describe("formatOutput integration — log show", () => {
242
+ test("formatOutput(data, 'text', 'log show') uses renderer", () => {
243
+ const data = [
244
+ {
245
+ ts: "2026-06-12T10:00:00.000Z",
246
+ pid: "12345",
247
+ tag: "4KNMR2PX",
248
+ msg: "hello",
249
+ thread: null,
250
+ workflow: null,
251
+ },
252
+ ];
253
+ const out = formatOutput(data, "text", "log show");
254
+ expect(typeof out).toBe("string");
255
+ expect(out).not.toContain("undefined");
256
+ expect(out.trimStart().startsWith("{")).toBe(false);
257
+ expect(out.trimStart().startsWith("[")).toBe(false);
258
+ expect(out).toContain("4KNMR2PX");
259
+ expect(out).toContain("hello");
260
+ });
261
+
262
+ test("formatOutput(data, 'json', 'log show') still emits parseable JSON", () => {
263
+ const data = [
264
+ {
265
+ ts: "2026-06-12T10:00:00.000Z",
266
+ pid: "12345",
267
+ tag: "4KNMR2PX",
268
+ msg: "hello",
269
+ thread: null,
270
+ workflow: null,
271
+ },
272
+ ];
273
+ const out = formatOutput(data, "json", "log show");
274
+ const parsed = JSON.parse(out);
275
+ expect(parsed).toEqual(data);
276
+ });
277
+
278
+ test("formatOutput(data, 'yaml', 'log show') still emits YAML", () => {
279
+ const data = [
280
+ {
281
+ ts: "2026-06-12T10:00:00.000Z",
282
+ pid: "12345",
283
+ tag: "4KNMR2PX",
284
+ msg: "hello",
285
+ thread: null,
286
+ workflow: null,
287
+ },
288
+ ];
289
+ const out = formatOutput(data, "yaml", "log show");
290
+ expect(typeof out).toBe("string");
291
+ expect(out).toContain("ts:");
292
+ expect(out).toContain("pid:");
293
+ });
294
+ });
@@ -0,0 +1,124 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { CasRef, ThreadId } from "@united-workforce/protocol";
4
+ import { generateUlid } from "@united-workforce/util";
5
+ import { describe, expect, test } from "vitest";
6
+ import type { ThreadListItemWithStatus } from "../commands/thread.js";
7
+ import { toThreadListPayload } from "../output-mappers.js";
8
+
9
+ const OUTPUT_MAPPERS_PATH = fileURLToPath(new URL("../output-mappers.ts", import.meta.url));
10
+
11
+ function makeItem(threadId: string): ThreadListItemWithStatus {
12
+ return {
13
+ thread: threadId as ThreadId,
14
+ workflow: "WORKFLOWHASH1" as CasRef,
15
+ head: "HEADHASH00001" as CasRef,
16
+ status: "idle",
17
+ currentRole: null,
18
+ statusDisplay: "idle",
19
+ workflowName: "test-workflow",
20
+ };
21
+ }
22
+
23
+ describe("toThreadListPayload — issue #343 (ULID timestamp decoded with padding stripped)", () => {
24
+ test("startedAt equals the millisecond timestamp originally passed to generateUlid", () => {
25
+ const ts = 1781219097830; // 2026-06-11T23:04:57.830Z
26
+ const ulid = generateUlid(ts);
27
+
28
+ const payload = toThreadListPayload([makeItem(ulid)]);
29
+
30
+ expect(payload.items).toHaveLength(1);
31
+ expect(payload.items[0]!.startedAt).toBe(ts);
32
+ });
33
+
34
+ test("startedAt is NOT the raw 50-bit value (i.e. NOT timestamp << 2)", () => {
35
+ const ts = 1781219097830;
36
+ const ulid = generateUlid(ts);
37
+
38
+ const payload = toThreadListPayload([makeItem(ulid)]);
39
+
40
+ // The buggy decoder produced ts * 4 = 7124876391323, pushing year to 2195.
41
+ expect(payload.items[0]!.startedAt).not.toBe(ts * 4);
42
+ expect(payload.items[0]!.startedAt).not.toBe(7124876391323);
43
+ });
44
+
45
+ test("startedAt decodes to year 2026 for the issue-reported ULID timestamp", () => {
46
+ const ts = 1781219097830;
47
+ const ulid = generateUlid(ts);
48
+
49
+ const payload = toThreadListPayload([makeItem(ulid)]);
50
+
51
+ const startedAt = payload.items[0]!.startedAt;
52
+ expect(startedAt).not.toBeNull();
53
+ if (startedAt === null) return;
54
+ const isoDate = new Date(startedAt).toISOString().slice(0, 10);
55
+ expect(isoDate).toBe("2026-06-11");
56
+ });
57
+
58
+ test("round-trips correctly across several timestamps", () => {
59
+ const timestamps = [
60
+ 0,
61
+ Date.UTC(2020, 0, 1, 0, 0, 0),
62
+ Date.UTC(2023, 5, 15, 12, 30, 45),
63
+ Date.UTC(2026, 4, 20, 0, 0, 0),
64
+ Date.UTC(2030, 11, 31, 23, 59, 59),
65
+ ];
66
+ const items = timestamps.map((t) => makeItem(generateUlid(t)));
67
+
68
+ const payload = toThreadListPayload(items);
69
+
70
+ for (let i = 0; i < timestamps.length; i++) {
71
+ expect(payload.items[i]!.startedAt).toBe(timestamps[i]);
72
+ }
73
+ });
74
+
75
+ test("startedAt is null for thread ids that are not valid 26-char Crockford Base32 ULIDs", () => {
76
+ const cases = ["", "TOOSHORT", "TOOLONGAAAAAAAAAAAAAAAAAA", "INVALID!@#$%^&CHARACTERS"];
77
+ const items = cases.map((id) => makeItem(id));
78
+
79
+ const payload = toThreadListPayload(items);
80
+
81
+ for (const item of payload.items) {
82
+ expect(item.startedAt).toBeNull();
83
+ }
84
+ });
85
+
86
+ test("preserves other thread-list item fields verbatim", () => {
87
+ const ts = 1781219097830;
88
+ const ulid = generateUlid(ts);
89
+ const item: ThreadListItemWithStatus = {
90
+ thread: ulid as ThreadId,
91
+ workflow: "WORKFLOWHASH9" as CasRef,
92
+ head: "HEADHASH99999" as CasRef,
93
+ status: "running",
94
+ currentRole: "developer",
95
+ statusDisplay: "running",
96
+ workflowName: "solve-issue",
97
+ };
98
+
99
+ const payload = toThreadListPayload([item]);
100
+
101
+ expect(payload.items[0]).toEqual({
102
+ threadId: ulid,
103
+ workflowHash: "WORKFLOWHASH9",
104
+ workflowName: "solve-issue",
105
+ status: "running",
106
+ currentRole: "developer",
107
+ startedAt: ts,
108
+ completedAt: null,
109
+ });
110
+ });
111
+ });
112
+
113
+ describe("output-mappers.ts source — issue #343 refactor", () => {
114
+ test("does NOT contain a local extractUlidTime function (removed in favor of util)", async () => {
115
+ const source = await readFile(OUTPUT_MAPPERS_PATH, "utf8");
116
+ expect(source).not.toMatch(/function\s+extractUlidTime\b/);
117
+ });
118
+
119
+ test("imports extractUlidTimestamp from @united-workforce/util", async () => {
120
+ const source = await readFile(OUTPUT_MAPPERS_PATH, "utf8");
121
+ expect(source).toMatch(/extractUlidTimestamp/);
122
+ expect(source).toMatch(/@united-workforce\/util/);
123
+ });
124
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { toWorkflowAddPayload, type WorkflowAddPayload } from "../output-mappers.js";
3
+
4
+ describe("toWorkflowAddPayload — issue #334", () => {
5
+ test("maps WorkflowAddOutput { name, hash } to plain payload shape", () => {
6
+ const out = toWorkflowAddPayload({ name: "review-pr", hash: "2TBP6T37TZAJZ" });
7
+ expect(out).toEqual({ name: "review-pr", hash: "2TBP6T37TZAJZ" });
8
+ });
9
+
10
+ test("returns a WorkflowAddPayload type with exactly two fields", () => {
11
+ const out: WorkflowAddPayload = toWorkflowAddPayload({
12
+ name: "solve-issue",
13
+ hash: "76C98RVXA5E4F",
14
+ });
15
+ expect(Object.keys(out).sort()).toEqual(["hash", "name"]);
16
+ });
17
+
18
+ test("performs no I/O — pure data mapping", () => {
19
+ // Repeated calls produce equal results
20
+ const a = toWorkflowAddPayload({ name: "a", hash: "AAA1234567890" });
21
+ const b = toWorkflowAddPayload({ name: "a", hash: "AAA1234567890" });
22
+ expect(a).toEqual(b);
23
+ });
24
+ });
@@ -13,17 +13,11 @@ import {
13
13
  listRunningThreads,
14
14
  readMarker,
15
15
  } from "../background/index.js";
16
- import { createUwfStore, type UwfStore } from "../store.js";
16
+ import type { UwfStore } from "../store.js";
17
+ import { makeUwfStore } from "./thread-test-helpers.js";
17
18
 
18
19
  // ── helpers ───────────────────────────────────────────────────────────────────
19
20
 
20
- async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
21
- const casDir = join(storageRoot, "cas");
22
- await mkdir(casDir, { recursive: true });
23
- process.env.OCAS_HOME = casDir;
24
- return createUwfStore(storageRoot);
25
- }
26
-
27
21
  async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
28
22
  const workflowPayload = {
29
23
  name: "test-workflow",
@@ -42,12 +36,19 @@ async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
42
36
  // ── test setup ────────────────────────────────────────────────────────────────
43
37
 
44
38
  let tmpDir: string;
39
+ let savedOcasHome: string | undefined;
45
40
 
46
41
  beforeEach(async () => {
42
+ savedOcasHome = process.env.OCAS_HOME;
47
43
  tmpDir = await mkdtemp(join(tmpdir(), "pid-recycling-test-"));
48
44
  });
49
45
 
50
46
  afterEach(async () => {
47
+ if (savedOcasHome === undefined) {
48
+ delete process.env.OCAS_HOME;
49
+ } else {
50
+ process.env.OCAS_HOME = savedOcasHome;
51
+ }
51
52
  await rm(tmpDir, { recursive: true, force: true });
52
53
  });
53
54
 
@@ -135,8 +135,6 @@ describe("prompt commands", () => {
135
135
  // Fresh install scenario
136
136
  expect(result).toContain("Fresh Install");
137
137
  expect(result).toContain("uwf setup");
138
- expect(result).toContain("--provider");
139
- expect(result).toContain("--api-key");
140
138
  expect(result).toContain("agent adapter");
141
139
  // Upgrade scenario
142
140
  expect(result).toContain("Upgrade");
@@ -147,6 +145,49 @@ describe("prompt commands", () => {
147
145
  expect(result.length).toBeGreaterThan(100);
148
146
  });
149
147
 
148
+ // Skip: pure documentation content assertions on bootstrap prompt text.
149
+ test.skip("prompt bootstrap has no LLM provider/model references", () => {
150
+ const result = cmdPromptBootstrap();
151
+ // Must NOT contain provider/model flags
152
+ expect(result).not.toContain("--provider");
153
+ expect(result).not.toContain("--base-url");
154
+ expect(result).not.toContain("--api-key");
155
+ expect(result).not.toContain("--model");
156
+ // Must NOT contain old Step 2 about provider config
157
+ expect(result).not.toContain("Configure provider and model");
158
+ // Must NOT contain preset providers table
159
+ expect(result).not.toContain("openrouter");
160
+ expect(result).not.toContain("OpenRouter");
161
+ expect(result).not.toContain("xAI");
162
+ expect(result).not.toContain("dashscope");
163
+ // Must NOT show provider/model config keys
164
+ expect(result).not.toContain("providers:");
165
+ expect(result).not.toContain("defaultModel");
166
+ expect(result).not.toContain("models:");
167
+ // Setup step must show only --agent
168
+ expect(result).toContain("uwf setup --agent");
169
+ // Config example must show only agents, defaultAgent, agentOverrides
170
+ expect(result).toContain("agents:");
171
+ expect(result).toContain("defaultAgent:");
172
+ // Must mention per-adapter LLM config
173
+ expect(result).toMatch(/~\/\.uwf\/agents\//);
174
+ });
175
+
176
+ // Skip: pure documentation content assertions on bootstrap prompt text.
177
+ test.skip("prompt bootstrap step numbering has no gaps after removing old Step 2", () => {
178
+ const result = cmdPromptBootstrap();
179
+ // Extract only the Fresh Install section
180
+ const freshStart = result.indexOf("## Scenario A: Fresh Install");
181
+ const freshEnd = result.indexOf("## Scenario B:");
182
+ const freshSection = result.slice(freshStart, freshEnd);
183
+ const stepHeaders = freshSection.match(/### Step \d+/g) ?? [];
184
+ const stepNumbers = stepHeaders.map((h) => Number.parseInt(h.replace("### Step ", ""), 10));
185
+ // Verify sequential numbering (0, 1, 2, 3, ...)
186
+ for (let i = 0; i < stepNumbers.length; i++) {
187
+ expect(stepNumbers[i]).toBe(i);
188
+ }
189
+ });
190
+
150
191
  test("prompt help subcommand is suppressed", { timeout: 30_000 }, () => {
151
192
  const cliPath = join(__dirname, "..", "..", "dist", "cli.js");
152
193
  const output = execFileSync("node", [cliPath, "prompt", "--help"], {
@@ -165,7 +206,9 @@ describe("prompt commands", () => {
165
206
  });
166
207
  });
167
208
 
168
- describe("prompt adapter-developing issue #214 v0.4 contract", () => {
209
+ // Skip: pure documentation content assertions text changes break these without
210
+ // indicating real bugs. Verified by human review instead. See #299 discussion.
211
+ describe.skip("prompt adapter-developing — issue #214 v0.4 contract", () => {
169
212
  const text = cmdPromptAdapterDeveloping();
170
213
  const lower = text.toLowerCase();
171
214
 
@@ -427,7 +470,8 @@ describe("prompt adapter-developing — issue #214 v0.4 contract", () => {
427
470
  });
428
471
  });
429
472
 
430
- describe("prompt workflow-authoring issue #226 edge location field", () => {
473
+ // Skip: pure documentation content assertions on reference text.
474
+ describe.skip("prompt workflow-authoring — issue #226 edge location field", () => {
431
475
  const text = cmdPromptWorkflowAuthoring();
432
476
  const lower = text.toLowerCase();
433
477
 
@@ -7,8 +7,10 @@ import { resolveHeadHash } from "../commands/shared.js";
7
7
  import { completeThread, createUwfStore, setThread } from "../store.js";
8
8
 
9
9
  let tmpDir: string;
10
+ let savedOcasHome: string | undefined;
10
11
 
11
12
  beforeEach(async () => {
13
+ savedOcasHome = process.env.OCAS_HOME;
12
14
  tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-resolve-head-"));
13
15
  const casDir = join(tmpDir, "cas");
14
16
  await mkdir(casDir, { recursive: true });
@@ -16,6 +18,11 @@ beforeEach(async () => {
16
18
  });
17
19
 
18
20
  afterEach(async () => {
21
+ if (savedOcasHome === undefined) {
22
+ delete process.env.OCAS_HOME;
23
+ } else {
24
+ process.env.OCAS_HOME = savedOcasHome;
25
+ }
19
26
  await rm(tmpDir, { recursive: true, force: true });
20
27
  });
21
28
 
@@ -13,7 +13,9 @@ import { parse } from "yaml";
13
13
  * which fixes the "path segment [0] is empty" error in worktree directories.
14
14
  */
15
15
 
16
- describe("solve-issue workflow: Gitea API PR creation", () => {
16
+ // Skip: pure workflow YAML prose content assertions — procedure text changes
17
+ // break these without indicating real bugs. See #299 discussion.
18
+ describe.skip("solve-issue workflow: Gitea API PR creation", () => {
17
19
  // Navigate up from packages/cli/src/__tests__ to repo root
18
20
  const workflowPath = join(
19
21
  dirname(fileURLToPath(import.meta.url)),
@@ -41,12 +41,19 @@ const THREAD_ID = "01ASKSTEPTEST000000000" as ThreadId;
41
41
  const STEP_SESSION_ID = "ses-original-step-001";
42
42
 
43
43
  let tmpDir: string;
44
+ let savedOcasHome: string | undefined;
44
45
 
45
46
  beforeEach(async () => {
46
- tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-ask-test-"));
47
+ savedOcasHome = process.env.OCAS_HOME;
48
+ tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-ask-test-"));
47
49
  });
48
50
 
49
51
  afterEach(async () => {
52
+ if (savedOcasHome === undefined) {
53
+ delete process.env.OCAS_HOME;
54
+ } else {
55
+ process.env.OCAS_HOME = savedOcasHome;
56
+ }
50
57
  await rm(tmpDir, { recursive: true, force: true });
51
58
  });
52
59