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,564 @@
1
+ /**
2
+ * T047: Unit tests for McpContextEnricher.
3
+ *
4
+ * Verifies:
5
+ * - Queries Context7 for library docs when dependencies listed in plan
6
+ * - Queries Azure MCP when plan mentions Azure services
7
+ * - Calls web.search when stuckIterations > 0
8
+ * - Falls back gracefully when MCP services unavailable
9
+ * - Returns structured context string suitable for prompt injection
10
+ */
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+
13
+ import { McpContextEnricher } from '../../../src/develop/mcpContextEnricher.js';
14
+ import type { McpManager } from '../../../src/mcp/mcpManager.js';
15
+
16
+ // ── Mock web search ───────────────────────────────────────────────────────────
17
+
18
+ vi.mock('../../../src/mcp/webSearch.js', () => ({
19
+ isWebSearchConfigured: vi.fn(() => false),
20
+ }));
21
+
22
+ // ── Helpers ───────────────────────────────────────────────────────────────────
23
+
24
+ function makeMcpManager(
25
+ availableServers: string[] = [],
26
+ callToolImpl?: (
27
+ server: string,
28
+ tool: string,
29
+ args: Record<string, unknown>,
30
+ ) => Promise<Record<string, unknown>>,
31
+ ): McpManager {
32
+ return {
33
+ isAvailable: (name: string) => availableServers.includes(name),
34
+ listServers: () => availableServers,
35
+ getServerConfig: () => undefined,
36
+ markConnected: () => {},
37
+ markDisconnected: () => {},
38
+ getAllConfigs: () => [],
39
+ callTool: callToolImpl
40
+ ? vi.fn(callToolImpl)
41
+ : vi.fn().mockRejectedValue(new Error('not wired')),
42
+ } as unknown as McpManager;
43
+ }
44
+
45
+ describe('McpContextEnricher', () => {
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+ });
49
+
50
+ describe('constructor', () => {
51
+ it('stores mcpManager reference', () => {
52
+ const manager = makeMcpManager();
53
+ const enricher = new McpContextEnricher(manager);
54
+ expect(enricher.mcpManager).toBe(manager);
55
+ });
56
+ });
57
+
58
+ describe('enrich() — Context7', () => {
59
+ it('queries Context7 when available and dependencies listed', async () => {
60
+ const callTool = vi
61
+ .fn()
62
+ .mockResolvedValueOnce({ libraryId: 'express-lib-id' })
63
+ .mockResolvedValueOnce({ content: 'Express.js API docs here' })
64
+ .mockResolvedValueOnce({ libraryId: 'zod-lib-id' })
65
+ .mockResolvedValueOnce({ content: 'Zod schema validation docs' });
66
+ const manager = makeMcpManager(['context7'], callTool);
67
+ const enricher = new McpContextEnricher(manager);
68
+
69
+ const result = await enricher.enrich({
70
+ mcpManager: manager,
71
+ dependencies: ['express', 'zod'],
72
+ });
73
+
74
+ expect(result.combined).toBeTruthy();
75
+ expect(result.libraryDocs).toBeDefined();
76
+ // Should have called resolve-library-id and query-docs for each dep
77
+ expect(callTool).toHaveBeenCalledWith(
78
+ 'context7',
79
+ 'resolve-library-id',
80
+ {
81
+ libraryName: 'express',
82
+ },
83
+ { timeoutMs: 30_000 },
84
+ );
85
+ expect(callTool).toHaveBeenCalledWith(
86
+ 'context7',
87
+ 'query-docs',
88
+ {
89
+ libraryId: 'express-lib-id',
90
+ topic: 'express',
91
+ },
92
+ { timeoutMs: 30_000 },
93
+ );
94
+ });
95
+
96
+ it('falls back to npmjs link when callTool throws for a dependency', async () => {
97
+ const callTool = vi.fn().mockRejectedValue(new Error('not wired'));
98
+ const manager = makeMcpManager(['context7'], callTool);
99
+ const enricher = new McpContextEnricher(manager);
100
+
101
+ const result = await enricher.enrich({
102
+ mcpManager: manager,
103
+ dependencies: ['express'],
104
+ });
105
+
106
+ expect(result.libraryDocs).toBeDefined();
107
+ expect(result.libraryDocs).toContain('npmjs.com/package/express');
108
+ });
109
+
110
+ it('skips Context7 when not available', async () => {
111
+ const manager = makeMcpManager([]); // no context7
112
+ const enricher = new McpContextEnricher(manager);
113
+
114
+ const result = await enricher.enrich({
115
+ mcpManager: manager,
116
+ dependencies: ['express', 'zod'],
117
+ });
118
+
119
+ expect(result.libraryDocs).toBeUndefined();
120
+ expect(result.combined).toBe('');
121
+ });
122
+
123
+ it('skips Context7 when no dependencies listed', async () => {
124
+ const manager = makeMcpManager(['context7']);
125
+ const enricher = new McpContextEnricher(manager);
126
+
127
+ const result = await enricher.enrich({
128
+ mcpManager: manager,
129
+ dependencies: [],
130
+ });
131
+
132
+ expect(result.libraryDocs).toBeUndefined();
133
+ });
134
+
135
+ it('filters out type-only packages from Context7 queries', async () => {
136
+ const callTool = vi
137
+ .fn()
138
+ .mockResolvedValueOnce({ libraryId: 'express-id' })
139
+ .mockResolvedValueOnce({ content: 'Express docs' });
140
+ const manager = makeMcpManager(['context7'], callTool);
141
+ const enricher = new McpContextEnricher(manager);
142
+
143
+ const result = await enricher.enrich({
144
+ mcpManager: manager,
145
+ dependencies: ['@types/node', 'typescript', 'vitest', 'express'],
146
+ });
147
+
148
+ expect(result.libraryDocs).toBeDefined();
149
+ if (result.libraryDocs) {
150
+ expect(result.libraryDocs).toContain('express');
151
+ expect(result.libraryDocs).not.toContain('@types/node');
152
+ expect(result.libraryDocs).not.toContain('typescript');
153
+ }
154
+ // Only express should trigger callTool calls (2 calls: resolve + query)
155
+ expect(callTool).toHaveBeenCalledTimes(2);
156
+ });
157
+ });
158
+
159
+ describe('enrich() — Azure MCP', () => {
160
+ it('calls mcpManager.callTool for Azure documentation when available', async () => {
161
+ const callTool = vi.fn().mockResolvedValue({
162
+ content: 'Use managed identity for Cosmos DB authentication.',
163
+ });
164
+ const manager = makeMcpManager(['azure'], callTool);
165
+ const enricher = new McpContextEnricher(manager);
166
+
167
+ const result = await enricher.enrich({
168
+ mcpManager: manager,
169
+ architectureNotes: 'Use Azure Cosmos DB for data storage and Azure OpenAI for inference.',
170
+ });
171
+
172
+ expect(result.azureGuidance).toBeDefined();
173
+ expect(result.azureGuidance).toContain('managed identity');
174
+ expect(callTool).toHaveBeenCalledWith(
175
+ 'azure',
176
+ 'documentation',
177
+ expect.objectContaining({
178
+ query: expect.stringContaining('cosmos db'),
179
+ }),
180
+ { timeoutMs: 30_000 },
181
+ );
182
+ });
183
+
184
+ it('falls back to static guidance when callTool throws', async () => {
185
+ const callTool = vi.fn().mockRejectedValue(new Error('not wired'));
186
+ const manager = makeMcpManager(['azure'], callTool);
187
+ const enricher = new McpContextEnricher(manager);
188
+
189
+ const result = await enricher.enrich({
190
+ mcpManager: manager,
191
+ architectureNotes: 'Use Azure Cosmos DB for data storage.',
192
+ });
193
+
194
+ expect(result.azureGuidance).toBeDefined();
195
+ expect(result.azureGuidance).toContain('Detected Azure services');
196
+ expect(result.combined).toContain('Azure');
197
+ });
198
+
199
+ it('skips Azure MCP when not available', async () => {
200
+ const manager = makeMcpManager([]);
201
+ const enricher = new McpContextEnricher(manager);
202
+
203
+ const result = await enricher.enrich({
204
+ mcpManager: manager,
205
+ architectureNotes: 'Use Azure Cosmos DB for data storage.',
206
+ });
207
+
208
+ expect(result.azureGuidance).toBeUndefined();
209
+ });
210
+
211
+ it('skips Azure MCP when plan does not mention Azure services', async () => {
212
+ const manager = makeMcpManager(['azure']);
213
+ const enricher = new McpContextEnricher(manager);
214
+
215
+ const result = await enricher.enrich({
216
+ mcpManager: manager,
217
+ architectureNotes: 'Use PostgreSQL and Express. No cloud dependencies.',
218
+ });
219
+
220
+ expect(result.azureGuidance).toBeUndefined();
221
+ });
222
+
223
+ it('detects various Azure keywords', async () => {
224
+ const callTool = vi.fn().mockRejectedValue(new Error('not wired'));
225
+ const manager = makeMcpManager(['azure'], callTool);
226
+ const enricher = new McpContextEnricher(manager);
227
+
228
+ const azureKeywords = ['cosmos db', 'blob storage', 'service bus', 'key vault'];
229
+ for (const keyword of azureKeywords) {
230
+ const result = await enricher.enrich({
231
+ mcpManager: manager,
232
+ architectureNotes: `Use ${keyword} for the implementation.`,
233
+ });
234
+ expect(
235
+ result.azureGuidance,
236
+ `Expected Azure guidance for keyword: ${keyword}`,
237
+ ).toBeDefined();
238
+ }
239
+ });
240
+ });
241
+
242
+ describe('enrich() — web.search', () => {
243
+ it('calls web.search when configured and stuckIterations >= 2', async () => {
244
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
245
+ vi.mocked(isWebSearchConfigured).mockReturnValue(true);
246
+
247
+ const manager = makeMcpManager([]);
248
+ const enricher = new McpContextEnricher(manager);
249
+
250
+ const result = await enricher.enrich({
251
+ mcpManager: manager,
252
+ stuckIterations: 2,
253
+ failingTests: ['suite > test A fails with TypeError'],
254
+ });
255
+
256
+ expect(result.webSearchResults).toBeDefined();
257
+ });
258
+
259
+ it('skips web.search when stuckIterations < 2', async () => {
260
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
261
+ vi.mocked(isWebSearchConfigured).mockReturnValue(true);
262
+
263
+ const manager = makeMcpManager([]);
264
+ const enricher = new McpContextEnricher(manager);
265
+
266
+ const result = await enricher.enrich({
267
+ mcpManager: manager,
268
+ stuckIterations: 1,
269
+ failingTests: ['suite > test A'],
270
+ });
271
+
272
+ expect(result.webSearchResults).toBeUndefined();
273
+ });
274
+
275
+ it('skips web.search when not configured', async () => {
276
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
277
+ vi.mocked(isWebSearchConfigured).mockReturnValue(false);
278
+
279
+ const manager = makeMcpManager([]);
280
+ const enricher = new McpContextEnricher(manager);
281
+
282
+ const result = await enricher.enrich({
283
+ mcpManager: manager,
284
+ stuckIterations: 3,
285
+ failingTests: ['test fails'],
286
+ });
287
+
288
+ expect(result.webSearchResults).toBeUndefined();
289
+ });
290
+ });
291
+
292
+ describe('graceful degradation', () => {
293
+ it('returns empty context when all services unavailable', async () => {
294
+ const manager = makeMcpManager([]);
295
+ const enricher = new McpContextEnricher(manager);
296
+
297
+ const result = await enricher.enrich({
298
+ mcpManager: manager,
299
+ dependencies: ['express'],
300
+ architectureNotes: 'Use Azure and express',
301
+ stuckIterations: 5,
302
+ failingTests: ['test fails'],
303
+ });
304
+
305
+ // All services unavailable (web search mocked to false, no MCP servers)
306
+ expect(result.combined).toBe('');
307
+ });
308
+
309
+ it('returns combined context string when multiple services respond', async () => {
310
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
311
+ vi.mocked(isWebSearchConfigured).mockReturnValue(true);
312
+
313
+ const callTool = vi
314
+ .fn()
315
+ // Context7 resolve + query for 'express'
316
+ .mockResolvedValueOnce({ libraryId: 'express-id' })
317
+ .mockResolvedValueOnce({ content: 'Express framework docs' })
318
+ // Azure documentation
319
+ .mockResolvedValueOnce({ content: 'Azure Cosmos DB guidance' });
320
+ const manager = makeMcpManager(['context7', 'azure'], callTool);
321
+ const enricher = new McpContextEnricher(manager);
322
+
323
+ const result = await enricher.enrich({
324
+ mcpManager: manager,
325
+ dependencies: ['express'],
326
+ architectureNotes: 'Use Azure Cosmos DB',
327
+ stuckIterations: 2,
328
+ failingTests: ['test A fails'],
329
+ });
330
+
331
+ // combined should include sections from multiple services
332
+ expect(result.combined.length).toBeGreaterThan(0);
333
+ // Should have at least some context
334
+ const hasMultipleSections = (result.combined.match(/###/g) ?? []).length >= 1;
335
+ expect(hasMultipleSections).toBe(true);
336
+ });
337
+ });
338
+
339
+ // ── T009: Contract tests per contracts/context-enricher.md ──────────────
340
+
341
+ describe('queryContext7 — contract: response field fallbacks', () => {
342
+ it('uses response.id as fallback when response.libraryId is missing', async () => {
343
+ const callTool = vi
344
+ .fn()
345
+ .mockResolvedValueOnce({ id: '/expressjs/express' }) // fallback field
346
+ .mockResolvedValueOnce({ content: 'Express docs from id fallback' });
347
+ const manager = makeMcpManager(['context7'], callTool);
348
+ const enricher = new McpContextEnricher(manager);
349
+
350
+ const result = await enricher.enrich({
351
+ mcpManager: manager,
352
+ dependencies: ['express'],
353
+ });
354
+
355
+ expect(result.libraryDocs).toBeDefined();
356
+ expect(result.libraryDocs).toContain('Express docs from id fallback');
357
+ // Should have called query-docs with the resolved id
358
+ expect(callTool).toHaveBeenCalledWith(
359
+ 'context7',
360
+ 'query-docs',
361
+ {
362
+ libraryId: '/expressjs/express',
363
+ topic: 'express',
364
+ },
365
+ { timeoutMs: 30_000 },
366
+ );
367
+ });
368
+
369
+ it('uses response.text as fallback content when response.content is missing', async () => {
370
+ const callTool = vi
371
+ .fn()
372
+ .mockResolvedValueOnce({ libraryId: 'zod-id' })
373
+ .mockResolvedValueOnce({ text: 'Zod docs from text fallback' }); // text fallback
374
+ const manager = makeMcpManager(['context7'], callTool);
375
+ const enricher = new McpContextEnricher(manager);
376
+
377
+ const result = await enricher.enrich({
378
+ mcpManager: manager,
379
+ dependencies: ['zod'],
380
+ });
381
+
382
+ expect(result.libraryDocs).toBeDefined();
383
+ expect(result.libraryDocs).toContain('Zod docs from text fallback');
384
+ });
385
+
386
+ it('processes max 5 non-skipped dependencies', async () => {
387
+ const callTool = vi.fn().mockResolvedValue({ libraryId: 'lib-id', content: 'docs' });
388
+ const manager = makeMcpManager(['context7'], callTool);
389
+ const enricher = new McpContextEnricher(manager);
390
+
391
+ const deps = ['dep1', 'dep2', 'dep3', 'dep4', 'dep5', 'dep6', 'dep7'];
392
+ await enricher.enrich({
393
+ mcpManager: manager,
394
+ dependencies: deps,
395
+ });
396
+
397
+ // Only 5 non-skipped deps should be processed (2 calls each: resolve + query)
398
+ expect(callTool).toHaveBeenCalledTimes(10); // 5 * 2
399
+ });
400
+
401
+ it('falls back to npmjs link when both libraryId and id are missing', async () => {
402
+ const callTool = vi.fn().mockResolvedValueOnce({}); // no libraryId, no id
403
+ const manager = makeMcpManager(['context7'], callTool);
404
+ const enricher = new McpContextEnricher(manager);
405
+
406
+ const result = await enricher.enrich({
407
+ mcpManager: manager,
408
+ dependencies: ['unknown-pkg'],
409
+ });
410
+
411
+ expect(result.libraryDocs).toBeDefined();
412
+ expect(result.libraryDocs).toContain('npmjs.com/package/unknown-pkg');
413
+ });
414
+ });
415
+
416
+ describe('queryAzureMcp — contract: response field fallbacks', () => {
417
+ it('uses response.text as fallback when response.content is missing', async () => {
418
+ const callTool = vi.fn().mockResolvedValue({
419
+ text: 'Azure guidance from text fallback',
420
+ });
421
+ const manager = makeMcpManager(['azure'], callTool);
422
+ const enricher = new McpContextEnricher(manager);
423
+
424
+ const result = await enricher.enrich({
425
+ mcpManager: manager,
426
+ architectureNotes: 'Use Azure Cosmos DB',
427
+ });
428
+
429
+ expect(result.azureGuidance).toBeDefined();
430
+ expect(result.azureGuidance).toContain('Azure guidance from text fallback');
431
+ });
432
+ });
433
+
434
+ describe('queryWebSearch — contract: MCP-first then fallback', () => {
435
+ it('tries MCP callTool websearch before Azure AI Foundry bridge', async () => {
436
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
437
+ vi.mocked(isWebSearchConfigured).mockReturnValue(true);
438
+
439
+ const callTool = vi.fn().mockResolvedValue({
440
+ content: 'MCP search results here',
441
+ });
442
+ const manager = makeMcpManager(['websearch'], callTool);
443
+ const enricher = new McpContextEnricher(manager);
444
+
445
+ const result = await enricher.enrich({
446
+ mcpManager: manager,
447
+ stuckIterations: 2,
448
+ failingTests: ['test A fails with TypeError'],
449
+ });
450
+
451
+ expect(result.webSearchResults).toContain('MCP search results here');
452
+ expect(callTool).toHaveBeenCalledWith(
453
+ 'websearch',
454
+ 'search',
455
+ expect.objectContaining({
456
+ query: expect.stringContaining('how to fix'),
457
+ }),
458
+ { timeoutMs: 30_000 },
459
+ );
460
+ });
461
+ });
462
+
463
+ // ── T010: Web search gating tests ───────────────────────────────────────
464
+
465
+ describe('enrich() — web search stuckIterations gating', () => {
466
+ it('MUST NOT invoke queryWebSearch when stuckIterations < 2', async () => {
467
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
468
+ vi.mocked(isWebSearchConfigured).mockReturnValue(true);
469
+
470
+ const callTool = vi.fn().mockResolvedValue({ content: 'should not be called' });
471
+ const manager = makeMcpManager(['websearch'], callTool);
472
+ const enricher = new McpContextEnricher(manager);
473
+
474
+ const result = await enricher.enrich({
475
+ mcpManager: manager,
476
+ stuckIterations: 1,
477
+ failingTests: ['test A fails'],
478
+ });
479
+
480
+ expect(result.webSearchResults).toBeUndefined();
481
+ // websearch callTool should NOT have been called
482
+ const webSearchCalls = callTool.mock.calls.filter((c: unknown[]) => c[0] === 'websearch');
483
+ expect(webSearchCalls).toHaveLength(0);
484
+ });
485
+
486
+ it('MUST invoke queryWebSearch when stuckIterations >= 2', async () => {
487
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
488
+ vi.mocked(isWebSearchConfigured).mockReturnValue(true);
489
+
490
+ const callTool = vi.fn().mockResolvedValue({ content: 'web search results' });
491
+ const manager = makeMcpManager(['websearch'], callTool);
492
+ const enricher = new McpContextEnricher(manager);
493
+
494
+ const result = await enricher.enrich({
495
+ mcpManager: manager,
496
+ stuckIterations: 2,
497
+ failingTests: ['test B fails with ReferenceError'],
498
+ });
499
+
500
+ expect(result.webSearchResults).toBeDefined();
501
+ const webSearchCalls = callTool.mock.calls.filter((c: unknown[]) => c[0] === 'websearch');
502
+ expect(webSearchCalls.length).toBeGreaterThan(0);
503
+ });
504
+
505
+ it('MUST invoke queryWebSearch when stuckIterations is 3', async () => {
506
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
507
+ vi.mocked(isWebSearchConfigured).mockReturnValue(true);
508
+
509
+ const callTool = vi.fn().mockResolvedValue({ content: 'search data' });
510
+ const manager = makeMcpManager(['websearch'], callTool);
511
+ const enricher = new McpContextEnricher(manager);
512
+
513
+ const result = await enricher.enrich({
514
+ mcpManager: manager,
515
+ stuckIterations: 3,
516
+ failingTests: ['test C'],
517
+ });
518
+
519
+ expect(result.webSearchResults).toBeDefined();
520
+ });
521
+
522
+ it('does not invoke queryWebSearch when failingTests is empty', async () => {
523
+ const { isWebSearchConfigured } = await import('../../../src/mcp/webSearch.js');
524
+ vi.mocked(isWebSearchConfigured).mockReturnValue(true);
525
+
526
+ const callTool = vi.fn();
527
+ const manager = makeMcpManager(['websearch'], callTool);
528
+ const enricher = new McpContextEnricher(manager);
529
+
530
+ const result = await enricher.enrich({
531
+ mcpManager: manager,
532
+ stuckIterations: 5,
533
+ failingTests: [],
534
+ });
535
+
536
+ expect(result.webSearchResults).toBeUndefined();
537
+ });
538
+ });
539
+
540
+ describe('enrich() — concurrent execution', () => {
541
+ it('runs queryContext7 and queryAzureMcp (results combined)', async () => {
542
+ const callTool = vi
543
+ .fn()
544
+ // Context7 resolve + query
545
+ .mockResolvedValueOnce({ libraryId: 'express-id' })
546
+ .mockResolvedValueOnce({ content: 'Express docs' })
547
+ // Azure documentation
548
+ .mockResolvedValueOnce({ content: 'Azure best practices' });
549
+ const manager = makeMcpManager(['context7', 'azure'], callTool);
550
+ const enricher = new McpContextEnricher(manager);
551
+
552
+ const result = await enricher.enrich({
553
+ mcpManager: manager,
554
+ dependencies: ['express'],
555
+ architectureNotes: 'Use Azure Cosmos DB for storage',
556
+ });
557
+
558
+ expect(result.libraryDocs).toBeDefined();
559
+ expect(result.azureGuidance).toBeDefined();
560
+ expect(result.combined).toContain('Library Documentation');
561
+ expect(result.combined).toContain('Azure');
562
+ });
563
+ });
564
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * T040: Unit tests for PoC output validator.
3
+ *
4
+ * Tests all 8 validation checks from poc-output contract.
5
+ */
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import { tmpdir } from 'node:os';
10
+
11
+ import { validatePocOutput } from '../../../src/develop/pocScaffolder.js';
12
+
13
+ describe('validatePocOutput', () => {
14
+ let tmpDir: string;
15
+
16
+ beforeEach(async () => {
17
+ tmpDir = await mkdtemp(join(tmpdir(), 'sofia-validate-poc-'));
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await rm(tmpDir, { recursive: true, force: true });
22
+ });
23
+
24
+ async function createCompleteScaffold(): Promise<void> {
25
+ await mkdir(join(tmpDir, 'src'), { recursive: true });
26
+ await mkdir(join(tmpDir, 'tests'), { recursive: true });
27
+ await writeFile(join(tmpDir, 'package.json'), JSON.stringify({
28
+ name: 'test-poc',
29
+ version: '0.1.0',
30
+ scripts: { test: 'vitest run', build: 'tsc', start: 'node dist/index.js' },
31
+ dependencies: {},
32
+ devDependencies: { vitest: '^3.0.0', typescript: '^5.0.0' },
33
+ }), 'utf-8');
34
+ await writeFile(join(tmpDir, 'README.md'), '# Test PoC\n\nA test proof of concept.', 'utf-8');
35
+ await writeFile(join(tmpDir, 'tsconfig.json'), JSON.stringify({
36
+ compilerOptions: { target: 'ES2022', module: 'Node16', strict: true },
37
+ }), 'utf-8');
38
+ await writeFile(join(tmpDir, '.gitignore'), 'node_modules/\ndist/\ncoverage/\n', 'utf-8');
39
+ await writeFile(join(tmpDir, '.sofia-metadata.json'), JSON.stringify({
40
+ sessionId: 'test-001',
41
+ featureSpec: '002-poc-generation',
42
+ }), 'utf-8');
43
+ await writeFile(join(tmpDir, 'src', 'index.ts'), 'export function main() { return "ok"; }', 'utf-8');
44
+ await writeFile(join(tmpDir, 'tests', 'index.test.ts'), 'import { test, expect } from "vitest"; test("works", () => { expect(true).toBe(true); });', 'utf-8');
45
+ }
46
+
47
+ it('1. validates package.json exists and has test script', async () => {
48
+ await createCompleteScaffold();
49
+ const result = await validatePocOutput(tmpDir);
50
+ expect(result.valid).toBe(true);
51
+ });
52
+
53
+ it('2. reports missing package.json', async () => {
54
+ await createCompleteScaffold();
55
+ const { rm } = await import('node:fs/promises');
56
+ await rm(join(tmpDir, 'package.json'));
57
+ const result = await validatePocOutput(tmpDir);
58
+ expect(result.valid).toBe(false);
59
+ expect(result.missingFiles).toContain('package.json');
60
+ });
61
+
62
+ it('3. reports missing README.md', async () => {
63
+ await createCompleteScaffold();
64
+ const { rm } = await import('node:fs/promises');
65
+ await rm(join(tmpDir, 'README.md'));
66
+ const result = await validatePocOutput(tmpDir);
67
+ expect(result.valid).toBe(false);
68
+ expect(result.missingFiles).toContain('README.md');
69
+ });
70
+
71
+ it('4. reports missing tsconfig.json', async () => {
72
+ await createCompleteScaffold();
73
+ const { rm } = await import('node:fs/promises');
74
+ await rm(join(tmpDir, 'tsconfig.json'));
75
+ const result = await validatePocOutput(tmpDir);
76
+ expect(result.valid).toBe(false);
77
+ expect(result.missingFiles).toContain('tsconfig.json');
78
+ });
79
+
80
+ it('5. reports missing .gitignore', async () => {
81
+ await createCompleteScaffold();
82
+ const { rm } = await import('node:fs/promises');
83
+ await rm(join(tmpDir, '.gitignore'));
84
+ const result = await validatePocOutput(tmpDir);
85
+ expect(result.valid).toBe(false);
86
+ expect(result.missingFiles).toContain('.gitignore');
87
+ });
88
+
89
+ it('6. reports missing .sofia-metadata.json', async () => {
90
+ await createCompleteScaffold();
91
+ const { rm } = await import('node:fs/promises');
92
+ await rm(join(tmpDir, '.sofia-metadata.json'));
93
+ const result = await validatePocOutput(tmpDir);
94
+ expect(result.valid).toBe(false);
95
+ expect(result.missingFiles).toContain('.sofia-metadata.json');
96
+ });
97
+
98
+ it('7. reports no TypeScript files in src/', async () => {
99
+ await createCompleteScaffold();
100
+ const { rm } = await import('node:fs/promises');
101
+ await rm(join(tmpDir, 'src', 'index.ts'));
102
+ const result = await validatePocOutput(tmpDir);
103
+ expect(result.valid).toBe(false);
104
+ expect(result.errors.some((e) => e.includes('src/'))).toBe(true);
105
+ });
106
+
107
+ it('8. reports no test files in tests/', async () => {
108
+ await createCompleteScaffold();
109
+ const { rm } = await import('node:fs/promises');
110
+ await rm(join(tmpDir, 'tests', 'index.test.ts'));
111
+ const result = await validatePocOutput(tmpDir);
112
+ expect(result.valid).toBe(false);
113
+ expect(result.errors.some((e) => e.includes('tests/'))).toBe(true);
114
+ });
115
+
116
+ it('reports error when package.json has no test script', async () => {
117
+ await createCompleteScaffold();
118
+ await writeFile(join(tmpDir, 'package.json'), JSON.stringify({
119
+ name: 'test-poc',
120
+ scripts: { build: 'tsc' }, // no test script
121
+ }), 'utf-8');
122
+ const result = await validatePocOutput(tmpDir);
123
+ expect(result.valid).toBe(false);
124
+ expect(result.errors).toContain('package.json is missing "test" script');
125
+ });
126
+
127
+ it('reports valid=true when all checks pass', async () => {
128
+ await createCompleteScaffold();
129
+ const result = await validatePocOutput(tmpDir);
130
+ expect(result.valid).toBe(true);
131
+ expect(result.missingFiles).toHaveLength(0);
132
+ expect(result.errors).toHaveLength(0);
133
+ });
134
+ });