sofia-cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.github/agents/copilot-instructions.md +39 -0
  2. package/.github/agents/speckit.analyze.agent.md +184 -0
  3. package/.github/agents/speckit.checklist.agent.md +294 -0
  4. package/.github/agents/speckit.clarify.agent.md +181 -0
  5. package/.github/agents/speckit.constitution.agent.md +84 -0
  6. package/.github/agents/speckit.implement.agent.md +135 -0
  7. package/.github/agents/speckit.plan.agent.md +90 -0
  8. package/.github/agents/speckit.specify.agent.md +258 -0
  9. package/.github/agents/speckit.tasks.agent.md +137 -0
  10. package/.github/agents/speckit.taskstoissues.agent.md +30 -0
  11. package/.github/copilot-instructions.md +257 -0
  12. package/.github/prompts/speckit.analyze.prompt.md +3 -0
  13. package/.github/prompts/speckit.checklist.prompt.md +3 -0
  14. package/.github/prompts/speckit.clarify.prompt.md +3 -0
  15. package/.github/prompts/speckit.constitution.prompt.md +3 -0
  16. package/.github/prompts/speckit.implement.prompt.md +3 -0
  17. package/.github/prompts/speckit.plan.prompt.md +3 -0
  18. package/.github/prompts/speckit.specify.prompt.md +3 -0
  19. package/.github/prompts/speckit.tasks.prompt.md +3 -0
  20. package/.github/prompts/speckit.taskstoissues.prompt.md +3 -0
  21. package/.github/workflows/ci.yml +38 -0
  22. package/.prettierrc +6 -0
  23. package/.specify/memory/constitution.md +181 -0
  24. package/.specify/scripts/bash/check-prerequisites.sh +166 -0
  25. package/.specify/scripts/bash/common.sh +156 -0
  26. package/.specify/scripts/bash/create-new-feature.sh +297 -0
  27. package/.specify/scripts/bash/setup-plan.sh +61 -0
  28. package/.specify/scripts/bash/update-agent-context.sh +810 -0
  29. package/.specify/templates/agent-file-template.md +28 -0
  30. package/.specify/templates/checklist-template.md +40 -0
  31. package/.specify/templates/constitution-template.md +50 -0
  32. package/.specify/templates/plan-template.md +113 -0
  33. package/.specify/templates/spec-template.md +115 -0
  34. package/.specify/templates/tasks-template.md +251 -0
  35. package/.vscode/mcp.json +42 -0
  36. package/.vscode/settings.json +19 -0
  37. package/CODE_OF_CONDUCT.md +128 -0
  38. package/LICENSE +21 -0
  39. package/README.md +213 -0
  40. package/dist/src/cli/developCommand.js +240 -0
  41. package/dist/src/cli/directCommands.js +143 -0
  42. package/dist/src/cli/envLoader.js +16 -0
  43. package/dist/src/cli/exportCommand.js +53 -0
  44. package/dist/src/cli/index.js +203 -0
  45. package/dist/src/cli/ioContext.js +109 -0
  46. package/dist/src/cli/preflight.js +57 -0
  47. package/dist/src/cli/statusCommand.js +110 -0
  48. package/dist/src/cli/workshopCommand.js +400 -0
  49. package/dist/src/develop/checkpointState.js +86 -0
  50. package/dist/src/develop/codeGenerator.js +319 -0
  51. package/dist/src/develop/dynamicScaffolder.js +226 -0
  52. package/dist/src/develop/githubMcpAdapter.js +122 -0
  53. package/dist/src/develop/index.js +15 -0
  54. package/dist/src/develop/mcpContextEnricher.js +195 -0
  55. package/dist/src/develop/pocScaffolder.js +542 -0
  56. package/dist/src/develop/ralphLoop.js +659 -0
  57. package/dist/src/develop/templateRegistry.js +364 -0
  58. package/dist/src/develop/testRunner.js +202 -0
  59. package/dist/src/logging/logger.js +58 -0
  60. package/dist/src/loop/conversationLoop.js +227 -0
  61. package/dist/src/loop/phaseSummarizer.js +87 -0
  62. package/dist/src/mcp/mcpManager.js +267 -0
  63. package/dist/src/mcp/mcpTransport.js +391 -0
  64. package/dist/src/mcp/retryPolicy.js +47 -0
  65. package/dist/src/mcp/webSearch.js +254 -0
  66. package/dist/src/phases/contextSummarizer.js +101 -0
  67. package/dist/src/phases/discoveryEnricher.js +156 -0
  68. package/dist/src/phases/phaseExtractors.js +222 -0
  69. package/dist/src/phases/phaseHandlers.js +328 -0
  70. package/dist/src/prompts/design.md +51 -0
  71. package/dist/src/prompts/develop-boundary.md +51 -0
  72. package/dist/src/prompts/develop.md +111 -0
  73. package/dist/src/prompts/discover.md +58 -0
  74. package/dist/src/prompts/ideate.md +56 -0
  75. package/dist/src/prompts/plan.md +51 -0
  76. package/dist/src/prompts/promptLoader.js +167 -0
  77. package/dist/src/prompts/promptLoader.ts +198 -0
  78. package/dist/src/prompts/select.md +47 -0
  79. package/dist/src/prompts/summarize/README.md +8 -0
  80. package/dist/src/prompts/summarize/design-summary.md +37 -0
  81. package/dist/src/prompts/summarize/develop-summary.md +25 -0
  82. package/dist/src/prompts/summarize/ideate-summary.md +27 -0
  83. package/dist/src/prompts/summarize/plan-summary.md +27 -0
  84. package/dist/src/prompts/summarize/select-summary.md +21 -0
  85. package/dist/src/prompts/system.md +28 -0
  86. package/dist/src/sessions/exportPaths.js +22 -0
  87. package/dist/src/sessions/exportWriter.js +406 -0
  88. package/dist/src/sessions/sessionManager.js +81 -0
  89. package/dist/src/sessions/sessionStore.js +65 -0
  90. package/dist/src/shared/activitySpinner.js +91 -0
  91. package/dist/src/shared/copilotClient.js +129 -0
  92. package/dist/src/shared/data/cards.json +1249 -0
  93. package/dist/src/shared/data/cardsLoader.js +51 -0
  94. package/dist/src/shared/errorClassifier.js +120 -0
  95. package/dist/src/shared/events.js +28 -0
  96. package/dist/src/shared/markdownRenderer.js +34 -0
  97. package/dist/src/shared/schemas/session.js +265 -0
  98. package/dist/src/shared/tableRenderer.js +20 -0
  99. package/dist/src/vendor/chalk.js +2 -0
  100. package/dist/src/vendor/cli-table3.js +3 -0
  101. package/dist/src/vendor/commander.js +2 -0
  102. package/dist/src/vendor/marked-terminal.js +3 -0
  103. package/dist/src/vendor/marked.js +2 -0
  104. package/dist/src/vendor/ora.js +2 -0
  105. package/dist/src/vendor/pino.js +2 -0
  106. package/dist/src/vendor/zod.js +2 -0
  107. package/dist/tests/e2e/developE2e.spec.js +126 -0
  108. package/dist/tests/e2e/developFailureE2e.spec.js +247 -0
  109. package/dist/tests/e2e/developPty.spec.js +75 -0
  110. package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +84 -0
  111. package/dist/tests/e2e/harness.spec.js +83 -0
  112. package/dist/tests/e2e/mcpLive.spec.js +120 -0
  113. package/dist/tests/e2e/newSession.e2e.spec.js +177 -0
  114. package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +62 -0
  115. package/dist/tests/e2e/workiqEnrichment.spec.js +56 -0
  116. package/dist/tests/e2e/zavaSimulation.spec.js +452 -0
  117. package/dist/tests/fixtures/test-fixture-project/src/add.js +3 -0
  118. package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +6 -0
  119. package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +8 -0
  120. package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +10 -0
  121. package/dist/tests/fixtures/test-fixture-project/vitest.config.js +6 -0
  122. package/dist/tests/integration/autoStartConversation.spec.js +138 -0
  123. package/dist/tests/integration/defaultCommand.spec.js +147 -0
  124. package/dist/tests/integration/directCommandNonTty.spec.js +224 -0
  125. package/dist/tests/integration/directCommandTty.spec.js +151 -0
  126. package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +175 -0
  127. package/dist/tests/integration/exportArtifacts.spec.js +202 -0
  128. package/dist/tests/integration/exportFallbackFlow.spec.js +99 -0
  129. package/dist/tests/integration/mcpDegradationFlow.spec.js +190 -0
  130. package/dist/tests/integration/mcpTransportFlow.spec.js +139 -0
  131. package/dist/tests/integration/newSessionFlow.spec.js +343 -0
  132. package/dist/tests/integration/pocGithubMcp.spec.js +186 -0
  133. package/dist/tests/integration/pocLocalFallback.spec.js +171 -0
  134. package/dist/tests/integration/pocScaffold.spec.js +163 -0
  135. package/dist/tests/integration/ralphLoopFlow.spec.js +359 -0
  136. package/dist/tests/integration/ralphLoopPartial.spec.js +368 -0
  137. package/dist/tests/integration/resumeAndBacktrack.spec.js +247 -0
  138. package/dist/tests/integration/spinnerLifecycle.spec.js +220 -0
  139. package/dist/tests/integration/summarizationFlow.spec.js +115 -0
  140. package/dist/tests/integration/testRunnerReal.spec.js +52 -0
  141. package/dist/tests/integration/webSearchAgent.spec.js +128 -0
  142. package/dist/tests/live/copilotSdkLive.spec.js +107 -0
  143. package/dist/tests/live/zavaFullWorkshop.spec.js +392 -0
  144. package/dist/tests/setup/loadEnv.js +3 -0
  145. package/dist/tests/unit/cli/developCommand.spec.js +567 -0
  146. package/dist/tests/unit/cli/directCommands.spec.js +279 -0
  147. package/dist/tests/unit/cli/envLoader.spec.js +58 -0
  148. package/dist/tests/unit/cli/ioContext.spec.js +119 -0
  149. package/dist/tests/unit/cli/preflight.spec.js +108 -0
  150. package/dist/tests/unit/cli/statusCommand.spec.js +111 -0
  151. package/dist/tests/unit/cli/workshopClientFallback.spec.js +80 -0
  152. package/dist/tests/unit/cli/workshopCommand.spec.js +329 -0
  153. package/dist/tests/unit/config/vitestEnvSetup.spec.js +13 -0
  154. package/dist/tests/unit/develop/checkpointState.spec.js +315 -0
  155. package/dist/tests/unit/develop/codeGenerator.spec.js +355 -0
  156. package/dist/tests/unit/develop/githubMcpAdapter.spec.js +231 -0
  157. package/dist/tests/unit/develop/mcpContextEnricher.spec.js +433 -0
  158. package/dist/tests/unit/develop/outputValidator.spec.js +119 -0
  159. package/dist/tests/unit/develop/pocScaffolder.spec.js +353 -0
  160. package/dist/tests/unit/develop/ralphLoop.spec.js +1248 -0
  161. package/dist/tests/unit/develop/templateRegistry.spec.js +85 -0
  162. package/dist/tests/unit/develop/testRunner.spec.js +249 -0
  163. package/dist/tests/unit/infraBicep.spec.js +92 -0
  164. package/dist/tests/unit/infraDeploy.spec.js +82 -0
  165. package/dist/tests/unit/infraTeardown.spec.js +63 -0
  166. package/dist/tests/unit/logging/logger.spec.js +43 -0
  167. package/dist/tests/unit/loop/conversationLoop.spec.js +592 -0
  168. package/dist/tests/unit/loop/phaseSummarizer.spec.js +141 -0
  169. package/dist/tests/unit/loop/streamingMarkdown.spec.js +147 -0
  170. package/dist/tests/unit/mcp/mcpManager.spec.js +279 -0
  171. package/dist/tests/unit/mcp/mcpTransport.spec.js +529 -0
  172. package/dist/tests/unit/mcp/retryPolicy.spec.js +218 -0
  173. package/dist/tests/unit/mcp/timeoutValidation.spec.js +46 -0
  174. package/dist/tests/unit/mcp/webSearch.spec.js +567 -0
  175. package/dist/tests/unit/phases/contextSummarizer.spec.js +140 -0
  176. package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +93 -0
  177. package/dist/tests/unit/phases/discoveryEnricher.spec.js +411 -0
  178. package/dist/tests/unit/phases/phaseExtractors.spec.js +352 -0
  179. package/dist/tests/unit/phases/phaseHandlers.spec.js +425 -0
  180. package/dist/tests/unit/prompts/promptLoader.spec.js +118 -0
  181. package/dist/tests/unit/schemas/pocSchemas.spec.js +412 -0
  182. package/dist/tests/unit/schemas/session.spec.js +257 -0
  183. package/dist/tests/unit/sessions/exportPaths.spec.js +31 -0
  184. package/dist/tests/unit/sessions/exportWriter.spec.js +655 -0
  185. package/dist/tests/unit/sessions/sessionManager.spec.js +151 -0
  186. package/dist/tests/unit/sessions/sessionStore.spec.js +116 -0
  187. package/dist/tests/unit/shared/activitySpinner.spec.js +175 -0
  188. package/dist/tests/unit/shared/cardsLoader.spec.js +76 -0
  189. package/dist/tests/unit/shared/copilotClient.spec.js +155 -0
  190. package/dist/tests/unit/shared/errorClassifier.spec.js +131 -0
  191. package/dist/tests/unit/shared/events.spec.js +55 -0
  192. package/dist/tests/unit/shared/markdownRenderer.spec.js +35 -0
  193. package/dist/tests/unit/shared/markdownRendererChunks.spec.js +70 -0
  194. package/dist/tests/unit/shared/tableRenderer.spec.js +34 -0
  195. package/dist/vitest.config.js +14 -0
  196. package/dist/vitest.live.config.js +18 -0
  197. package/docs/README.md +35 -0
  198. package/docs/architecture.md +169 -0
  199. package/docs/cli-usage.md +207 -0
  200. package/docs/environment.md +66 -0
  201. package/docs/export-format.md +146 -0
  202. package/docs/session-model.md +113 -0
  203. package/eslint.config.js +35 -0
  204. package/infra/deploy.sh +193 -0
  205. package/infra/gather-env.sh +211 -0
  206. package/infra/main.bicep +90 -0
  207. package/infra/main.bicepparam +18 -0
  208. package/infra/resources.bicep +134 -0
  209. package/infra/teardown.sh +114 -0
  210. package/package.json +63 -0
  211. package/specs/001-cli-workshop-rebuild/checklists/requirements.md +35 -0
  212. package/specs/001-cli-workshop-rebuild/contracts/cli.md +59 -0
  213. package/specs/001-cli-workshop-rebuild/contracts/export-summary-json.md +23 -0
  214. package/specs/001-cli-workshop-rebuild/contracts/session-json.md +30 -0
  215. package/specs/001-cli-workshop-rebuild/data-model.md +210 -0
  216. package/specs/001-cli-workshop-rebuild/plan.md +361 -0
  217. package/specs/001-cli-workshop-rebuild/quickstart.md +83 -0
  218. package/specs/001-cli-workshop-rebuild/research.md +116 -0
  219. package/specs/001-cli-workshop-rebuild/spec.md +240 -0
  220. package/specs/001-cli-workshop-rebuild/tasks.md +476 -0
  221. package/specs/002-poc-generation/contracts/poc-output.md +172 -0
  222. package/specs/002-poc-generation/contracts/ralph-loop.md +113 -0
  223. package/specs/002-poc-generation/data-model.md +172 -0
  224. package/specs/002-poc-generation/plan.md +109 -0
  225. package/specs/002-poc-generation/quickstart.md +97 -0
  226. package/specs/002-poc-generation/research.md +786 -0
  227. package/specs/002-poc-generation/spec.md +81 -0
  228. package/specs/002-poc-generation/tasks-fix.md +198 -0
  229. package/specs/002-poc-generation/tasks.md +252 -0
  230. package/specs/003-mcp-transport-integration/checklists/requirements.md +37 -0
  231. package/specs/003-mcp-transport-integration/contracts/context-enricher.md +220 -0
  232. package/specs/003-mcp-transport-integration/contracts/discovery-enricher.md +267 -0
  233. package/specs/003-mcp-transport-integration/contracts/github-adapter.md +149 -0
  234. package/specs/003-mcp-transport-integration/contracts/mcp-transport.md +288 -0
  235. package/specs/003-mcp-transport-integration/data-model.md +326 -0
  236. package/specs/003-mcp-transport-integration/plan.md +114 -0
  237. package/specs/003-mcp-transport-integration/quickstart.md +311 -0
  238. package/specs/003-mcp-transport-integration/research.md +395 -0
  239. package/specs/003-mcp-transport-integration/spec.md +234 -0
  240. package/specs/003-mcp-transport-integration/tasks.md +324 -0
  241. package/specs/003-next-spec-gaps.md +150 -0
  242. package/specs/004-dev-resume-hardening/checklists/requirements.md +37 -0
  243. package/specs/004-dev-resume-hardening/contracts/cli.md +160 -0
  244. package/specs/004-dev-resume-hardening/data-model.md +321 -0
  245. package/specs/004-dev-resume-hardening/plan.md +107 -0
  246. package/specs/004-dev-resume-hardening/quickstart.md +115 -0
  247. package/specs/004-dev-resume-hardening/research.md +142 -0
  248. package/specs/004-dev-resume-hardening/spec.md +221 -0
  249. package/specs/004-dev-resume-hardening/tasks.md +333 -0
  250. package/specs/005-ai-search-deploy/checklists/requirements.md +39 -0
  251. package/specs/005-ai-search-deploy/contracts/web-search-tool.md +241 -0
  252. package/specs/005-ai-search-deploy/data-model.md +130 -0
  253. package/specs/005-ai-search-deploy/plan.md +93 -0
  254. package/specs/005-ai-search-deploy/quickstart.md +96 -0
  255. package/specs/005-ai-search-deploy/research.md +187 -0
  256. package/specs/005-ai-search-deploy/spec.md +143 -0
  257. package/specs/005-ai-search-deploy/tasks.md +284 -0
  258. package/specs/006-workshop-extraction-fixes/checklists/requirements.md +61 -0
  259. package/specs/006-workshop-extraction-fixes/contracts/summarization-and-export.md +131 -0
  260. package/specs/006-workshop-extraction-fixes/data-model.md +149 -0
  261. package/specs/006-workshop-extraction-fixes/plan.md +123 -0
  262. package/specs/006-workshop-extraction-fixes/quickstart.md +101 -0
  263. package/specs/006-workshop-extraction-fixes/research.md +143 -0
  264. package/specs/006-workshop-extraction-fixes/spec.md +210 -0
  265. package/specs/006-workshop-extraction-fixes/tasks.md +316 -0
  266. package/src/cli/developCommand.ts +308 -0
  267. package/src/cli/directCommands.ts +195 -0
  268. package/src/cli/envLoader.ts +17 -0
  269. package/src/cli/exportCommand.ts +65 -0
  270. package/src/cli/index.ts +249 -0
  271. package/src/cli/ioContext.ts +139 -0
  272. package/src/cli/preflight.ts +86 -0
  273. package/src/cli/statusCommand.ts +118 -0
  274. package/src/cli/workshopCommand.ts +496 -0
  275. package/src/develop/checkpointState.ts +121 -0
  276. package/src/develop/codeGenerator.ts +402 -0
  277. package/src/develop/dynamicScaffolder.ts +284 -0
  278. package/src/develop/githubMcpAdapter.ts +199 -0
  279. package/src/develop/index.ts +34 -0
  280. package/src/develop/mcpContextEnricher.ts +279 -0
  281. package/src/develop/pocScaffolder.ts +646 -0
  282. package/src/develop/ralphLoop.ts +1044 -0
  283. package/src/develop/templateRegistry.ts +427 -0
  284. package/src/develop/testRunner.ts +276 -0
  285. package/src/logging/logger.ts +73 -0
  286. package/src/loop/conversationLoop.ts +355 -0
  287. package/src/loop/phaseSummarizer.ts +114 -0
  288. package/src/mcp/mcpManager.ts +365 -0
  289. package/src/mcp/mcpTransport.ts +562 -0
  290. package/src/mcp/retryPolicy.ts +87 -0
  291. package/src/mcp/webSearch.ts +388 -0
  292. package/src/originalPrompts/design_thinking.md +178 -0
  293. package/src/originalPrompts/design_thinking_persona.md +76 -0
  294. package/src/originalPrompts/document_generator_example.md +77 -0
  295. package/src/originalPrompts/document_generator_persona.md +47 -0
  296. package/src/originalPrompts/facilitator_persona.md +125 -0
  297. package/src/originalPrompts/guardrails.md +47 -0
  298. package/src/phases/contextSummarizer.ts +154 -0
  299. package/src/phases/discoveryEnricher.ts +223 -0
  300. package/src/phases/phaseExtractors.ts +247 -0
  301. package/src/phases/phaseHandlers.ts +450 -0
  302. package/src/prompts/design.md +51 -0
  303. package/src/prompts/develop-boundary.md +51 -0
  304. package/src/prompts/develop.md +111 -0
  305. package/src/prompts/discover.md +58 -0
  306. package/src/prompts/ideate.md +56 -0
  307. package/src/prompts/plan.md +51 -0
  308. package/src/prompts/promptLoader.ts +198 -0
  309. package/src/prompts/select.md +47 -0
  310. package/src/prompts/summarize/README.md +8 -0
  311. package/src/prompts/summarize/design-summary.md +37 -0
  312. package/src/prompts/summarize/develop-summary.md +25 -0
  313. package/src/prompts/summarize/ideate-summary.md +27 -0
  314. package/src/prompts/summarize/plan-summary.md +27 -0
  315. package/src/prompts/summarize/select-summary.md +21 -0
  316. package/src/prompts/system.md +28 -0
  317. package/src/sessions/exportPaths.ts +28 -0
  318. package/src/sessions/exportWriter.ts +490 -0
  319. package/src/sessions/sessionManager.ts +119 -0
  320. package/src/sessions/sessionStore.ts +69 -0
  321. package/src/shared/activitySpinner.ts +108 -0
  322. package/src/shared/copilotClient.ts +291 -0
  323. package/src/shared/data/cards.json +1249 -0
  324. package/src/shared/data/cardsLoader.ts +70 -0
  325. package/src/shared/errorClassifier.ts +160 -0
  326. package/src/shared/events.ts +103 -0
  327. package/src/shared/markdownRenderer.ts +44 -0
  328. package/src/shared/schemas/session.ts +346 -0
  329. package/src/shared/tableRenderer.ts +28 -0
  330. package/src/types/marked-terminal.d.ts +5 -0
  331. package/src/vendor/chalk.ts +2 -0
  332. package/src/vendor/cli-table3.ts +3 -0
  333. package/src/vendor/commander.ts +2 -0
  334. package/src/vendor/marked-terminal.ts +3 -0
  335. package/src/vendor/marked.ts +2 -0
  336. package/src/vendor/ora.ts +2 -0
  337. package/src/vendor/pino.ts +3 -0
  338. package/src/vendor/zod.ts +3 -0
  339. package/tests/e2e/developE2e.spec.ts +152 -0
  340. package/tests/e2e/developFailureE2e.spec.ts +289 -0
  341. package/tests/e2e/developPty.spec.ts +86 -0
  342. package/tests/e2e/discoveryWebSearchRelevance.spec.ts +103 -0
  343. package/tests/e2e/harness.spec.ts +104 -0
  344. package/tests/e2e/mcpLive.spec.ts +149 -0
  345. package/tests/e2e/newSession.e2e.spec.ts +245 -0
  346. package/tests/e2e/ralphLoopEnrichmentComparison.spec.ts +70 -0
  347. package/tests/e2e/workiqEnrichment.spec.ts +72 -0
  348. package/tests/e2e/zava-assessment/agent-interaction-script.md +258 -0
  349. package/tests/e2e/zava-assessment/company-profile.md +98 -0
  350. package/tests/e2e/zava-assessment/expected-results-checklist.md +454 -0
  351. package/tests/e2e/zavaSimulation.spec.ts +511 -0
  352. package/tests/fixtures/completedSession.json +141 -0
  353. package/tests/fixtures/test-fixture-project/package-lock.json +1585 -0
  354. package/tests/fixtures/test-fixture-project/package.json +12 -0
  355. package/tests/fixtures/test-fixture-project/src/add.ts +3 -0
  356. package/tests/fixtures/test-fixture-project/tests/failing.test.ts +7 -0
  357. package/tests/fixtures/test-fixture-project/tests/hanging.test.ts +9 -0
  358. package/tests/fixtures/test-fixture-project/tests/passing.test.ts +13 -0
  359. package/tests/fixtures/test-fixture-project/vitest.config.ts +7 -0
  360. package/tests/integration/autoStartConversation.spec.ts +168 -0
  361. package/tests/integration/defaultCommand.spec.ts +179 -0
  362. package/tests/integration/directCommandNonTty.spec.ts +260 -0
  363. package/tests/integration/directCommandTty.spec.ts +185 -0
  364. package/tests/integration/discoveryEnrichmentFlow.spec.ts +209 -0
  365. package/tests/integration/exportArtifacts.spec.ts +232 -0
  366. package/tests/integration/exportFallbackFlow.spec.ts +115 -0
  367. package/tests/integration/mcpDegradationFlow.spec.ts +231 -0
  368. package/tests/integration/mcpTransportFlow.spec.ts +178 -0
  369. package/tests/integration/newSessionFlow.spec.ts +406 -0
  370. package/tests/integration/pocGithubMcp.spec.ts +224 -0
  371. package/tests/integration/pocLocalFallback.spec.ts +205 -0
  372. package/tests/integration/pocScaffold.spec.ts +220 -0
  373. package/tests/integration/ralphLoopFlow.spec.ts +430 -0
  374. package/tests/integration/ralphLoopPartial.spec.ts +416 -0
  375. package/tests/integration/resumeAndBacktrack.spec.ts +278 -0
  376. package/tests/integration/spinnerLifecycle.spec.ts +270 -0
  377. package/tests/integration/summarizationFlow.spec.ts +135 -0
  378. package/tests/integration/testRunnerReal.spec.ts +63 -0
  379. package/tests/integration/webSearchAgent.spec.ts +155 -0
  380. package/tests/live/copilotSdkLive.spec.ts +149 -0
  381. package/tests/live/zavaFullWorkshop.spec.ts +515 -0
  382. package/tests/setup/loadEnv.ts +5 -0
  383. package/tests/unit/cli/developCommand.spec.ts +679 -0
  384. package/tests/unit/cli/directCommands.spec.ts +325 -0
  385. package/tests/unit/cli/envLoader.spec.ts +73 -0
  386. package/tests/unit/cli/ioContext.spec.ts +148 -0
  387. package/tests/unit/cli/preflight.spec.ts +125 -0
  388. package/tests/unit/cli/statusCommand.spec.ts +134 -0
  389. package/tests/unit/cli/workshopClientFallback.spec.ts +100 -0
  390. package/tests/unit/cli/workshopCommand.spec.ts +378 -0
  391. package/tests/unit/config/vitestEnvSetup.spec.ts +24 -0
  392. package/tests/unit/develop/checkpointState.spec.ts +378 -0
  393. package/tests/unit/develop/codeGenerator.spec.ts +447 -0
  394. package/tests/unit/develop/githubMcpAdapter.spec.ts +283 -0
  395. package/tests/unit/develop/mcpContextEnricher.spec.ts +564 -0
  396. package/tests/unit/develop/outputValidator.spec.ts +134 -0
  397. package/tests/unit/develop/pocScaffolder.spec.ts +451 -0
  398. package/tests/unit/develop/ralphLoop.spec.ts +1439 -0
  399. package/tests/unit/develop/templateRegistry.spec.ts +106 -0
  400. package/tests/unit/develop/testRunner.spec.ts +294 -0
  401. package/tests/unit/infraBicep.spec.ts +116 -0
  402. package/tests/unit/infraDeploy.spec.ts +102 -0
  403. package/tests/unit/infraTeardown.spec.ts +77 -0
  404. package/tests/unit/logging/logger.spec.ts +50 -0
  405. package/tests/unit/loop/conversationLoop.spec.ts +719 -0
  406. package/tests/unit/loop/phaseSummarizer.spec.ts +169 -0
  407. package/tests/unit/loop/streamingMarkdown.spec.ts +180 -0
  408. package/tests/unit/mcp/mcpManager.spec.ts +336 -0
  409. package/tests/unit/mcp/mcpTransport.spec.ts +689 -0
  410. package/tests/unit/mcp/retryPolicy.spec.ts +278 -0
  411. package/tests/unit/mcp/timeoutValidation.spec.ts +55 -0
  412. package/tests/unit/mcp/webSearch.spec.ts +718 -0
  413. package/tests/unit/phases/contextSummarizer.spec.ts +158 -0
  414. package/tests/unit/phases/discoveryEnricher.repeatCalls.spec.ts +125 -0
  415. package/tests/unit/phases/discoveryEnricher.spec.ts +512 -0
  416. package/tests/unit/phases/phaseExtractors.spec.ts +406 -0
  417. package/tests/unit/phases/phaseHandlers.spec.ts +483 -0
  418. package/tests/unit/prompts/promptLoader.spec.ts +144 -0
  419. package/tests/unit/schemas/pocSchemas.spec.ts +457 -0
  420. package/tests/unit/schemas/session.spec.ts +328 -0
  421. package/tests/unit/sessions/exportPaths.spec.ts +38 -0
  422. package/tests/unit/sessions/exportWriter.spec.ts +737 -0
  423. package/tests/unit/sessions/sessionManager.spec.ts +174 -0
  424. package/tests/unit/sessions/sessionStore.spec.ts +136 -0
  425. package/tests/unit/shared/activitySpinner.spec.ts +211 -0
  426. package/tests/unit/shared/cardsLoader.spec.ts +89 -0
  427. package/tests/unit/shared/copilotClient.spec.ts +185 -0
  428. package/tests/unit/shared/errorClassifier.spec.ts +152 -0
  429. package/tests/unit/shared/events.spec.ts +71 -0
  430. package/tests/unit/shared/markdownRenderer.spec.ts +42 -0
  431. package/tests/unit/shared/markdownRendererChunks.spec.ts +83 -0
  432. package/tests/unit/shared/tableRenderer.spec.ts +38 -0
  433. package/tsconfig.json +20 -0
  434. package/vitest.config.ts +15 -0
  435. package/vitest.live.config.ts +19 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Session manager tests (T043).
3
+ *
4
+ * Validates backtracking and artifact invalidation:
5
+ * - Moving to an earlier phase invalidates downstream artifacts
6
+ * - Session status is correctly updated on backtrack
7
+ * - Backtrack to same phase is a no-op
8
+ * - Cannot backtrack to a phase after the current one
9
+ */
10
+ import { describe, it, expect } from 'vitest';
11
+
12
+ import { backtrackSession } from '../../../src/sessions/sessionManager.js';
13
+ import type { WorkshopSession } from '../../../src/shared/schemas/session.js';
14
+
15
+ function createPopulatedSession(): WorkshopSession {
16
+ const now = new Date().toISOString();
17
+ return {
18
+ sessionId: 'bt-session',
19
+ schemaVersion: '1.0.0',
20
+ createdAt: now,
21
+ updatedAt: now,
22
+ phase: 'Plan',
23
+ status: 'Active',
24
+ participants: [],
25
+ artifacts: { generatedFiles: [] },
26
+ turns: [
27
+ { phase: 'Discover', sequence: 1, role: 'user', content: 'Business info', timestamp: now },
28
+ { phase: 'Discover', sequence: 2, role: 'assistant', content: 'Got it', timestamp: now },
29
+ { phase: 'Ideate', sequence: 3, role: 'user', content: 'Ideas please', timestamp: now },
30
+ { phase: 'Ideate', sequence: 4, role: 'assistant', content: 'Here are ideas', timestamp: now },
31
+ { phase: 'Design', sequence: 5, role: 'user', content: 'Evaluate', timestamp: now },
32
+ { phase: 'Design', sequence: 6, role: 'assistant', content: 'Evaluation done', timestamp: now },
33
+ { phase: 'Select', sequence: 7, role: 'user', content: 'Pick one', timestamp: now },
34
+ { phase: 'Select', sequence: 8, role: 'assistant', content: 'Selected', timestamp: now },
35
+ ],
36
+ businessContext: {
37
+ businessDescription: 'Test Corp',
38
+ challenges: ['Growth'],
39
+ },
40
+ workflow: {
41
+ activities: [{ id: 'a1', name: 'Activity' }],
42
+ edges: [],
43
+ },
44
+ ideas: [
45
+ { id: 'i1', title: 'Idea 1', description: 'First idea', workflowStepIds: ['a1'] },
46
+ ],
47
+ evaluation: {
48
+ method: 'feasibility-value-matrix',
49
+ ideas: [{ ideaId: 'i1', feasibility: 8, value: 9 }],
50
+ },
51
+ selection: {
52
+ ideaId: 'i1',
53
+ selectionRationale: 'Best fit',
54
+ confirmedByUser: true,
55
+ confirmedAt: now,
56
+ },
57
+ plan: {
58
+ milestones: [
59
+ { id: 'm1', title: 'M1', items: ['First milestone'] },
60
+ ],
61
+ },
62
+ };
63
+ }
64
+
65
+ describe('sessionManager', () => {
66
+ describe('backtrackSession', () => {
67
+ it('backtracking to Discover invalidates all data including Discover', () => {
68
+ const session = createPopulatedSession();
69
+ const result = backtrackSession(session, 'Discover');
70
+
71
+ expect(result.success).toBe(true);
72
+ expect(result.session.phase).toBe('Discover');
73
+ // Backtracking clears the target phase data too (will be re-run)
74
+ expect(result.session.businessContext).toBeUndefined();
75
+ expect(result.session.workflow).toBeUndefined();
76
+ expect(result.session.ideas).toBeUndefined();
77
+ expect(result.session.evaluation).toBeUndefined();
78
+ expect(result.session.selection).toBeUndefined();
79
+ expect(result.session.plan).toBeUndefined();
80
+ });
81
+
82
+ it('backtracking to Ideate preserves Discover data, clears Ideate+', () => {
83
+ const session = createPopulatedSession();
84
+ const result = backtrackSession(session, 'Ideate');
85
+
86
+ expect(result.success).toBe(true);
87
+ expect(result.session.phase).toBe('Ideate');
88
+ // Discover data preserved
89
+ expect(result.session.businessContext).toBeDefined();
90
+ expect(result.session.workflow).toBeDefined();
91
+ // Ideate and downstream cleared (Ideate is the target, so it's re-run)
92
+ expect(result.session.ideas).toBeUndefined();
93
+ expect(result.session.evaluation).toBeUndefined();
94
+ expect(result.session.selection).toBeUndefined();
95
+ expect(result.session.plan).toBeUndefined();
96
+ });
97
+
98
+ it('backtracking to Design clears evaluation and downstream', () => {
99
+ const session = createPopulatedSession();
100
+ const result = backtrackSession(session, 'Design');
101
+
102
+ expect(result.success).toBe(true);
103
+ expect(result.session.phase).toBe('Design');
104
+ // Preserve Discover + Ideate
105
+ expect(result.session.businessContext).toBeDefined();
106
+ expect(result.session.ideas).toBeDefined();
107
+ // Design target + downstream cleared
108
+ expect(result.session.evaluation).toBeUndefined();
109
+ expect(result.session.selection).toBeUndefined();
110
+ expect(result.session.plan).toBeUndefined();
111
+ });
112
+
113
+ it('backtracking to Select preserves evaluation', () => {
114
+ const session = createPopulatedSession();
115
+ const result = backtrackSession(session, 'Select');
116
+
117
+ expect(result.success).toBe(true);
118
+ expect(result.session.phase).toBe('Select');
119
+ expect(result.session.evaluation).toBeDefined();
120
+ expect(result.session.selection).toBeUndefined();
121
+ expect(result.session.plan).toBeUndefined();
122
+ });
123
+
124
+ it('backtracking to same phase is a no-op', () => {
125
+ const session = createPopulatedSession();
126
+ const result = backtrackSession(session, 'Plan');
127
+
128
+ expect(result.success).toBe(true);
129
+ expect(result.session.phase).toBe('Plan');
130
+ // All data preserved
131
+ expect(result.session.plan).toBeDefined();
132
+ expect(result.session.selection).toBeDefined();
133
+ expect(result.invalidatedPhases).toEqual([]);
134
+ });
135
+
136
+ it('backtracking forward fails', () => {
137
+ const session = createPopulatedSession();
138
+ session.phase = 'Ideate';
139
+ const result = backtrackSession(session, 'Plan');
140
+
141
+ expect(result.success).toBe(false);
142
+ expect(result.error).toContain('Cannot backtrack forward');
143
+ });
144
+
145
+ it('removes turns from invalidated phases', () => {
146
+ const session = createPopulatedSession();
147
+ const result = backtrackSession(session, 'Ideate');
148
+
149
+ // Only Discover turns should remain
150
+ const turns = result.session.turns!;
151
+ expect(turns.every(t => t.phase === 'Discover')).toBe(true);
152
+ expect(turns.length).toBe(2);
153
+ });
154
+
155
+ it('updates session status and timestamp on backtrack', () => {
156
+ const session = createPopulatedSession();
157
+ // Force an old timestamp
158
+ session.updatedAt = '2020-01-01T00:00:00Z';
159
+ const result = backtrackSession(session, 'Discover');
160
+
161
+ expect(result.session.status).toBe('Active');
162
+ expect(result.session.updatedAt).not.toBe('2020-01-01T00:00:00Z');
163
+ });
164
+
165
+ it('reports which phases were invalidated', () => {
166
+ const session = createPopulatedSession();
167
+ const result = backtrackSession(session, 'Ideate');
168
+
169
+ expect(result.invalidatedPhases).toEqual(
170
+ expect.arrayContaining(['Ideate', 'Design', 'Select', 'Plan']),
171
+ );
172
+ });
173
+ });
174
+ });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Unit tests for session persistence adapter.
3
+ *
4
+ * Contract: .sofia/sessions/<sessionId>.json
5
+ * - Atomic write (write-then-rename)
6
+ * - Persists after every turn
7
+ * - Preserves unknown fields
8
+ * - Never persists secrets
9
+ */
10
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
11
+ import { mkdtemp, rm, readdir, readFile } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ import { tmpdir } from 'node:os';
14
+
15
+ import { SessionStore } from '../../../src/sessions/sessionStore.js';
16
+ import type { WorkshopSession } from '../../../src/shared/schemas/session.js';
17
+
18
+ // ── Helpers ──────────────────────────────────────────────────────────────────
19
+
20
+ function minimalSession(id: string = 'sess-001'): WorkshopSession {
21
+ return {
22
+ sessionId: id,
23
+ schemaVersion: '1',
24
+ createdAt: '2026-01-01T00:00:00Z',
25
+ updatedAt: '2026-01-01T00:00:00Z',
26
+ phase: 'Discover',
27
+ status: 'Active',
28
+ participants: [],
29
+ artifacts: { generatedFiles: [] },
30
+ };
31
+ }
32
+
33
+ // ── Tests ────────────────────────────────────────────────────────────────────
34
+
35
+ describe('SessionStore', () => {
36
+ let tmpDir: string;
37
+ let store: SessionStore;
38
+
39
+ beforeEach(async () => {
40
+ tmpDir = await mkdtemp(join(tmpdir(), 'sofia-test-'));
41
+ store = new SessionStore(tmpDir);
42
+ });
43
+
44
+ afterEach(async () => {
45
+ await rm(tmpDir, { recursive: true, force: true });
46
+ });
47
+
48
+ it('saves and loads a session', async () => {
49
+ const session = minimalSession();
50
+ await store.save(session);
51
+ const loaded = await store.load('sess-001');
52
+ expect(loaded.sessionId).toBe('sess-001');
53
+ expect(loaded.phase).toBe('Discover');
54
+ });
55
+
56
+ it('creates the sessions directory on first save', async () => {
57
+ const nestedDir = join(tmpDir, 'nested', 'sessions');
58
+ const nestedStore = new SessionStore(nestedDir);
59
+ await nestedStore.save(minimalSession());
60
+ const files = await readdir(nestedDir);
61
+ expect(files).toContain('sess-001.json');
62
+ });
63
+
64
+ it('overwrites existing session file on re-save', async () => {
65
+ const session = minimalSession();
66
+ await store.save(session);
67
+ const updated = { ...session, updatedAt: '2026-06-01T00:00:00Z', phase: 'Ideate' as const };
68
+ await store.save(updated);
69
+ const loaded = await store.load('sess-001');
70
+ expect(loaded.phase).toBe('Ideate');
71
+ expect(loaded.updatedAt).toBe('2026-06-01T00:00:00Z');
72
+ });
73
+
74
+ it('preserves unknown fields (forward compatibility)', async () => {
75
+ const session = minimalSession() as WorkshopSession & { futureField: string };
76
+ session.futureField = 'hello-future';
77
+ await store.save(session);
78
+ const loaded = await store.load('sess-001');
79
+ expect((loaded as Record<string, unknown>).futureField).toBe('hello-future');
80
+ });
81
+
82
+ it('throws when loading a non-existent session', async () => {
83
+ await expect(store.load('nonexistent')).rejects.toThrow();
84
+ });
85
+
86
+ it('lists sessions', async () => {
87
+ await store.save(minimalSession('a'));
88
+ await store.save(minimalSession('b'));
89
+ const ids = await store.list();
90
+ expect(ids.sort()).toEqual(['a', 'b']);
91
+ });
92
+
93
+ it('returns empty list when no sessions exist', async () => {
94
+ const ids = await store.list();
95
+ expect(ids).toEqual([]);
96
+ });
97
+
98
+ it('checks existence of a session', async () => {
99
+ await store.save(minimalSession());
100
+ expect(await store.exists('sess-001')).toBe(true);
101
+ expect(await store.exists('nope')).toBe(false);
102
+ });
103
+
104
+ it('writes valid JSON that can be parsed independently', async () => {
105
+ await store.save(minimalSession());
106
+ const filePath = join(tmpDir, 'sess-001.json');
107
+ const raw = await readFile(filePath, 'utf-8');
108
+ const parsed = JSON.parse(raw);
109
+ expect(parsed.sessionId).toBe('sess-001');
110
+ });
111
+
112
+ it('deletes a session', async () => {
113
+ await store.save(minimalSession());
114
+ expect(await store.exists('sess-001')).toBe(true);
115
+ await store.delete('sess-001');
116
+ expect(await store.exists('sess-001')).toBe(false);
117
+ });
118
+
119
+ it('handles concurrent saves to different sessions', async () => {
120
+ await Promise.all([
121
+ store.save(minimalSession('c1')),
122
+ store.save(minimalSession('c2')),
123
+ store.save(minimalSession('c3')),
124
+ ]);
125
+ const ids = await store.list();
126
+ expect(ids.sort()).toEqual(['c1', 'c2', 'c3']);
127
+ });
128
+
129
+ it('rejects session with invalid schema on load', async () => {
130
+ // Write an invalid file directly
131
+ const { writeFile, mkdir } = await import('node:fs/promises');
132
+ await mkdir(tmpDir, { recursive: true });
133
+ await writeFile(join(tmpDir, 'bad.json'), JSON.stringify({ invalid: true }));
134
+ await expect(store.load('bad')).rejects.toThrow();
135
+ });
136
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Tests for ActivitySpinner module (T084).
3
+ *
4
+ * Verifies spinner lifecycle methods: startThinking, startToolCall,
5
+ * completeToolCall, stop, isActive, TTY/JSON suppression, and
6
+ * that ora is configured with discardStdin: false to avoid
7
+ * conflicting with the app's readline on process.stdin.
8
+ */
9
+ import { describe, it, expect, beforeEach } from 'vitest';
10
+ import { Writable } from 'node:stream';
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ import { ActivitySpinner, createNoOpSpinner } from '../../../src/shared/activitySpinner.js';
16
+
17
+ // ── Helpers ─────────────────────────────────────────────────────────────────
18
+
19
+ /** Create a writable stream that captures output. */
20
+ function createCaptureStream(): Writable & { getOutput: () => string } {
21
+ const chunks: string[] = [];
22
+ const stream = new Writable({
23
+ write(chunk, _encoding, callback) {
24
+ chunks.push(chunk.toString());
25
+ callback();
26
+ },
27
+ });
28
+ (stream as Writable & { getOutput: () => string }).getOutput = () => chunks.join('');
29
+ return stream as Writable & { getOutput: () => string };
30
+ }
31
+
32
+ // ── Tests ────────────────────────────────────────────────────────────────────
33
+
34
+ describe('ActivitySpinner (T084)', () => {
35
+ describe('TTY mode (enabled)', () => {
36
+ let stream: ReturnType<typeof createCaptureStream>;
37
+ let spinner: ActivitySpinner;
38
+
39
+ beforeEach(() => {
40
+ stream = createCaptureStream();
41
+ spinner = new ActivitySpinner({
42
+ isTTY: true,
43
+ isJsonMode: false,
44
+ stream,
45
+ });
46
+ });
47
+
48
+ it('startThinking() starts a spinner', () => {
49
+ spinner.startThinking();
50
+ expect(spinner.isActive()).toBe(true);
51
+ spinner.stop();
52
+ });
53
+
54
+ it('startToolCall() starts/updates spinner with tool name', () => {
55
+ spinner.startToolCall('WorkIQ');
56
+ expect(spinner.isActive()).toBe(true);
57
+ spinner.stop();
58
+ });
59
+
60
+ it('startThinking() then startToolCall() transitions spinner text', () => {
61
+ spinner.startThinking();
62
+ expect(spinner.isActive()).toBe(true);
63
+ spinner.startToolCall('Context7');
64
+ expect(spinner.isActive()).toBe(true);
65
+ spinner.stop();
66
+ });
67
+
68
+ it('completeToolCall() stops spinner and prints summary', () => {
69
+ spinner.startToolCall('WorkIQ');
70
+ spinner.completeToolCall('WorkIQ', 'Found 12 processes');
71
+
72
+ expect(spinner.isActive()).toBe(false);
73
+ // Summary output is now handled by io.writeToolSummary(),
74
+ // not by the spinner itself.
75
+ });
76
+
77
+ it('stop() clears any active spinner', () => {
78
+ spinner.startThinking();
79
+ expect(spinner.isActive()).toBe(true);
80
+ spinner.stop();
81
+ expect(spinner.isActive()).toBe(false);
82
+ });
83
+
84
+ it('isActive() returns false when no spinner is running', () => {
85
+ expect(spinner.isActive()).toBe(false);
86
+ });
87
+
88
+ it('stop() is safe to call when no spinner is active', () => {
89
+ expect(() => spinner.stop()).not.toThrow();
90
+ });
91
+
92
+ it('completeToolCall() works even if spinner was already stopped', () => {
93
+ spinner.completeToolCall('GitHub', '3 repos found');
94
+ // Should not throw; summary output handled by io.writeToolSummary()
95
+ });
96
+
97
+ it('handles multiple sequential tool calls', () => {
98
+ // First tool
99
+ spinner.startToolCall('WorkIQ');
100
+ spinner.completeToolCall('WorkIQ', 'Found 5 processes');
101
+
102
+ // Second tool
103
+ spinner.startToolCall('Context7');
104
+ spinner.completeToolCall('Context7', '12 docs retrieved');
105
+
106
+ // Spinner should be inactive after all tools complete
107
+ expect(spinner.isActive()).toBe(false);
108
+ // Summary output handled by io.writeToolSummary(), not the spinner
109
+ });
110
+ });
111
+
112
+ describe('non-TTY mode (disabled)', () => {
113
+ let stream: ReturnType<typeof createCaptureStream>;
114
+ let spinner: ActivitySpinner;
115
+
116
+ beforeEach(() => {
117
+ stream = createCaptureStream();
118
+ spinner = new ActivitySpinner({
119
+ isTTY: false,
120
+ isJsonMode: false,
121
+ stream,
122
+ });
123
+ });
124
+
125
+ it('startThinking() is a no-op', () => {
126
+ spinner.startThinking();
127
+ expect(spinner.isActive()).toBe(false);
128
+ });
129
+
130
+ it('startToolCall() is a no-op', () => {
131
+ spinner.startToolCall('WorkIQ');
132
+ expect(spinner.isActive()).toBe(false);
133
+ });
134
+
135
+ it('completeToolCall() is a no-op (no output)', () => {
136
+ spinner.completeToolCall('WorkIQ', 'Found stuff');
137
+ expect(stream.getOutput()).toBe('');
138
+ });
139
+
140
+ it('stop() is safe and a no-op', () => {
141
+ expect(() => spinner.stop()).not.toThrow();
142
+ });
143
+ });
144
+
145
+ describe('JSON mode (disabled)', () => {
146
+ let stream: ReturnType<typeof createCaptureStream>;
147
+ let spinner: ActivitySpinner;
148
+
149
+ beforeEach(() => {
150
+ stream = createCaptureStream();
151
+ spinner = new ActivitySpinner({
152
+ isTTY: true,
153
+ isJsonMode: true,
154
+ stream,
155
+ });
156
+ });
157
+
158
+ it('all operations are no-ops in JSON mode', () => {
159
+ spinner.startThinking();
160
+ expect(spinner.isActive()).toBe(false);
161
+ spinner.startToolCall('WorkIQ');
162
+ expect(spinner.isActive()).toBe(false);
163
+ spinner.completeToolCall('WorkIQ', 'data');
164
+ expect(stream.getOutput()).toBe('');
165
+ spinner.stop();
166
+ });
167
+ });
168
+
169
+ describe('createNoOpSpinner', () => {
170
+ it('returns a spinner where all operations are no-ops', () => {
171
+ const noop = createNoOpSpinner();
172
+ expect(noop.isActive()).toBe(false);
173
+ noop.startThinking();
174
+ expect(noop.isActive()).toBe(false);
175
+ noop.startToolCall('TestTool');
176
+ expect(noop.isActive()).toBe(false);
177
+ noop.stop();
178
+ });
179
+ });
180
+
181
+ describe('ora configuration', () => {
182
+ it('creates ora with discardStdin: false to avoid stdin conflicts', () => {
183
+ const stream = createCaptureStream();
184
+ const spinner = new ActivitySpinner({ isTTY: true, isJsonMode: false, stream });
185
+
186
+ // Start thinking to trigger ora creation
187
+ spinner.startThinking();
188
+
189
+ // Access the internal ora instance to verify the option.
190
+ // ora stores options in a private field, but we can verify behaviour
191
+ // by checking that the spinner was created (isActive) and that our
192
+ // code explicitly passes discardStdin: false in the source.
193
+ expect(spinner.isActive()).toBe(true);
194
+
195
+ // Verify via source inspection: read the activitySpinner source and
196
+ // confirm discardStdin: false is present in all ora() calls
197
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
198
+ const source = fs.readFileSync(
199
+ path.resolve(__dirname, '../../../src/shared/activitySpinner.ts'),
200
+ 'utf8',
201
+ );
202
+ // Count ora constructor calls vs discardStdin: false occurrences
203
+ const oraCallCount = (source.match(/ora\(\{/g) || []).length;
204
+ const discardFalseCount = (source.match(/discardStdin:\s*false/g) || []).length;
205
+ expect(oraCallCount).toBeGreaterThan(0);
206
+ expect(discardFalseCount).toBe(oraCallCount);
207
+
208
+ spinner.stop();
209
+ });
210
+ });
211
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Cards loader tests.
3
+ *
4
+ * Validates that the AI Discovery Cards dataset:
5
+ * - Loads and validates against the Zod schema
6
+ * - Filters cards by category
7
+ * - Searches cards by keyword
8
+ * - Caches the dataset after first load
9
+ */
10
+ import { describe, it, expect } from 'vitest';
11
+
12
+ import { loadCardsDataset, getCardsByCategory, searchCards } from '../../../src/shared/data/cardsLoader.js';
13
+
14
+ describe('cardsLoader', () => {
15
+ it('loads the cards dataset successfully', async () => {
16
+ const dataset = await loadCardsDataset();
17
+ expect(dataset).toBeDefined();
18
+ expect(dataset.categories).toBeInstanceOf(Array);
19
+ expect(dataset.categories.length).toBeGreaterThan(0);
20
+ expect(dataset.cards).toBeInstanceOf(Array);
21
+ expect(dataset.cards.length).toBeGreaterThan(0);
22
+ });
23
+
24
+ it('cards have required fields', async () => {
25
+ const dataset = await loadCardsDataset();
26
+ for (const card of dataset.cards) {
27
+ expect(card.cardId).toBeDefined();
28
+ expect(card.category).toBeDefined();
29
+ expect(card.title).toBeDefined();
30
+ expect(card.description).toBeDefined();
31
+ expect(card.typicalScenarios).toBeInstanceOf(Array);
32
+ expect(card.azureServices).toBeInstanceOf(Array);
33
+ }
34
+ });
35
+
36
+ it('categories are non-empty strings', async () => {
37
+ const dataset = await loadCardsDataset();
38
+ for (const cat of dataset.categories) {
39
+ expect(typeof cat).toBe('string');
40
+ expect(cat.length).toBeGreaterThan(0);
41
+ }
42
+ });
43
+
44
+ it('filters cards by category', async () => {
45
+ const dataset = await loadCardsDataset();
46
+ const firstCategory = dataset.categories[0];
47
+ const filtered = await getCardsByCategory(firstCategory);
48
+
49
+ expect(filtered.length).toBeGreaterThan(0);
50
+ expect(filtered.every(c => c.category === firstCategory)).toBe(true);
51
+ });
52
+
53
+ it('returns empty array for unknown category', async () => {
54
+ const filtered = await getCardsByCategory('NonexistentCategory123');
55
+ expect(filtered).toEqual([]);
56
+ });
57
+
58
+ it('searches cards by keyword in title', async () => {
59
+ const dataset = await loadCardsDataset();
60
+ const firstCard = dataset.cards[0];
61
+ // Search for a word from the first card's title
62
+ const keyword = firstCard.title.split(' ')[0];
63
+ const results = await searchCards(keyword);
64
+
65
+ expect(results.length).toBeGreaterThan(0);
66
+ expect(results.some(c => c.cardId === firstCard.cardId)).toBe(true);
67
+ });
68
+
69
+ it('searches cards case-insensitively', async () => {
70
+ const dataset = await loadCardsDataset();
71
+ const firstCard = dataset.cards[0];
72
+ const keyword = firstCard.title.split(' ')[0].toLowerCase();
73
+ const results = await searchCards(keyword);
74
+
75
+ expect(results.length).toBeGreaterThan(0);
76
+ });
77
+
78
+ it('returns empty for unmatched search', async () => {
79
+ const results = await searchCards('xyzzy_nonexistent_query_99999');
80
+ expect(results).toEqual([]);
81
+ });
82
+
83
+ it('caches dataset on subsequent loads', async () => {
84
+ const first = await loadCardsDataset();
85
+ const second = await loadCardsDataset();
86
+ // Same object reference (cached)
87
+ expect(first).toBe(second);
88
+ });
89
+ });