@united-workforce/cli 0.3.0 → 0.5.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 (319) hide show
  1. package/README.md +45 -11
  2. package/dist/.build-fingerprint +1 -0
  3. package/dist/__tests__/adapter-json-roundtrip.test.js +17 -7
  4. package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
  5. package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
  6. package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
  7. package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
  8. package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
  9. package/dist/__tests__/build-step-entry.test.d.ts +2 -0
  10. package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
  11. package/dist/__tests__/build-step-entry.test.js +173 -0
  12. package/dist/__tests__/build-step-entry.test.js.map +1 -0
  13. package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
  14. package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
  15. package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
  16. package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
  17. package/dist/__tests__/concurrency.test.d.ts +2 -0
  18. package/dist/__tests__/concurrency.test.d.ts.map +1 -0
  19. package/dist/__tests__/concurrency.test.js +196 -0
  20. package/dist/__tests__/concurrency.test.js.map +1 -0
  21. package/dist/__tests__/config.test.js +26 -302
  22. package/dist/__tests__/config.test.js.map +1 -1
  23. package/dist/__tests__/current-role.test.js +7 -6
  24. package/dist/__tests__/current-role.test.js.map +1 -1
  25. package/dist/__tests__/e2e-mock-agent.test.js +43 -30
  26. package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
  27. package/dist/__tests__/format-text-default.test.d.ts +2 -0
  28. package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
  29. package/dist/__tests__/format-text-default.test.js +43 -0
  30. package/dist/__tests__/format-text-default.test.js.map +1 -0
  31. package/dist/__tests__/format-text-registry.test.d.ts +2 -0
  32. package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
  33. package/dist/__tests__/format-text-registry.test.js +158 -0
  34. package/dist/__tests__/format-text-registry.test.js.map +1 -0
  35. package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
  36. package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
  37. package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
  38. package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
  39. package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
  40. package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
  41. package/dist/__tests__/log-text-renderer.test.js +265 -0
  42. package/dist/__tests__/log-text-renderer.test.js.map +1 -0
  43. package/dist/__tests__/moderator-evaluate.test.js +9 -50
  44. package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
  45. package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
  46. package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
  47. package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
  48. package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
  49. package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
  50. package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
  51. package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
  52. package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
  53. package/dist/__tests__/pid-recycling.test.d.ts +2 -0
  54. package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
  55. package/dist/__tests__/pid-recycling.test.js +273 -0
  56. package/dist/__tests__/pid-recycling.test.js.map +1 -0
  57. package/dist/__tests__/prompt.test.js +365 -2
  58. package/dist/__tests__/prompt.test.js.map +1 -1
  59. package/dist/__tests__/resolve-head-hash.test.js +12 -4
  60. package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
  61. package/dist/__tests__/setup-agent-discovery.test.js +21 -30
  62. package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
  63. package/dist/__tests__/setup-complexity.test.js +2 -168
  64. package/dist/__tests__/setup-complexity.test.js.map +1 -1
  65. package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
  66. package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
  67. package/dist/__tests__/setup-no-llm.test.js +52 -0
  68. package/dist/__tests__/setup-no-llm.test.js.map +1 -0
  69. package/dist/__tests__/solve-issue-tea-worktree.test.js +27 -28
  70. package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
  71. package/dist/__tests__/step-ask.test.d.ts +2 -0
  72. package/dist/__tests__/step-ask.test.d.ts.map +1 -0
  73. package/dist/__tests__/step-ask.test.js +507 -0
  74. package/dist/__tests__/step-ask.test.js.map +1 -0
  75. package/dist/__tests__/step-show-json.test.js +1 -0
  76. package/dist/__tests__/step-show-json.test.js.map +1 -1
  77. package/dist/__tests__/step-timing.test.js +2 -0
  78. package/dist/__tests__/step-timing.test.js.map +1 -1
  79. package/dist/__tests__/store-global-cas.test.js +2 -2
  80. package/dist/__tests__/store-global-cas.test.js.map +1 -1
  81. package/dist/__tests__/store-unified-threads.test.js +28 -26
  82. package/dist/__tests__/store-unified-threads.test.js.map +1 -1
  83. package/dist/__tests__/thread-cancel-status.test.js +25 -19
  84. package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
  85. package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
  86. package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
  87. package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
  88. package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
  89. package/dist/__tests__/thread-list-filters.test.js +354 -17
  90. package/dist/__tests__/thread-list-filters.test.js.map +1 -1
  91. package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
  92. package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
  93. package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
  94. package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
  95. package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
  96. package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
  97. package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
  98. package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
  99. package/dist/__tests__/thread-poke.test.d.ts +2 -0
  100. package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
  101. package/dist/__tests__/thread-poke.test.js +422 -0
  102. package/dist/__tests__/thread-poke.test.js.map +1 -0
  103. package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
  104. package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
  105. package/dist/__tests__/thread-resume.test.js +21 -15
  106. package/dist/__tests__/thread-resume.test.js.map +1 -1
  107. package/dist/__tests__/thread-show-status.test.js +17 -28
  108. package/dist/__tests__/thread-show-status.test.js.map +1 -1
  109. package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
  110. package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
  111. package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
  112. package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
  113. package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
  114. package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
  115. package/dist/__tests__/thread-suspend-step.test.js +13 -16
  116. package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
  117. package/dist/__tests__/thread-suspended-display.test.js +10 -22
  118. package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
  119. package/dist/__tests__/thread-test-helpers.d.ts +7 -0
  120. package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
  121. package/dist/__tests__/thread-test-helpers.js +13 -0
  122. package/dist/__tests__/thread-test-helpers.js.map +1 -1
  123. package/dist/__tests__/thread.test.js +15 -13
  124. package/dist/__tests__/thread.test.js.map +1 -1
  125. package/dist/__tests__/validate-semantic.test.js +105 -23
  126. package/dist/__tests__/validate-semantic.test.js.map +1 -1
  127. package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
  128. package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
  129. package/dist/__tests__/workflow-list-recursive.test.js +286 -0
  130. package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
  131. package/dist/__tests__/workflow-resolution.test.js +46 -28
  132. package/dist/__tests__/workflow-resolution.test.js.map +1 -1
  133. package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
  134. package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
  135. package/dist/__tests__/workflow-show-resolution.test.js +213 -0
  136. package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
  137. package/dist/__tests__/workflow-validate.test.d.ts +2 -0
  138. package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
  139. package/dist/__tests__/workflow-validate.test.js +707 -0
  140. package/dist/__tests__/workflow-validate.test.js.map +1 -0
  141. package/dist/__tests__/write-envelope.test.d.ts +2 -0
  142. package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
  143. package/dist/__tests__/write-envelope.test.js +201 -0
  144. package/dist/__tests__/write-envelope.test.js.map +1 -0
  145. package/dist/background/background.d.ts +22 -1
  146. package/dist/background/background.d.ts.map +1 -1
  147. package/dist/background/background.js +83 -6
  148. package/dist/background/background.js.map +1 -1
  149. package/dist/background/index.d.ts +1 -1
  150. package/dist/background/index.d.ts.map +1 -1
  151. package/dist/background/index.js +1 -1
  152. package/dist/background/index.js.map +1 -1
  153. package/dist/background/types.d.ts +1 -0
  154. package/dist/background/types.d.ts.map +1 -1
  155. package/dist/cli.js +120 -62
  156. package/dist/cli.js.map +1 -1
  157. package/dist/commands/config.d.ts +3 -1
  158. package/dist/commands/config.d.ts.map +1 -1
  159. package/dist/commands/config.js +17 -31
  160. package/dist/commands/config.js.map +1 -1
  161. package/dist/commands/prompt.d.ts.map +1 -1
  162. package/dist/commands/prompt.js +57 -31
  163. package/dist/commands/prompt.js.map +1 -1
  164. package/dist/commands/setup.d.ts +12 -39
  165. package/dist/commands/setup.d.ts.map +1 -1
  166. package/dist/commands/setup.js +72 -303
  167. package/dist/commands/setup.js.map +1 -1
  168. package/dist/commands/step.d.ts +44 -1
  169. package/dist/commands/step.d.ts.map +1 -1
  170. package/dist/commands/step.js +255 -11
  171. package/dist/commands/step.js.map +1 -1
  172. package/dist/commands/thread.d.ts +16 -3
  173. package/dist/commands/thread.d.ts.map +1 -1
  174. package/dist/commands/thread.js +423 -142
  175. package/dist/commands/thread.js.map +1 -1
  176. package/dist/commands/workflow.d.ts +9 -1
  177. package/dist/commands/workflow.d.ts.map +1 -1
  178. package/dist/commands/workflow.js +126 -6
  179. package/dist/commands/workflow.js.map +1 -1
  180. package/dist/concurrency/concurrency.d.ts +34 -0
  181. package/dist/concurrency/concurrency.d.ts.map +1 -0
  182. package/dist/concurrency/concurrency.js +216 -0
  183. package/dist/concurrency/concurrency.js.map +1 -0
  184. package/dist/concurrency/index.d.ts +3 -0
  185. package/dist/concurrency/index.d.ts.map +1 -0
  186. package/dist/concurrency/index.js +2 -0
  187. package/dist/concurrency/index.js.map +1 -0
  188. package/dist/concurrency/types.d.ts +19 -0
  189. package/dist/concurrency/types.d.ts.map +1 -0
  190. package/dist/concurrency/types.js +2 -0
  191. package/dist/concurrency/types.js.map +1 -0
  192. package/dist/format.d.ts +69 -2
  193. package/dist/format.d.ts.map +1 -1
  194. package/dist/format.js +198 -1
  195. package/dist/format.js.map +1 -1
  196. package/dist/moderator/__tests__/evaluate.test.js +31 -17
  197. package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
  198. package/dist/moderator/evaluate.d.ts.map +1 -1
  199. package/dist/moderator/evaluate.js +4 -16
  200. package/dist/moderator/evaluate.js.map +1 -1
  201. package/dist/moderator/index.d.ts +1 -2
  202. package/dist/moderator/index.d.ts.map +1 -1
  203. package/dist/moderator/index.js +0 -1
  204. package/dist/moderator/index.js.map +1 -1
  205. package/dist/moderator/types.d.ts +6 -10
  206. package/dist/moderator/types.d.ts.map +1 -1
  207. package/dist/moderator/types.js +1 -3
  208. package/dist/moderator/types.js.map +1 -1
  209. package/dist/output-mappers.d.ts +122 -0
  210. package/dist/output-mappers.d.ts.map +1 -0
  211. package/dist/output-mappers.js +134 -0
  212. package/dist/output-mappers.js.map +1 -0
  213. package/dist/schemas.d.ts +6 -1
  214. package/dist/schemas.d.ts.map +1 -1
  215. package/dist/schemas.js +34 -5
  216. package/dist/schemas.js.map +1 -1
  217. package/dist/store.d.ts +28 -9
  218. package/dist/store.d.ts.map +1 -1
  219. package/dist/store.js +75 -16
  220. package/dist/store.js.map +1 -1
  221. package/dist/text-renderers.d.ts +30 -0
  222. package/dist/text-renderers.d.ts.map +1 -0
  223. package/dist/text-renderers.js +251 -0
  224. package/dist/text-renderers.js.map +1 -0
  225. package/dist/validate-semantic.d.ts.map +1 -1
  226. package/dist/validate-semantic.js +95 -61
  227. package/dist/validate-semantic.js.map +1 -1
  228. package/dist/validate.d.ts +6 -0
  229. package/dist/validate.d.ts.map +1 -1
  230. package/dist/validate.js +24 -0
  231. package/dist/validate.js.map +1 -1
  232. package/examples/brainstorm.yaml +130 -0
  233. package/examples/debate.yaml +169 -0
  234. package/examples/socratic-questioning.yaml +112 -0
  235. package/package.json +9 -10
  236. package/src/__tests__/adapter-json-roundtrip.test.ts +16 -7
  237. package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
  238. package/src/__tests__/build-step-entry.test.ts +203 -0
  239. package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
  240. package/src/__tests__/concurrency.test.ts +266 -0
  241. package/src/__tests__/config.test.ts +33 -321
  242. package/src/__tests__/current-role.test.ts +7 -6
  243. package/src/__tests__/e2e-mock-agent.test.ts +65 -30
  244. package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
  245. package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
  246. package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
  247. package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
  248. package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
  249. package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
  250. package/src/__tests__/format-text-default.test.ts +49 -0
  251. package/src/__tests__/format-text-registry.test.ts +173 -0
  252. package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
  253. package/src/__tests__/log-text-renderer.test.ts +294 -0
  254. package/src/__tests__/moderator-evaluate.test.ts +9 -52
  255. package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
  256. package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
  257. package/src/__tests__/pid-recycling.test.ts +329 -0
  258. package/src/__tests__/prompt.test.ts +443 -2
  259. package/src/__tests__/resolve-head-hash.test.ts +11 -4
  260. package/src/__tests__/setup-agent-discovery.test.ts +26 -51
  261. package/src/__tests__/setup-complexity.test.ts +1 -203
  262. package/src/__tests__/setup-no-llm.test.ts +68 -0
  263. package/src/__tests__/solve-issue-tea-worktree.test.ts +27 -31
  264. package/src/__tests__/step-ask.test.ts +677 -0
  265. package/src/__tests__/step-show-json.test.ts +1 -0
  266. package/src/__tests__/step-timing.test.ts +2 -0
  267. package/src/__tests__/store-global-cas.test.ts +2 -2
  268. package/src/__tests__/store-unified-threads.test.ts +30 -27
  269. package/src/__tests__/thread-cancel-status.test.ts +27 -20
  270. package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
  271. package/src/__tests__/thread-list-filters.test.ts +443 -17
  272. package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
  273. package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
  274. package/src/__tests__/thread-poke.test.ts +554 -0
  275. package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
  276. package/src/__tests__/thread-resume.test.ts +20 -15
  277. package/src/__tests__/thread-show-status.test.ts +17 -29
  278. package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
  279. package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
  280. package/src/__tests__/thread-suspend-step.test.ts +13 -16
  281. package/src/__tests__/thread-suspended-display.test.ts +10 -22
  282. package/src/__tests__/thread-test-helpers.ts +15 -1
  283. package/src/__tests__/thread.test.ts +14 -14
  284. package/src/__tests__/validate-semantic.test.ts +118 -33
  285. package/src/__tests__/workflow-list-recursive.test.ts +370 -0
  286. package/src/__tests__/workflow-resolution.test.ts +48 -29
  287. package/src/__tests__/workflow-show-resolution.test.ts +286 -0
  288. package/src/__tests__/workflow-validate.test.ts +828 -0
  289. package/src/__tests__/write-envelope.test.ts +257 -0
  290. package/src/background/background.ts +88 -6
  291. package/src/background/index.ts +2 -0
  292. package/src/background/types.ts +1 -0
  293. package/src/cli.ts +184 -77
  294. package/src/commands/config.ts +16 -33
  295. package/src/commands/prompt.ts +57 -31
  296. package/src/commands/setup.ts +80 -358
  297. package/src/commands/step.ts +339 -12
  298. package/src/commands/thread.ts +511 -171
  299. package/src/commands/workflow.ts +155 -4
  300. package/src/concurrency/concurrency.ts +245 -0
  301. package/src/concurrency/index.ts +10 -0
  302. package/src/concurrency/types.ts +19 -0
  303. package/src/format.ts +282 -2
  304. package/src/moderator/__tests__/evaluate.test.ts +34 -17
  305. package/src/moderator/evaluate.ts +5 -17
  306. package/src/moderator/index.ts +1 -6
  307. package/src/moderator/types.ts +6 -14
  308. package/src/output-mappers.ts +254 -0
  309. package/src/schemas.ts +51 -5
  310. package/src/store.ts +86 -20
  311. package/src/text-renderers.ts +355 -0
  312. package/src/validate-semantic.ts +125 -73
  313. package/src/validate.ts +27 -0
  314. package/dist/__tests__/setup-validate.test.d.ts +0 -2
  315. package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
  316. package/dist/__tests__/setup-validate.test.js +0 -108
  317. package/dist/__tests__/setup-validate.test.js.map +0 -1
  318. package/src/__tests__/setup-validate.test.ts +0 -148
  319. /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
@@ -1,11 +1,11 @@
1
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { CasRef, ThreadId } from "@united-workforce/protocol";
5
5
  import { createThreadIndexEntry } from "@united-workforce/protocol";
6
6
  import { extractUlidTimestamp, generateUlid } from "@united-workforce/util";
7
7
  import { afterEach, beforeEach, describe, expect, test } from "vitest";
8
- import { createMarker, deleteMarker } from "../background/index.js";
8
+ import { createMarker, deleteMarker, getProcessStartTime } from "../background/index.js";
9
9
  import { cmdThreadList } from "../commands/thread.js";
10
10
  import { parseTimeInput } from "../commands/thread-time-parser.js";
11
11
  import type { UwfStore } from "../store.js";
@@ -13,19 +13,13 @@ import {
13
13
  completeThread as completeThreadInStore,
14
14
  createUwfStore,
15
15
  loadAllThreads,
16
+ saveWorkflowRegistry,
16
17
  setThread,
17
18
  } from "../store.js";
19
+ import { makeUwfStore } from "./thread-test-helpers.js";
18
20
 
19
21
  // ── helpers ───────────────────────────────────────────────────────────────────
20
22
 
21
- async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
22
- const casDir = join(storageRoot, "cas");
23
- await mkdir(casDir, { recursive: true });
24
- // Set OCAS_HOME to use the test's CAS directory
25
- process.env.OCAS_HOME = casDir;
26
- return createUwfStore(storageRoot);
27
- }
28
-
29
23
  async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
30
24
  const workflowPayload = {
31
25
  name: "test-workflow",
@@ -66,6 +60,7 @@ async function markThreadRunning(storageRoot: string, threadId: ThreadId, workfl
66
60
  workflow,
67
61
  pid: process.pid, // Use current process PID so isPidAlive returns true
68
62
  startedAt: Date.now(),
63
+ processStartTime: getProcessStartTime(process.pid),
69
64
  });
70
65
  }
71
66
 
@@ -76,18 +71,25 @@ async function completeThread(
76
71
  _headHash: CasRef,
77
72
  ) {
78
73
  const uwfIdx = await createUwfStore(storageRoot);
79
- completeThreadInStore(uwfIdx.varStore, threadId, "completed");
74
+ completeThreadInStore(uwfIdx.varStore, threadId, "end");
80
75
  }
81
76
 
82
77
  // ── test setup ────────────────────────────────────────────────────────────────
83
78
 
84
79
  let tmpDir: string;
80
+ let savedOcasHome: string | undefined;
85
81
 
86
82
  beforeEach(async () => {
83
+ savedOcasHome = process.env.OCAS_HOME;
87
84
  tmpDir = await mkdtemp(join(tmpdir(), "thread-list-filters-test-"));
88
85
  });
89
86
 
90
87
  afterEach(async () => {
88
+ if (savedOcasHome === undefined) {
89
+ delete process.env.OCAS_HOME;
90
+ } else {
91
+ process.env.OCAS_HOME = savedOcasHome;
92
+ }
91
93
  await rm(tmpDir, { recursive: true, force: true });
92
94
  });
93
95
 
@@ -135,7 +137,7 @@ describe("cmdThreadList status filter", () => {
135
137
  if (thread3Head === undefined) throw new Error("thread3 head not found");
136
138
  await completeThread(tmpDir, thread3, workflowHash, thread3Head);
137
139
 
138
- const result = await cmdThreadList(tmpDir, ["idle", "completed"], null, null, null, null);
140
+ const result = await cmdThreadList(tmpDir, ["idle", "end"], null, null, null, null);
139
141
 
140
142
  // Clean up marker
141
143
  await deleteMarker(tmpDir, thread2);
@@ -160,14 +162,14 @@ describe("cmdThreadList status filter", () => {
160
162
  if (thread3Head === undefined) throw new Error("thread3 head not found");
161
163
  await completeThread(tmpDir, thread3, workflowHash, thread3Head);
162
164
 
163
- const result = await cmdThreadList(tmpDir, ["completed"], null, null, null, null);
165
+ const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null);
164
166
 
165
167
  expect(result).toHaveLength(1);
166
168
  expect(result[0]?.thread).toBe(thread3);
167
- expect(result[0]?.status).toBe("completed");
169
+ expect(result[0]?.status).toBe("end");
168
170
  });
169
171
 
170
- test("should return all threads when no status filter provided", async () => {
172
+ test("should return only active threads when no filter and no --all", async () => {
171
173
  const uwf = await makeUwfStore(tmpDir);
172
174
  const workflowHash = await createTestWorkflow(uwf);
173
175
 
@@ -185,8 +187,290 @@ describe("cmdThreadList status filter", () => {
185
187
 
186
188
  const result = await cmdThreadList(tmpDir, null, null, null, null, null);
187
189
 
190
+ // Default behavior (issue #147): only active threads (idle + running)
191
+ expect(result).toHaveLength(2);
192
+ expect(result.map((r) => r.thread).sort()).toEqual([thread1, thread2].sort());
193
+
194
+ // Clean up marker
195
+ await deleteMarker(tmpDir, thread2);
196
+ });
197
+
198
+ test("should return all threads when --all (showAll=true)", async () => {
199
+ const uwf = await makeUwfStore(tmpDir);
200
+ const workflowHash = await createTestWorkflow(uwf);
201
+
202
+ const thread1 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
203
+ const thread2 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
204
+ const thread3 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
205
+
206
+ await markThreadRunning(tmpDir, thread2, workflowHash);
207
+
208
+ const uwfIdx = await createUwfStore(tmpDir);
209
+ const index = loadAllThreads(uwfIdx.varStore);
210
+ const thread3Head = index[thread3]!.head;
211
+ if (thread3Head === undefined) throw new Error("thread3 head not found");
212
+ await completeThread(tmpDir, thread3, workflowHash, thread3Head);
213
+
214
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
215
+
188
216
  expect(result).toHaveLength(3);
189
217
  expect(result.map((r) => r.thread).sort()).toEqual([thread1, thread2, thread3].sort());
218
+
219
+ // Clean up marker
220
+ await deleteMarker(tmpDir, thread2);
221
+ });
222
+ });
223
+
224
+ // ── default behavior tests (issue #147) ───────────────────────────────────────
225
+
226
+ describe("cmdThreadList default behavior (issue #147)", () => {
227
+ test("default returns only idle + running threads", async () => {
228
+ const uwf = await makeUwfStore(tmpDir);
229
+ const workflowHash = await createTestWorkflow(uwf);
230
+
231
+ const threadA = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
232
+ const threadB = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
233
+ const threadC = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
234
+ const threadD = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
235
+
236
+ await markThreadRunning(tmpDir, threadB, workflowHash);
237
+
238
+ const uwfIdx = await createUwfStore(tmpDir);
239
+ const index = loadAllThreads(uwfIdx.varStore);
240
+ const threadCHead = index[threadC]!.head;
241
+ if (threadCHead === undefined) throw new Error("threadC head not found");
242
+ await completeThread(tmpDir, threadC, workflowHash, threadCHead);
243
+
244
+ // Cancel threadD
245
+ const threadDHead = index[threadD]!.head;
246
+ if (threadDHead === undefined) throw new Error("threadD head not found");
247
+ const uwfCancel = await createUwfStore(tmpDir);
248
+ completeThreadInStore(uwfCancel.varStore, threadD, "cancelled");
249
+
250
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null);
251
+
252
+ expect(result).toHaveLength(2);
253
+ expect(result.map((r) => r.thread).sort()).toEqual([threadA, threadB].sort());
254
+
255
+ await deleteMarker(tmpDir, threadB);
256
+ });
257
+
258
+ test("default excludes completed threads", async () => {
259
+ const uwf = await makeUwfStore(tmpDir);
260
+ const workflowHash = await createTestWorkflow(uwf);
261
+
262
+ const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 6000);
263
+ const completedThreads: ThreadId[] = [];
264
+ for (let i = 0; i < 5; i++) {
265
+ const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (5 - i) * 1000);
266
+ completedThreads.push(t);
267
+ const uwfIdx = await createUwfStore(tmpDir);
268
+ const index = loadAllThreads(uwfIdx.varStore);
269
+ const head = index[t]!.head;
270
+ if (head === undefined) throw new Error("head not found");
271
+ await completeThread(tmpDir, t, workflowHash, head);
272
+ }
273
+
274
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null);
275
+
276
+ expect(result).toHaveLength(1);
277
+ expect(result[0]?.thread).toBe(idleThread);
278
+ });
279
+
280
+ test("default excludes cancelled threads", async () => {
281
+ const uwf = await makeUwfStore(tmpDir);
282
+ const workflowHash = await createTestWorkflow(uwf);
283
+
284
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
285
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
286
+
287
+ const cancelled: ThreadId[] = [];
288
+ for (let i = 0; i < 3; i++) {
289
+ const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (3 - i) * 1000);
290
+ cancelled.push(t);
291
+ const uwfIdx = await createUwfStore(tmpDir);
292
+ completeThreadInStore(uwfIdx.varStore, t, "cancelled");
293
+ }
294
+
295
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null);
296
+
297
+ expect(result).toHaveLength(1);
298
+ expect(result[0]?.thread).toBe(runningThread);
299
+
300
+ await deleteMarker(tmpDir, runningThread);
301
+ });
302
+
303
+ test("--all (showAll=true) returns every status", async () => {
304
+ const uwf = await makeUwfStore(tmpDir);
305
+ const workflowHash = await createTestWorkflow(uwf);
306
+
307
+ const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
308
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
309
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
310
+
311
+ const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
312
+ const uwfIdx = await createUwfStore(tmpDir);
313
+ const idx = loadAllThreads(uwfIdx.varStore);
314
+ const ch = idx[completedThread]!.head;
315
+ if (ch === undefined) throw new Error("completedThread head not found");
316
+ await completeThread(tmpDir, completedThread, workflowHash, ch);
317
+
318
+ const cancelledThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
319
+ completeThreadInStore(uwfIdx.varStore, cancelledThread, "cancelled");
320
+
321
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
322
+
323
+ expect(result).toHaveLength(4);
324
+ expect(result.map((r) => r.thread).sort()).toEqual(
325
+ [idleThread, runningThread, completedThread, cancelledThread].sort(),
326
+ );
327
+
328
+ await deleteMarker(tmpDir, runningThread);
329
+ });
330
+
331
+ test("explicit --status overrides default (still returns just the filtered statuses)", async () => {
332
+ const uwf = await makeUwfStore(tmpDir);
333
+ const workflowHash = await createTestWorkflow(uwf);
334
+
335
+ const _idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
336
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
337
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
338
+
339
+ const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
340
+ const uwfIdx = await createUwfStore(tmpDir);
341
+ const idx = loadAllThreads(uwfIdx.varStore);
342
+ const ch = idx[completedThread]!.head;
343
+ if (ch === undefined) throw new Error("completedThread head not found");
344
+ await completeThread(tmpDir, completedThread, workflowHash, ch);
345
+
346
+ const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null);
347
+
348
+ expect(result).toHaveLength(1);
349
+ expect(result[0]?.thread).toBe(completedThread);
350
+ expect(result[0]?.status).toBe("end");
351
+
352
+ await deleteMarker(tmpDir, runningThread);
353
+ });
354
+
355
+ test("--status active keeps working", async () => {
356
+ const uwf = await makeUwfStore(tmpDir);
357
+ const workflowHash = await createTestWorkflow(uwf);
358
+
359
+ const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
360
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
361
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
362
+
363
+ const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
364
+ const uwfIdx = await createUwfStore(tmpDir);
365
+ const idx = loadAllThreads(uwfIdx.varStore);
366
+ const ch = idx[completedThread]!.head;
367
+ if (ch === undefined) throw new Error("completedThread head not found");
368
+ await completeThread(tmpDir, completedThread, workflowHash, ch);
369
+
370
+ const result = await cmdThreadList(tmpDir, ["idle", "running"], null, null, null, null);
371
+
372
+ expect(result).toHaveLength(2);
373
+ expect(result.map((r) => r.thread).sort()).toEqual([idleThread, runningThread].sort());
374
+
375
+ await deleteMarker(tmpDir, runningThread);
376
+ });
377
+
378
+ test("--status + --all — explicit status wins", async () => {
379
+ const uwf = await makeUwfStore(tmpDir);
380
+ const workflowHash = await createTestWorkflow(uwf);
381
+
382
+ const _idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
383
+ const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
384
+ await markThreadRunning(tmpDir, runningThread, workflowHash);
385
+
386
+ const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
387
+ const uwfIdx = await createUwfStore(tmpDir);
388
+ const idx = loadAllThreads(uwfIdx.varStore);
389
+ const ch = idx[completedThread]!.head;
390
+ if (ch === undefined) throw new Error("completedThread head not found");
391
+ await completeThread(tmpDir, completedThread, workflowHash, ch);
392
+
393
+ const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null, true);
394
+
395
+ expect(result).toHaveLength(1);
396
+ expect(result[0]?.thread).toBe(completedThread);
397
+
398
+ await deleteMarker(tmpDir, runningThread);
399
+ });
400
+
401
+ test("default returns empty when no threads", async () => {
402
+ await makeUwfStore(tmpDir);
403
+
404
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null);
405
+
406
+ expect(result).toHaveLength(0);
407
+ });
408
+
409
+ test("default + time range filter composes correctly", async () => {
410
+ const uwf = await makeUwfStore(tmpDir);
411
+ const workflowHash = await createTestWorkflow(uwf);
412
+
413
+ const ts1 = Date.UTC(2026, 4, 20, 0, 0, 0);
414
+ const ts2 = Date.UTC(2026, 4, 21, 0, 0, 0);
415
+ const ts3 = Date.UTC(2026, 4, 22, 0, 0, 0);
416
+ const ts4 = Date.UTC(2026, 4, 23, 0, 0, 0);
417
+ const ts5 = Date.UTC(2026, 4, 24, 0, 0, 0);
418
+
419
+ const _t1 = await createTestThread(uwf, tmpDir, workflowHash, ts1);
420
+ const t2 = await createTestThread(uwf, tmpDir, workflowHash, ts2);
421
+ const t3 = await createTestThread(uwf, tmpDir, workflowHash, ts3);
422
+ const t4 = await createTestThread(uwf, tmpDir, workflowHash, ts4);
423
+ const _t5 = await createTestThread(uwf, tmpDir, workflowHash, ts5);
424
+
425
+ // Mark t3 running
426
+ await markThreadRunning(tmpDir, t3, workflowHash);
427
+
428
+ // Complete t4 (should be excluded by default)
429
+ const uwfIdx = await createUwfStore(tmpDir);
430
+ const idx = loadAllThreads(uwfIdx.varStore);
431
+ const t4head = idx[t4]!.head;
432
+ if (t4head === undefined) throw new Error("t4 head not found");
433
+ await completeThread(tmpDir, t4, workflowHash, t4head);
434
+
435
+ // afterMs in middle of range to exclude _t1
436
+ const afterMs = Date.UTC(2026, 4, 20, 12, 0, 0);
437
+ const result = await cmdThreadList(tmpDir, null, afterMs, null, null, null);
438
+
439
+ // Expected: t2 (idle), t3 (running), _t5 (idle); excludes t4 (completed) and _t1 (filtered by time)
440
+ expect(result).toHaveLength(3);
441
+ const ids = result.map((r) => r.thread).sort();
442
+ expect(ids).toEqual([t2, t3, _t5].sort());
443
+
444
+ await deleteMarker(tmpDir, t3);
445
+ });
446
+
447
+ test("default + pagination composes correctly", async () => {
448
+ const uwf = await makeUwfStore(tmpDir);
449
+ const workflowHash = await createTestWorkflow(uwf);
450
+
451
+ // Create 10 idle threads + 5 completed threads
452
+ const idleThreads: ThreadId[] = [];
453
+ for (let i = 0; i < 10; i++) {
454
+ idleThreads.push(
455
+ await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (15 - i) * 1000),
456
+ );
457
+ }
458
+ for (let i = 0; i < 5; i++) {
459
+ const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (5 - i) * 1000);
460
+ const uwfIdx = await createUwfStore(tmpDir);
461
+ const idx = loadAllThreads(uwfIdx.varStore);
462
+ const head = idx[t]!.head;
463
+ if (head === undefined) throw new Error("head not found");
464
+ await completeThread(tmpDir, t, workflowHash, head);
465
+ }
466
+
467
+ const result = await cmdThreadList(tmpDir, null, null, null, 2, 3);
468
+
469
+ expect(result).toHaveLength(3);
470
+ // All results should be idle (default excludes completed)
471
+ for (const r of result) {
472
+ expect(r.status).toBe("idle");
473
+ }
190
474
  });
191
475
  });
192
476
 
@@ -382,11 +666,11 @@ describe("combined filters", () => {
382
666
  await completeThread(tmpDir, thread, workflowHash, headHash);
383
667
  }
384
668
 
385
- const result = await cmdThreadList(tmpDir, ["completed"], null, null, 3, 5);
669
+ const result = await cmdThreadList(tmpDir, ["end"], null, null, 3, 5);
386
670
 
387
671
  expect(result).toHaveLength(5);
388
672
  for (const r of result) {
389
- expect(r.status).toBe("completed");
673
+ expect(r.status).toBe("end");
390
674
  }
391
675
  });
392
676
 
@@ -570,3 +854,145 @@ describe("ISO date parsing", () => {
570
854
  expect(() => parseTimeInput("invalid", nowMs)).toThrow();
571
855
  });
572
856
  });
857
+
858
+ // ── corrupt thread resilience (#250) ──────────────────────────────────────────
859
+
860
+ describe("corrupt thread resilience (#250)", () => {
861
+ test("thread list returns corrupt entry when CAS node is missing", async () => {
862
+ const uwf = await makeUwfStore(tmpDir);
863
+
864
+ // Create a valid thread
865
+ const workflowHash = await createTestWorkflow(uwf);
866
+ const now = Date.now();
867
+ const _validId = await createTestThread(uwf, tmpDir, workflowHash, now);
868
+
869
+ // Create another thread with a unique start node, then delete its workflow CAS to corrupt it
870
+ const corruptThreadId = generateUlid(now + 1000) as ThreadId;
871
+ const startPayload = {
872
+ workflow: workflowHash,
873
+ prompt: "corrupt thread prompt — unique to avoid CAS hash collision",
874
+ cwd: tmpDir,
875
+ };
876
+ const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
877
+ setThread(uwf.varStore, corruptThreadId, createThreadIndexEntry(headHash));
878
+
879
+ // Delete the workflow CAS node — start node still exists but workflow ref dangles
880
+ uwf.store.cas.delete(workflowHash);
881
+
882
+ // thread list should NOT throw — it should return both threads
883
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
884
+
885
+ // Both threads should appear (the valid one is now also corrupt since workflow is shared)
886
+ // In practice: both become corrupt because they share the same workflow CAS node
887
+ // This matches the real scenario from issue #250 — gc deleted a shared node
888
+ expect(result.length).toBeGreaterThanOrEqual(2);
889
+ const corruptItems = result.filter((r) => r.status === "corrupt");
890
+ expect(corruptItems.length).toBeGreaterThanOrEqual(1);
891
+ for (const item of corruptItems) {
892
+ expect(item.statusDisplay).toBe("corrupt");
893
+ }
894
+ });
895
+
896
+ test("corrupt threads appear in default filter (without --all)", async () => {
897
+ const uwf = await makeUwfStore(tmpDir);
898
+ const workflowHash = await createTestWorkflow(uwf);
899
+
900
+ // Create a thread then corrupt it
901
+ const corruptId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
902
+ const corruptEntry = loadAllThreads(uwf.varStore)[corruptId];
903
+ uwf.store.cas.delete(corruptEntry.head);
904
+
905
+ // Default filter (no --all, no --status) should include corrupt
906
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
907
+ expect(result).toHaveLength(1);
908
+ expect(result[0].status).toBe("corrupt");
909
+ });
910
+ });
911
+
912
+ // ── orphan thread detection (#286) ────────────────────────────────────────────
913
+
914
+ describe("orphan thread detection (#286)", () => {
915
+ test("thread list includes workflowName when workflow is in registry", async () => {
916
+ const uwf = await makeUwfStore(tmpDir);
917
+ const workflowHash = await createTestWorkflow(uwf);
918
+
919
+ // Register the workflow in registry
920
+ saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
921
+
922
+ const threadId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
923
+
924
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
925
+ expect(result).toHaveLength(1);
926
+ expect(result[0].thread).toBe(threadId);
927
+ expect(result[0].workflowName).toBe("test-workflow");
928
+ });
929
+
930
+ test("thread list returns workflowName: null for orphaned threads", async () => {
931
+ const uwf = await makeUwfStore(tmpDir);
932
+ const workflowHash = await createTestWorkflow(uwf);
933
+
934
+ // Do NOT register the workflow — thread is orphaned
935
+ const threadId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
936
+
937
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
938
+ expect(result).toHaveLength(1);
939
+ expect(result[0].thread).toBe(threadId);
940
+ expect(result[0].workflowName).toBeNull();
941
+ });
942
+
943
+ test("mixed registered and orphaned threads in the same list", async () => {
944
+ const uwf = await makeUwfStore(tmpDir);
945
+ const workflowHash = await createTestWorkflow(uwf);
946
+
947
+ // Register the workflow
948
+ saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
949
+
950
+ // Create a thread using the registered workflow
951
+ const now = Date.now();
952
+ const registeredId = await createTestThread(uwf, tmpDir, workflowHash, now);
953
+
954
+ // Create a second workflow (different hash), not registered
955
+ const orphanWorkflowPayload = {
956
+ name: "orphan-workflow",
957
+ roles: {
958
+ role1: {
959
+ goal: "orphan goal",
960
+ outputSchema: { type: "object" as const, properties: {} },
961
+ },
962
+ },
963
+ graph: { start: "role1" },
964
+ conditions: {},
965
+ };
966
+ const orphanHash = await uwf.store.cas.put(uwf.schemas.workflow, orphanWorkflowPayload);
967
+ const orphanId = await createTestThread(uwf, tmpDir, orphanHash, now + 1000);
968
+
969
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
970
+ expect(result).toHaveLength(2);
971
+
972
+ // Sorted newest first, so orphan (later timestamp) comes first
973
+ const orphanItem = result.find((r) => r.thread === orphanId);
974
+ const registeredItem = result.find((r) => r.thread === registeredId);
975
+
976
+ expect(orphanItem).toBeDefined();
977
+ expect(orphanItem!.workflowName).toBeNull();
978
+
979
+ expect(registeredItem).toBeDefined();
980
+ expect(registeredItem!.workflowName).toBe("test-workflow");
981
+ });
982
+
983
+ test("corrupt threads have workflowName: null", async () => {
984
+ const uwf = await makeUwfStore(tmpDir);
985
+ const workflowHash = await createTestWorkflow(uwf);
986
+ saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
987
+
988
+ // Create a thread then corrupt it by deleting its head CAS node
989
+ const corruptId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
990
+ const corruptEntry = loadAllThreads(uwf.varStore)[corruptId];
991
+ uwf.store.cas.delete(corruptEntry.head);
992
+
993
+ const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
994
+ expect(result).toHaveLength(1);
995
+ expect(result[0].status).toBe("corrupt");
996
+ expect(result[0].workflowName).toBeNull();
997
+ });
998
+ });
@@ -0,0 +1,110 @@
1
+ import { OUTPUT_TEMPLATES } from "@united-workforce/protocol";
2
+ import { Liquid } from "liquidjs";
3
+ import { describe, expect, test } from "vitest";
4
+
5
+ /**
6
+ * Issue #351 — `uwf thread list --format text` rendered the `STARTED` column
7
+ * as `58414-12-06` because `THREAD_LIST_TEMPLATE` piped `item.startedAt` (Unix
8
+ * **ms** per `THREAD_LIST_OUTPUT_SCHEMA`) directly into LiquidJS's `| date`
9
+ * filter, which expects Unix **seconds**.
10
+ *
11
+ * This integration test renders the template against a known ms timestamp
12
+ * and asserts the year falls within the realistic 20xx range, confirming
13
+ * the ms→s conversion is in place at the protocol layer.
14
+ */
15
+
16
+ function makeEngine(): Liquid {
17
+ return new Liquid({ cache: false, strictFilters: false, strictVariables: false });
18
+ }
19
+
20
+ describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", () => {
21
+ test("renders item.startedAt=1781229932779 as a 2026 calendar date (not 58414)", async () => {
22
+ const engine = makeEngine();
23
+ const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], {
24
+ items: [
25
+ {
26
+ threadId: "01K5HMKZQB7VDA8E2K9P3R5XBC",
27
+ workflowHash: "WF1234567890A",
28
+ workflowName: null,
29
+ status: "idle",
30
+ currentRole: "planner",
31
+ startedAt: 1781229932779,
32
+ completedAt: null,
33
+ },
34
+ ],
35
+ });
36
+
37
+ expect(out).not.toContain("58414");
38
+ expect(out).toMatch(/\b20\d{2}-\d{2}-\d{2}\b/);
39
+ // The STARTED cell must NOT begin with a 5-digit year.
40
+ expect(out).not.toMatch(/\b\d{5}-\d{2}-\d{2}\b/);
41
+ });
42
+
43
+ test("renders `-` for items with startedAt=null (null guard preserved)", async () => {
44
+ const engine = makeEngine();
45
+ const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], {
46
+ items: [
47
+ {
48
+ threadId: "01K5HMKZQB7VDA8E2K9P3R5XBC",
49
+ workflowHash: "WF1234567890A",
50
+ workflowName: null,
51
+ status: "idle",
52
+ currentRole: "planner",
53
+ startedAt: null,
54
+ completedAt: null,
55
+ },
56
+ ],
57
+ });
58
+
59
+ expect(out).not.toContain("58414");
60
+ expect(out).not.toContain("Invalid Date");
61
+ expect(out).not.toContain("1970-01-01");
62
+ // Last token of the row is the rendered STARTED cell — must be `-`.
63
+ const dataRow = out
64
+ .split("\n")
65
+ .find((line: string) => line.includes("01K5HMKZQB7VDA8E2K9P3R5XBC"));
66
+ expect(dataRow).toBeDefined();
67
+ expect(dataRow?.trimEnd().endsWith("-")).toBe(true);
68
+ });
69
+
70
+ test("renders multiple ms timestamps across years 2020–2030 with correct year prefix", async () => {
71
+ const engine = makeEngine();
72
+ const items = [
73
+ {
74
+ threadId: "ID1",
75
+ workflowHash: "WF",
76
+ workflowName: null,
77
+ status: "idle",
78
+ currentRole: null,
79
+ startedAt: Date.UTC(2020, 0, 1, 0, 0, 0),
80
+ completedAt: null,
81
+ },
82
+ {
83
+ threadId: "ID2",
84
+ workflowHash: "WF",
85
+ workflowName: null,
86
+ status: "idle",
87
+ currentRole: null,
88
+ startedAt: Date.UTC(2026, 5, 12, 5, 25, 0),
89
+ completedAt: null,
90
+ },
91
+ {
92
+ threadId: "ID3",
93
+ workflowHash: "WF",
94
+ workflowName: null,
95
+ status: "idle",
96
+ currentRole: null,
97
+ startedAt: Date.UTC(2030, 11, 31, 23, 59, 0),
98
+ completedAt: null,
99
+ },
100
+ ];
101
+
102
+ const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], { items });
103
+
104
+ expect(out).toContain("2020-");
105
+ expect(out).toContain("2026-");
106
+ expect(out).toContain("2030-");
107
+ expect(out).not.toContain("58414");
108
+ expect(out).not.toMatch(/\b\d{5}-\d{2}-\d{2}\b/);
109
+ });
110
+ });