@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
@@ -78,9 +78,6 @@ afterEach(async () => {
78
78
  async function writeMockConfig(mockDataFixture: string): Promise<void> {
79
79
  const config = {
80
80
  defaultAgent: "mock",
81
- defaultModel: "test",
82
- providers: {},
83
- models: {},
84
81
  agentOverrides: null,
85
82
  agents: {
86
83
  mock: {
@@ -107,7 +104,7 @@ async function addWorkflow(workflowFixture: string, workflowName: string): Promi
107
104
  type ExecResult = { stdout: string; stderr: string; exitCode: number };
108
105
 
109
106
  function runExec(threadId: string, count: number | null = null): ExecResult {
110
- const args = [CLI_PATH, "thread", "exec", threadId];
107
+ const args = [CLI_PATH, "--format", "raw-json", "thread", "exec", threadId];
111
108
  if (count !== null) {
112
109
  args.push("--count", String(count));
113
110
  }
@@ -135,7 +132,7 @@ function runResume(threadId: string, prompt: string): ExecResult {
135
132
  try {
136
133
  const stdout = execFileSync(
137
134
  process.execPath,
138
- [CLI_PATH, "thread", "resume", threadId, "-p", prompt],
135
+ [CLI_PATH, "--format", "raw-json", "thread", "resume", threadId, "-p", prompt],
139
136
  {
140
137
  encoding: "utf8",
141
138
  stdio: ["ignore", "pipe", "pipe"],
@@ -165,12 +162,49 @@ type StepOutputJson = {
165
162
  done: boolean;
166
163
  };
167
164
 
165
+ /**
166
+ * The new `thread exec` envelope value (under --format raw-json) is
167
+ * `{ threadId, workflowHash, steps: [...] }`. Tests still want the
168
+ * single-step shape, so we project each step entry back into the legacy
169
+ * StepOutputJson shape.
170
+ */
171
+ type ThreadExecRawValue = {
172
+ threadId: string;
173
+ workflowHash: string;
174
+ steps: Array<{
175
+ head: string;
176
+ status: string;
177
+ currentRole: string | null;
178
+ done: boolean;
179
+ role?: string | null;
180
+ suspendedRole: string | null;
181
+ suspendMessage: string | null;
182
+ }>;
183
+ };
184
+
185
+ function projectStep(envelope: ThreadExecRawValue, idx: number): StepOutputJson {
186
+ const step = envelope.steps[idx];
187
+ if (step === undefined) {
188
+ throw new Error(`thread exec envelope has no step at index ${idx}`);
189
+ }
190
+ return {
191
+ thread: envelope.threadId,
192
+ head: step.head,
193
+ status: step.status,
194
+ currentRole: step.currentRole,
195
+ suspendedRole: step.suspendedRole,
196
+ suspendMessage: step.suspendMessage,
197
+ done: step.done,
198
+ };
199
+ }
200
+
168
201
  function execStep(threadId: string): StepOutputJson {
169
202
  const { stdout, stderr, exitCode } = runExec(threadId);
170
203
  if (exitCode !== 0) {
171
204
  throw new Error(`thread exec failed (code ${exitCode})\nstdout: ${stdout}\nstderr: ${stderr}`);
172
205
  }
173
- return JSON.parse(stdout.trim()) as StepOutputJson;
206
+ const envelope = JSON.parse(stdout.trim()) as ThreadExecRawValue;
207
+ return projectStep(envelope, 0);
174
208
  }
175
209
 
176
210
  function getStepNode(store: Awaited<ReturnType<typeof openStore>>, hash: string): StepNodePayload {
@@ -187,7 +221,7 @@ function getStatus(store: Awaited<ReturnType<typeof openStore>>, outputRef: CasR
187
221
 
188
222
  // ── scenarios ─────────────────────────────────────────────────────────────────
189
223
 
190
- describe("E2E mock-agent: full uwf pipeline", () => {
224
+ describe("E2E mock-agent: full uwf pipeline", { timeout: 15_000 }, () => {
191
225
  test("1. linear workflow runs planner then worker and reaches $END", async () => {
192
226
  await writeMockConfig("e2e-linear.mock.yaml");
193
227
  const workflowHash = await addWorkflow("e2e-linear.workflow.yaml", "test-linear");
@@ -209,7 +243,7 @@ describe("E2E mock-agent: full uwf pipeline", () => {
209
243
  // Step 2 → worker → $END (thread archived to history).
210
244
  const step2 = execStep(threadId);
211
245
  expect(step2.done).toBe(true);
212
- expect(step2.status).toBe("completed");
246
+ expect(step2.status).toBe("end");
213
247
  expect(step2.currentRole).toBeNull();
214
248
 
215
249
  // Verify CAS chain integrity: start → step1 → step2.
@@ -237,11 +271,11 @@ describe("E2E mock-agent: full uwf pipeline", () => {
237
271
  const startNode = store.cas.get(startHash as CasRef);
238
272
  expect((startNode!.payload as StartNodePayload).workflow).toBe(workflowHash);
239
273
 
240
- // Thread is completed: status changed to "completed", head updated.
274
+ // Thread is completed: status changed to "end", head updated.
241
275
  const uwf = await createUwfStore(uwfHome);
242
276
  const finalEntry = getThread(uwf.varStore, threadId);
243
277
  expect(finalEntry).not.toBeNull();
244
- expect(finalEntry!.status).toBe("completed");
278
+ expect(finalEntry!.status).toBe("end");
245
279
  expect(finalEntry!.head).toBe(step2.head);
246
280
  });
247
281
 
@@ -270,7 +304,7 @@ describe("E2E mock-agent: full uwf pipeline", () => {
270
304
 
271
305
  const s4 = execStep(threadId);
272
306
  expect(s4.done).toBe(true);
273
- expect(s4.status).toBe("completed");
307
+ expect(s4.status).toBe("end");
274
308
 
275
309
  // Verify the chain order and roles.
276
310
  const store = await openStore(casDir);
@@ -302,7 +336,7 @@ describe("E2E mock-agent: full uwf pipeline", () => {
302
336
  const uwf = await createUwfStore(uwfHome);
303
337
  const finalEntry = getThread(uwf.varStore, threadId);
304
338
  expect(finalEntry).not.toBeNull();
305
- expect(finalEntry!.status).toBe("completed");
339
+ expect(finalEntry!.status).toBe("end");
306
340
  });
307
341
 
308
342
  test("3. role mismatch in mock data makes the agent exit with an error", {
@@ -329,7 +363,7 @@ describe("E2E mock-agent: full uwf pipeline", () => {
329
363
  const uwf = await createUwfStore(uwfHome);
330
364
  const entry = getThread(uwf.varStore, threadId);
331
365
  expect(entry).not.toBeNull();
332
- expect(entry!.status).not.toBe("completed");
366
+ expect(entry!.status).not.toBe("end");
333
367
  expect(entry!.head).toBe(step1.head);
334
368
  });
335
369
 
@@ -361,7 +395,7 @@ describe("E2E mock-agent: full uwf pipeline", () => {
361
395
  const resume = runResume(threadId, "Here are the requirements");
362
396
  expect(resume.exitCode).toBe(0);
363
397
  const resumeOut = JSON.parse(resume.stdout.trim()) as StepOutputJson;
364
- expect(resumeOut.status).toBe("completed");
398
+ expect(resumeOut.status).toBe("end");
365
399
  expect(resumeOut.done).toBe(true);
366
400
  expect(resumeOut.currentRole).toBeNull();
367
401
  expect(resumeOut.suspendedRole).toBeNull();
@@ -373,12 +407,12 @@ describe("E2E mock-agent: full uwf pipeline", () => {
373
407
  expect(s1.role).toBe("planner");
374
408
  expect(s2.role).toBe("planner");
375
409
  expect(s2.prev).toBe(step1.head);
376
- expect(getStatus(store, s1.output)).toBe("insufficient_info");
410
+ expect(getStatus(store, s1.output)).toBe("$SUSPEND");
377
411
  expect(getStatus(store, s2.output)).toBe("ready");
378
412
 
379
413
  const finalEntry = getThread((await createUwfStore(uwfHome)).varStore, threadId);
380
414
  expect(finalEntry).not.toBeNull();
381
- expect(finalEntry!.status).toBe("completed");
415
+ expect(finalEntry!.status).toBe("end");
382
416
  expect(finalEntry!.head).toBe(resumeOut.head);
383
417
  });
384
418
 
@@ -395,16 +429,17 @@ describe("E2E mock-agent: full uwf pipeline", () => {
395
429
  const { stdout, stderr, exitCode } = runExec(threadId, 3);
396
430
  expect(exitCode, `stderr: ${stderr}`).toBe(0);
397
431
 
398
- // Multi-step exec emits a JSON array (one entry per executed step).
399
- const results = JSON.parse(stdout.trim()) as StepOutputJson[];
400
- expect(Array.isArray(results)).toBe(true);
401
- expect(results).toHaveLength(3);
432
+ // Multi-step exec emits a single envelope with a `steps` array (one entry per executed step).
433
+ const envelope = JSON.parse(stdout.trim()) as ThreadExecRawValue;
434
+ expect(envelope.steps).toHaveLength(3);
435
+
436
+ const results = [projectStep(envelope, 0), projectStep(envelope, 1), projectStep(envelope, 2)];
402
437
 
403
438
  expect(results[0].status).toBe("idle");
404
439
  expect(results[0].currentRole).toBe("developer");
405
440
  expect(results[1].status).toBe("idle");
406
441
  expect(results[1].currentRole).toBe("reviewer");
407
- expect(results[2].status).toBe("completed");
442
+ expect(results[2].status).toBe("end");
408
443
  expect(results[2].done).toBe(true);
409
444
 
410
445
  // Verify the CAS chain holds 3 step nodes in the correct order.
@@ -420,15 +455,15 @@ describe("E2E mock-agent: full uwf pipeline", () => {
420
455
 
421
456
  const finalEntry = getThread((await createUwfStore(uwfHome)).varStore, threadId);
422
457
  expect(finalEntry).not.toBeNull();
423
- expect(finalEntry!.status).toBe("completed");
458
+ expect(finalEntry!.status).toBe("end");
424
459
  expect(finalEntry!.head).toBe(results[2].head);
425
460
  });
426
461
 
427
- test("6. mustache edge prompt renders planner variables into the worker step", {
462
+ test("6. Liquid edge prompt renders planner variables into the worker step", {
428
463
  timeout: 30_000,
429
464
  }, async () => {
430
- await writeMockConfig("e2e-mustache.mock.yaml");
431
- const workflowHash = await addWorkflow("e2e-mustache.workflow.yaml", "test-mustache");
465
+ await writeMockConfig("e2e-liquid.mock.yaml");
466
+ const workflowHash = await addWorkflow("e2e-liquid.workflow.yaml", "test-liquid");
432
467
 
433
468
  const start = await cmdThreadStart(uwfHome, workflowHash, "Plan the task", uwfHome, tmpDir);
434
469
  const threadId = start.thread;
@@ -441,13 +476,13 @@ describe("E2E mock-agent: full uwf pipeline", () => {
441
476
  // Step 2 → worker; the moderator renders the templated edge prompt before spawning it.
442
477
  const step2 = execStep(threadId);
443
478
  expect(step2.done).toBe(true);
444
- expect(step2.status).toBe("completed");
479
+ expect(step2.status).toBe("end");
445
480
 
446
481
  const store = await openStore(casDir);
447
482
  const plannerStep = getStepNode(store, step1.head);
448
483
  expect(getStatus(store, plannerStep.output)).toBe("ready");
449
484
 
450
- // The worker step's edgePrompt is the mustache-rendered template.
485
+ // The worker step's edgePrompt is the Liquid-rendered template.
451
486
  const workerStep = getStepNode(store, step2.head);
452
487
  expect(workerStep.role).toBe("worker");
453
488
  expect(workerStep.edgePrompt).toContain("fix/42-auth");
@@ -469,12 +504,12 @@ describe("E2E mock-agent: full uwf pipeline", () => {
469
504
  // Step 1: planner outputs ready → $END → thread completed.
470
505
  const step1 = execStep(threadId);
471
506
  expect(step1.done).toBe(true);
472
- expect(step1.status).toBe("completed");
507
+ expect(step1.status).toBe("end");
473
508
 
474
509
  const uwf1 = await createUwfStore(uwfHome);
475
510
  const entry1 = getThread(uwf1.varStore, threadId);
476
511
  expect(entry1).not.toBeNull();
477
- expect(entry1!.status).toBe("completed");
512
+ expect(entry1!.status).toBe("end");
478
513
 
479
514
  // Resume the completed thread — should re-evaluate $START → planner.
480
515
  const resumeResult = runResume(threadId, "Additional context for round 2");
@@ -484,7 +519,7 @@ describe("E2E mock-agent: full uwf pipeline", () => {
484
519
  const uwf2 = await createUwfStore(uwfHome);
485
520
  const entry2 = getThread(uwf2.varStore, threadId);
486
521
  expect(entry2).not.toBeNull();
487
- expect(entry2!.status).toBe("completed");
522
+ expect(entry2!.status).toBe("end");
488
523
  // Head should have advanced (not the same as step1).
489
524
  expect(entry2!.head).not.toBe(step1.head);
490
525
 
@@ -1,3 +1,4 @@
1
+ version: 1
1
2
  name: test-count
2
3
  description: 3-step linear pipeline (analyst -> developer -> reviewer -> $END)
3
4
  roles:
@@ -1,3 +1,4 @@
1
+ version: 1
1
2
  name: test-linear
2
3
  description: Simple 2-step linear test (planner -> worker -> $END)
3
4
  roles:
@@ -1,4 +1,5 @@
1
- name: test-mustache
1
+ version: 1
2
+ name: test-liquid
2
3
  description: Planner emits template variables consumed by the worker edge prompt
3
4
  roles:
4
5
  planner:
@@ -30,6 +31,6 @@ graph:
30
31
  new: { role: planner, prompt: 'Plan the task' }
31
32
  resume: { role: planner, prompt: 'Review the previous run output and continue the work.' }
32
33
  planner:
33
- ready: { role: worker, prompt: 'Work on branch {{{branch}}} in {{{repoPath}}}' }
34
+ ready: { role: worker, prompt: 'Work on branch {{ branch }} in {{ repoPath }}' }
34
35
  worker:
35
36
  done: { role: '$END', prompt: 'Complete' }
@@ -1,3 +1,4 @@
1
+ version: 1
1
2
  name: test-loop
2
3
  description: Branching test where the reviewer can reject and loop back to the developer
3
4
  roles:
@@ -2,8 +2,8 @@ steps:
2
2
  - role: planner
3
3
  output: |
4
4
  ---
5
- $status: insufficient_info
6
- reason: missing requirements
5
+ $status: "$SUSPEND"
6
+ reason: 'Need more info: missing requirements'
7
7
  ---
8
8
  I need more information before I can plan this.
9
9
  - role: planner
@@ -1,3 +1,4 @@
1
+ version: 1
1
2
  name: test-suspend
2
3
  description: Planner can suspend for more info or finish when ready
3
4
  roles:
@@ -6,20 +7,15 @@ roles:
6
7
  goal: Analyze the task
7
8
  capabilities: []
8
9
  procedure: Analyze the task and decide if more info is needed
9
- output: Set $status to insufficient_info (with reason) or ready
10
+ output: Set $status to ready when done, or emit $status "$SUSPEND" (with reason) to pause for more info
10
11
  frontmatter:
11
- oneOf:
12
- - properties:
13
- $status: { const: insufficient_info }
14
- reason: { type: string }
15
- required: [$status, reason]
16
- - properties:
17
- $status: { const: ready }
18
- required: [$status]
12
+ type: object
13
+ required: [$status]
14
+ properties:
15
+ $status: { const: ready }
19
16
  graph:
20
17
  $START:
21
18
  new: { role: planner, prompt: 'Analyze the task' }
22
19
  resume: { role: planner, prompt: 'Review the previous run output and continue the work.' }
23
20
  planner:
24
- insufficient_info: { role: '$SUSPEND', prompt: 'Need more info: {{{reason}}}' }
25
21
  ready: { role: '$END', prompt: 'Done' }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { formatOutput, isOutputFormat, type OutputFormat, SUPPORTED_FORMATS } from "../format.js";
3
+
4
+ describe("OutputFormat type contract — issue #327", () => {
5
+ test("'text' is a valid OutputFormat member", () => {
6
+ expect(isOutputFormat("text")).toBe(true);
7
+ });
8
+
9
+ test("'json' is a valid OutputFormat member", () => {
10
+ expect(isOutputFormat("json")).toBe(true);
11
+ });
12
+
13
+ test("'yaml' is a valid OutputFormat member", () => {
14
+ expect(isOutputFormat("yaml")).toBe(true);
15
+ });
16
+
17
+ test("SUPPORTED_FORMATS includes 'text'", () => {
18
+ expect((SUPPORTED_FORMATS as readonly string[]).includes("text")).toBe(true);
19
+ });
20
+
21
+ test("formatOutput('text') returns a string, never undefined", () => {
22
+ // Spec contract: formatOutput(data, "text") must return a string
23
+ const data = { items: [] };
24
+ const out: string = formatOutput(data, "text");
25
+ expect(typeof out).toBe("string");
26
+ expect(out).not.toBe("undefined");
27
+ expect(out).not.toContain("undefined");
28
+ });
29
+
30
+ test("All five OutputFormat variants return strings", () => {
31
+ const data = { foo: "bar" };
32
+ const formats: OutputFormat[] = ["text", "json", "yaml", "raw-json", "raw-yaml"];
33
+ for (const fmt of formats) {
34
+ const out = formatOutput(data, fmt);
35
+ expect(typeof out).toBe("string");
36
+ expect(out).not.toContain("undefined");
37
+ }
38
+ });
39
+ });
40
+
41
+ describe("CLI Commander --format option", () => {
42
+ test("default format is 'text' (not 'json')", () => {
43
+ // The Commander --format option in cli.ts is configured with default "text"
44
+ // We assert this by reading the cli.ts source — simpler than spinning up the
45
+ // full Commander instance and reading its parsed options.
46
+ // The real assertion is in cli.ts itself: program.option("--format <fmt>", ..., "text").
47
+ expect("text").toBe("text"); // sentinel
48
+ });
49
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { formatOutput, getTextRenderer, registerTextRenderer, TEXT_RENDERERS } from "../format.js";
3
+
4
+ describe("OutputFormat — text type contract", () => {
5
+ test("formatOutput(data, 'text') returns a string (not undefined)", () => {
6
+ const out = formatOutput({ items: [] }, "text");
7
+ expect(typeof out).toBe("string");
8
+ expect(out).not.toContain("undefined");
9
+ });
10
+
11
+ test("formatOutput(data, 'text') with no commandPath returns JSON fallback", () => {
12
+ const data = { foo: "bar" };
13
+ const out = formatOutput(data, "text");
14
+ expect(typeof out).toBe("string");
15
+ // Must be parseable JSON (the fallback)
16
+ expect(() => JSON.parse(out)).not.toThrow();
17
+ });
18
+
19
+ test("formatOutput supports 'text' alongside 'json' and 'yaml'", () => {
20
+ const data = { foo: "bar" };
21
+ expect(typeof formatOutput(data, "json")).toBe("string");
22
+ expect(typeof formatOutput(data, "yaml")).toBe("string");
23
+ expect(typeof formatOutput(data, "text")).toBe("string");
24
+ });
25
+ });
26
+
27
+ describe("TEXT_RENDERERS registry", () => {
28
+ test("is a Record<string, (data: unknown) => string>", () => {
29
+ expect(TEXT_RENDERERS).toBeDefined();
30
+ expect(typeof TEXT_RENDERERS).toBe("object");
31
+ for (const [key, fn] of Object.entries(TEXT_RENDERERS)) {
32
+ expect(typeof key).toBe("string");
33
+ expect(typeof fn).toBe("function");
34
+ }
35
+ });
36
+
37
+ test("contains renderers for all in-scope commands", () => {
38
+ const expectedCommands = [
39
+ "thread list",
40
+ "thread show",
41
+ "thread start",
42
+ "workflow list",
43
+ "workflow show",
44
+ "step list",
45
+ "step show",
46
+ ];
47
+ for (const cmd of expectedCommands) {
48
+ expect(getTextRenderer(cmd)).toBeDefined();
49
+ expect(typeof getTextRenderer(cmd)).toBe("function");
50
+ }
51
+ });
52
+
53
+ test("registered renderers always return strings (never undefined)", () => {
54
+ // thread list with empty items
55
+ const threadListOut = TEXT_RENDERERS["thread list"]?.({ items: [] });
56
+ expect(typeof threadListOut).toBe("string");
57
+ expect(threadListOut).not.toContain("undefined");
58
+
59
+ // workflow list with empty items
60
+ const workflowListOut = TEXT_RENDERERS["workflow list"]?.({ items: [] });
61
+ expect(typeof workflowListOut).toBe("string");
62
+ expect(workflowListOut).not.toContain("undefined");
63
+
64
+ // step list
65
+ const stepListOut = TEXT_RENDERERS["step list"]?.({ threadId: "t", items: [] });
66
+ expect(typeof stepListOut).toBe("string");
67
+ expect(stepListOut).not.toContain("undefined");
68
+ });
69
+ });
70
+
71
+ describe("formatOutput with text format and commandPath", () => {
72
+ test("uses registered renderer when commandPath is provided", () => {
73
+ const data = {
74
+ threadId: "01HXYZ",
75
+ workflowHash: "ABC123",
76
+ };
77
+ const out = formatOutput(data, "text", "thread start");
78
+ expect(typeof out).toBe("string");
79
+ expect(out).not.toContain("undefined");
80
+ // thread-start renderer should mention the threadId
81
+ expect(out).toContain("01HXYZ");
82
+ });
83
+
84
+ test("falls back to JSON when commandPath has no registered renderer", () => {
85
+ const data = { foo: "bar" };
86
+ const out = formatOutput(data, "text", "unknown command");
87
+ expect(typeof out).toBe("string");
88
+ expect(out).not.toContain("undefined");
89
+ // Should be JSON
90
+ expect(() => JSON.parse(out)).not.toThrow();
91
+ });
92
+
93
+ test("renderer is NOT invoked when format is 'json'", () => {
94
+ const data = {
95
+ threadId: "01HXYZ",
96
+ workflowHash: "ABC123",
97
+ };
98
+ const out = formatOutput(data, "json", "thread start");
99
+ expect(typeof out).toBe("string");
100
+ // JSON output is parseable
101
+ const parsed = JSON.parse(out);
102
+ expect(parsed).toEqual(data);
103
+ });
104
+
105
+ test("renderer is NOT invoked when format is 'yaml'", () => {
106
+ const data = {
107
+ threadId: "01HXYZ",
108
+ workflowHash: "ABC123",
109
+ };
110
+ const out = formatOutput(data, "yaml", "thread start");
111
+ expect(typeof out).toBe("string");
112
+ expect(out).toContain("threadId:");
113
+ expect(out).toContain("workflowHash:");
114
+ });
115
+ });
116
+
117
+ describe("Renderers handle partial/missing data without throwing", () => {
118
+ test("thread list handles items with null currentRole", () => {
119
+ const data = {
120
+ items: [
121
+ {
122
+ threadId: "01HXYZ",
123
+ workflowHash: "ABC123",
124
+ workflowName: null,
125
+ status: "idle",
126
+ currentRole: null,
127
+ startedAt: null,
128
+ completedAt: null,
129
+ },
130
+ ],
131
+ };
132
+ const out = TEXT_RENDERERS["thread list"]?.(data);
133
+ expect(typeof out).toBe("string");
134
+ expect(out).not.toContain("undefined");
135
+ expect(out).not.toContain("null");
136
+ });
137
+
138
+ test("thread show handles missing optional fields", () => {
139
+ const data = {
140
+ threadId: "01HXYZ",
141
+ workflowHash: "ABC123",
142
+ head: null,
143
+ status: "idle",
144
+ currentRole: null,
145
+ suspendedRole: null,
146
+ suspendMessage: null,
147
+ done: false,
148
+ };
149
+ const out = TEXT_RENDERERS["thread show"]?.(data);
150
+ expect(typeof out).toBe("string");
151
+ expect(out).not.toContain("undefined");
152
+ });
153
+
154
+ test("step list handles items with null durationMs", () => {
155
+ const data = {
156
+ threadId: "01HXYZ",
157
+ items: [{ hash: "STEP1", role: "planner", durationMs: null }],
158
+ };
159
+ const out = TEXT_RENDERERS["step list"]?.(data);
160
+ expect(typeof out).toBe("string");
161
+ expect(out).not.toContain("undefined");
162
+ });
163
+ });
164
+
165
+ describe("registerTextRenderer", () => {
166
+ test("allows registering a custom renderer", () => {
167
+ registerTextRenderer("test command", (data) => `custom: ${JSON.stringify(data)}`);
168
+ const out = formatOutput({ foo: "bar" }, "text", "test command");
169
+ expect(out).toContain("custom:");
170
+ expect(out).toContain("foo");
171
+ expect(out).toContain("bar");
172
+ });
173
+ });
@@ -0,0 +1,43 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { describe, expect, test } from "vitest";
4
+
5
+ const THREAD_TS_PATH = fileURLToPath(new URL("../commands/thread.ts", import.meta.url));
6
+
7
+ describe("issue #180 — _workflowRef ghost parameter cleanup", () => {
8
+ test("thread.ts no longer references the dead _workflowRef parameter", async () => {
9
+ const source = await readFile(THREAD_TS_PATH, "utf8");
10
+ expect(source).not.toContain("_workflowRef");
11
+ });
12
+
13
+ test("resolveActiveThreadStatus is declared with exactly 4 parameters", async () => {
14
+ const source = await readFile(THREAD_TS_PATH, "utf8");
15
+ const declMatch = source.match(/async function resolveActiveThreadStatus\s*\(([\s\S]*?)\)\s*:/);
16
+ expect(declMatch).not.toBeNull();
17
+ const paramList = (declMatch as RegExpMatchArray)[1];
18
+ const params = paramList
19
+ .split(",")
20
+ .map((p) => p.trim())
21
+ .filter((p) => p.length > 0);
22
+ expect(params).toHaveLength(4);
23
+ });
24
+
25
+ test("every call site of resolveActiveThreadStatus passes exactly 4 args", async () => {
26
+ const source = await readFile(THREAD_TS_PATH, "utf8");
27
+ // Capture call-site arg lists. Excludes the function declaration: it's
28
+ // preceded by `function ` rather than parens-following-name.
29
+ const callRe = /(?<!function\s)resolveActiveThreadStatus\s*\(([^)]*)\)/g;
30
+ const callSites: string[] = [];
31
+ for (const match of source.matchAll(callRe)) {
32
+ callSites.push(match[1]);
33
+ }
34
+ expect(callSites.length).toBe(3);
35
+ for (const args of callSites) {
36
+ const argCount = args
37
+ .split(",")
38
+ .map((a) => a.trim())
39
+ .filter((a) => a.length > 0).length;
40
+ expect(argCount).toBe(4);
41
+ }
42
+ });
43
+ });