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,1439 @@
1
+ /**
2
+ * T022: Unit tests for RalphLoop orchestrator.
3
+ *
4
+ * Verifies:
5
+ * - Lifecycle (validate → scaffold → install → iterate)
6
+ * - Termination on tests-passing
7
+ * - Termination on max-iterations
8
+ * - Iteration count tracking
9
+ * - Session persistence callback called after each iteration
10
+ * - Ctrl+C handling sets user-stopped
11
+ */
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
+ import { mkdtemp, rm } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { tmpdir } from 'node:os';
16
+
17
+ import { RalphLoop } from '../../../src/develop/ralphLoop.js';
18
+ import { PocScaffolder, validatePocOutput } from '../../../src/develop/pocScaffolder.js';
19
+ import { TestRunner } from '../../../src/develop/testRunner.js';
20
+ import { GitHubMcpAdapter } from '../../../src/develop/githubMcpAdapter.js';
21
+ import type { WorkshopSession } from '../../../src/shared/schemas/session.js';
22
+ import type { LoopIO } from '../../../src/loop/conversationLoop.js';
23
+ import type { CopilotClient } from '../../../src/shared/copilotClient.js';
24
+ import type { TestResults } from '../../../src/shared/schemas/session.js';
25
+
26
+ // ── Mocks ─────────────────────────────────────────────────────────────────────
27
+
28
+ // Mock npm install to always succeed
29
+ vi.mock('node:child_process', async (importOriginal) => {
30
+ const actual = await importOriginal<typeof import('node:child_process')>();
31
+ return {
32
+ ...actual,
33
+ spawn: vi.fn((cmd: string, args: string[]) => {
34
+ if (cmd === 'npm' && args.includes('install')) {
35
+ // Simulate successful npm install
36
+ const emitter = {
37
+ stdout: { on: vi.fn() },
38
+ stderr: { on: vi.fn() },
39
+ on: vi.fn((event: string, cb: (code: number) => void) => {
40
+ if (event === 'close') cb(0);
41
+ }),
42
+ kill: vi.fn(),
43
+ killed: false,
44
+ };
45
+ return emitter;
46
+ }
47
+ return actual.spawn(cmd, args);
48
+ }),
49
+ };
50
+ });
51
+
52
+ // Mock validatePocOutput — default: valid
53
+ vi.mock('../../../src/develop/pocScaffolder.js', async (importOriginal) => {
54
+ const actual = await importOriginal<typeof import('../../../src/develop/pocScaffolder.js')>();
55
+ return {
56
+ ...actual,
57
+ validatePocOutput: vi.fn().mockResolvedValue({ valid: true, missingFiles: [], errors: [] }),
58
+ };
59
+ });
60
+
61
+ // ── Helpers ───────────────────────────────────────────────────────────────────
62
+
63
+ function makeSession(overrides?: Partial<WorkshopSession>): WorkshopSession {
64
+ const now = new Date().toISOString();
65
+ return {
66
+ sessionId: 'ralph-test-session',
67
+ schemaVersion: '1.0.0',
68
+ createdAt: now,
69
+ updatedAt: now,
70
+ phase: 'Develop',
71
+ status: 'Active',
72
+ participants: [],
73
+ artifacts: { generatedFiles: [] },
74
+ ideas: [
75
+ {
76
+ id: 'idea-1',
77
+ title: 'Test AI App',
78
+ description: 'A test AI application.',
79
+ workflowStepIds: [],
80
+ },
81
+ ],
82
+ selection: {
83
+ ideaId: 'idea-1',
84
+ selectionRationale: 'Best idea',
85
+ confirmedByUser: true,
86
+ },
87
+ plan: {
88
+ milestones: [{ id: 'm1', title: 'Setup', items: [] }],
89
+ architectureNotes: 'Node.js + TypeScript',
90
+ },
91
+ ...overrides,
92
+ };
93
+ }
94
+
95
+ function makeIo(): LoopIO {
96
+ return {
97
+ write: vi.fn(),
98
+ writeActivity: vi.fn(),
99
+ writeToolSummary: vi.fn(),
100
+ readInput: vi.fn().mockResolvedValue(null),
101
+ showDecisionGate: vi.fn(),
102
+ isJsonMode: false,
103
+ isTTY: false,
104
+ };
105
+ }
106
+
107
+ function makePassingClient(): CopilotClient {
108
+ return {
109
+ createSession: vi.fn().mockResolvedValue({
110
+ send: vi.fn().mockReturnValue({
111
+ async *[Symbol.asyncIterator]() {
112
+ yield {
113
+ type: 'TextDelta',
114
+ text: '```typescript file=src/index.ts\nexport function main() { return "ok"; }\n```',
115
+ timestamp: '',
116
+ };
117
+ },
118
+ }),
119
+ getHistory: () => [],
120
+ }),
121
+ };
122
+ }
123
+
124
+ function makePassingTestRunner(): TestRunner {
125
+ return {
126
+ run: vi.fn().mockResolvedValue({
127
+ passed: 2,
128
+ failed: 0,
129
+ skipped: 0,
130
+ total: 2,
131
+ durationMs: 300,
132
+ failures: [],
133
+ rawOutput: 'All tests pass',
134
+ } satisfies TestResults),
135
+ } as unknown as TestRunner;
136
+ }
137
+
138
+ function _makeFailingTestRunner(failCount = 1): TestRunner {
139
+ let callCount = 0;
140
+ return {
141
+ run: vi.fn().mockImplementation(async (): Promise<TestResults> => {
142
+ callCount++;
143
+ if (callCount > failCount) {
144
+ return {
145
+ passed: 2,
146
+ failed: 0,
147
+ skipped: 0,
148
+ total: 2,
149
+ durationMs: 300,
150
+ failures: [],
151
+ rawOutput: 'All pass',
152
+ };
153
+ }
154
+ return {
155
+ passed: 0,
156
+ failed: 1,
157
+ skipped: 0,
158
+ total: 1,
159
+ durationMs: 400,
160
+ failures: [{ testName: 'suite > test A', message: 'Expected 1 but got 0' }],
161
+ rawOutput: 'FAIL tests/index.test.ts',
162
+ };
163
+ }),
164
+ } as unknown as TestRunner;
165
+ }
166
+
167
+ function makeAlwaysFailingTestRunner(): TestRunner {
168
+ return {
169
+ run: vi.fn().mockResolvedValue({
170
+ passed: 0,
171
+ failed: 1,
172
+ skipped: 0,
173
+ total: 1,
174
+ durationMs: 400,
175
+ failures: [{ testName: 'suite > always fail', message: 'always fails' }],
176
+ rawOutput: 'FAIL',
177
+ } satisfies TestResults),
178
+ } as unknown as TestRunner;
179
+ }
180
+
181
+ function makeFakeScaffolder(outputDir: string): PocScaffolder {
182
+ return {
183
+ scaffold: vi.fn().mockImplementation(async () => {
184
+ // Create minimal required files
185
+ const { writeFile, mkdir } = await import('node:fs/promises');
186
+ await mkdir(join(outputDir, 'src'), { recursive: true });
187
+ await mkdir(join(outputDir, 'tests'), { recursive: true });
188
+ await writeFile(
189
+ join(outputDir, 'package.json'),
190
+ JSON.stringify({
191
+ name: 'test-poc',
192
+ scripts: { test: 'vitest run' },
193
+ dependencies: {},
194
+ devDependencies: {},
195
+ }),
196
+ 'utf-8',
197
+ );
198
+ await writeFile(join(outputDir, 'src', 'index.ts'), 'export function main() {}', 'utf-8');
199
+ return {
200
+ createdFiles: ['package.json', 'src/index.ts'],
201
+ skippedFiles: [],
202
+ context: {
203
+ projectName: 'test-poc',
204
+ ideaTitle: 'Test',
205
+ ideaDescription: 'Test',
206
+ techStack: { language: 'TypeScript', runtime: 'Node.js 20', testRunner: 'npm test' },
207
+ planSummary: 'Test',
208
+ sessionId: 'ralph-test-session',
209
+ outputDir,
210
+ },
211
+ };
212
+ }),
213
+ getTemplateFiles: () => ['package.json', 'src/index.ts'],
214
+ } as unknown as PocScaffolder;
215
+ }
216
+
217
+ // ── Tests ─────────────────────────────────────────────────────────────────────
218
+
219
+ describe('RalphLoop', () => {
220
+ let tmpDir: string;
221
+
222
+ beforeEach(async () => {
223
+ tmpDir = await mkdtemp(join(tmpdir(), 'sofia-ralph-test-'));
224
+ });
225
+
226
+ afterEach(async () => {
227
+ await rm(tmpDir, { recursive: true, force: true });
228
+ vi.clearAllMocks();
229
+ });
230
+
231
+ describe('validation', () => {
232
+ it('throws when session has no selection', async () => {
233
+ const session = makeSession({ selection: undefined });
234
+ const io = makeIo();
235
+ const client = makePassingClient();
236
+
237
+ const ralph = new RalphLoop({
238
+ client,
239
+ io,
240
+ session,
241
+ outputDir: tmpDir,
242
+ maxIterations: 1,
243
+ });
244
+
245
+ await expect(ralph.run()).rejects.toThrow(/selection/i);
246
+ });
247
+
248
+ it('throws when session has no plan', async () => {
249
+ const session = makeSession({ plan: undefined });
250
+ const io = makeIo();
251
+ const client = makePassingClient();
252
+
253
+ const ralph = new RalphLoop({
254
+ client,
255
+ io,
256
+ session,
257
+ outputDir: tmpDir,
258
+ maxIterations: 1,
259
+ });
260
+
261
+ await expect(ralph.run()).rejects.toThrow(/plan/i);
262
+ });
263
+ });
264
+
265
+ describe('lifecycle with passing tests', () => {
266
+ it('terminates with tests-passing when all tests pass immediately', async () => {
267
+ const session = makeSession();
268
+ const io = makeIo();
269
+ const client = makePassingClient();
270
+ const testRunner = makePassingTestRunner();
271
+ const scaffolder = makeFakeScaffolder(tmpDir);
272
+
273
+ const ralph = new RalphLoop({
274
+ client,
275
+ io,
276
+ session,
277
+ outputDir: tmpDir,
278
+ maxIterations: 5,
279
+ testRunner,
280
+ scaffolder,
281
+ });
282
+
283
+ const result = await ralph.run();
284
+
285
+ expect(result.finalStatus).toBe('success');
286
+ expect(result.terminationReason).toBe('tests-passing');
287
+ });
288
+
289
+ it('tracks iteration count', async () => {
290
+ const session = makeSession();
291
+ const io = makeIo();
292
+ const client = makePassingClient();
293
+ const testRunner = makePassingTestRunner();
294
+ const scaffolder = makeFakeScaffolder(tmpDir);
295
+
296
+ const ralph = new RalphLoop({
297
+ client,
298
+ io,
299
+ session,
300
+ outputDir: tmpDir,
301
+ maxIterations: 5,
302
+ testRunner,
303
+ scaffolder,
304
+ });
305
+
306
+ const result = await ralph.run();
307
+
308
+ // Iteration 1 (scaffold) + Iteration 2 (test run, passing)
309
+ expect(result.iterationsCompleted).toBeGreaterThanOrEqual(2);
310
+ });
311
+
312
+ it('calls onSessionUpdate after each iteration', async () => {
313
+ const session = makeSession();
314
+ const io = makeIo();
315
+ const client = makePassingClient();
316
+ const testRunner = makePassingTestRunner();
317
+ const scaffolder = makeFakeScaffolder(tmpDir);
318
+ const onSessionUpdate = vi.fn().mockResolvedValue(undefined);
319
+
320
+ const ralph = new RalphLoop({
321
+ client,
322
+ io,
323
+ session,
324
+ outputDir: tmpDir,
325
+ maxIterations: 3,
326
+ testRunner,
327
+ scaffolder,
328
+ onSessionUpdate,
329
+ });
330
+
331
+ await ralph.run();
332
+
333
+ // Should be called at least once (after scaffold, after passing tests)
334
+ expect(onSessionUpdate).toHaveBeenCalled();
335
+ });
336
+
337
+ it('returns updated session with poc state', async () => {
338
+ const session = makeSession();
339
+ const io = makeIo();
340
+ const client = makePassingClient();
341
+ const testRunner = makePassingTestRunner();
342
+ const scaffolder = makeFakeScaffolder(tmpDir);
343
+
344
+ const ralph = new RalphLoop({
345
+ client,
346
+ io,
347
+ session,
348
+ outputDir: tmpDir,
349
+ maxIterations: 5,
350
+ testRunner,
351
+ scaffolder,
352
+ });
353
+
354
+ const result = await ralph.run();
355
+
356
+ expect(result.session.poc).toBeDefined();
357
+ expect(result.session.poc!.iterations.length).toBeGreaterThan(0);
358
+ expect(result.session.poc!.iterations[0].outcome).toBe('scaffold');
359
+ expect(result.session.poc!.finalStatus).toBe('success');
360
+ });
361
+ });
362
+
363
+ describe('termination on max-iterations', () => {
364
+ it('terminates with max-iterations when all tests keep failing', async () => {
365
+ const session = makeSession();
366
+ const io = makeIo();
367
+ const client = makePassingClient();
368
+ const testRunner = makeAlwaysFailingTestRunner();
369
+ const scaffolder = makeFakeScaffolder(tmpDir);
370
+
371
+ const ralph = new RalphLoop({
372
+ client,
373
+ io,
374
+ session,
375
+ outputDir: tmpDir,
376
+ maxIterations: 3,
377
+ testRunner,
378
+ scaffolder,
379
+ });
380
+
381
+ const result = await ralph.run();
382
+
383
+ expect(result.terminationReason).toBe('max-iterations');
384
+ expect(result.iterationsCompleted).toBe(3); // 1 scaffold + 2 test iterations
385
+ });
386
+
387
+ it('sets finalStatus=partial when some tests pass at max-iterations', async () => {
388
+ const session = makeSession();
389
+ const io = makeIo();
390
+ const client = makePassingClient();
391
+
392
+ // Partially passing test runner
393
+ const testRunner: TestRunner = {
394
+ run: vi.fn().mockResolvedValue({
395
+ passed: 1,
396
+ failed: 1,
397
+ skipped: 0,
398
+ total: 2,
399
+ durationMs: 400,
400
+ failures: [{ testName: 'test B', message: 'fails' }],
401
+ rawOutput: '',
402
+ } satisfies TestResults),
403
+ } as unknown as TestRunner;
404
+
405
+ const scaffolder = makeFakeScaffolder(tmpDir);
406
+
407
+ const ralph = new RalphLoop({
408
+ client,
409
+ io,
410
+ session,
411
+ outputDir: tmpDir,
412
+ maxIterations: 2,
413
+ testRunner,
414
+ scaffolder,
415
+ });
416
+
417
+ const result = await ralph.run();
418
+
419
+ expect(result.terminationReason).toBe('max-iterations');
420
+ expect(result.finalStatus).toBe('partial');
421
+ });
422
+
423
+ it('sets finalStatus=failed when no tests pass at max-iterations', async () => {
424
+ const session = makeSession();
425
+ const io = makeIo();
426
+ const client = makePassingClient();
427
+ const testRunner = makeAlwaysFailingTestRunner();
428
+ const scaffolder = makeFakeScaffolder(tmpDir);
429
+
430
+ const ralph = new RalphLoop({
431
+ client,
432
+ io,
433
+ session,
434
+ outputDir: tmpDir,
435
+ maxIterations: 2,
436
+ testRunner,
437
+ scaffolder,
438
+ });
439
+
440
+ const result = await ralph.run();
441
+
442
+ expect(result.terminationReason).toBe('max-iterations');
443
+ expect(result.finalStatus).toBe('failed');
444
+ });
445
+
446
+ it('leaves session.poc.finalStatus unset when user stops (Ctrl+C)', async () => {
447
+ const session = makeSession();
448
+ const io = makeIo();
449
+ const testRunner = makeAlwaysFailingTestRunner();
450
+ const scaffolder = makeFakeScaffolder(tmpDir);
451
+
452
+ // Client that emits SIGINT mid-generation to simulate Ctrl+C
453
+ const client: CopilotClient = {
454
+ createSession: vi.fn().mockResolvedValue({
455
+ send: vi.fn().mockReturnValue({
456
+ async *[Symbol.asyncIterator]() {
457
+ process.emit('SIGINT');
458
+ yield {
459
+ type: 'TextDelta',
460
+ text: '```typescript file=src/index.ts\nexport function main() { return "ok"; }\n```',
461
+ timestamp: '',
462
+ };
463
+ },
464
+ }),
465
+ getHistory: () => [],
466
+ }),
467
+ };
468
+
469
+ const sessionUpdates: import('../../../src/shared/schemas/session.js').WorkshopSession[] = [];
470
+ const ralph = new RalphLoop({
471
+ client,
472
+ io,
473
+ session,
474
+ outputDir: tmpDir,
475
+ maxIterations: 5,
476
+ testRunner,
477
+ scaffolder,
478
+ onSessionUpdate: async (s) => {
479
+ sessionUpdates.push(s);
480
+ },
481
+ });
482
+
483
+ const result = await ralph.run();
484
+
485
+ expect(result.terminationReason).toBe('user-stopped');
486
+ // Contract: finalStatus must NOT be persisted to the session on user abort
487
+ expect(result.session.poc?.finalStatus).toBeUndefined();
488
+ // The persisted session updates should also not have finalStatus set
489
+ const lastUpdate = sessionUpdates[sessionUpdates.length - 1];
490
+ expect(lastUpdate?.poc?.finalStatus).toBeUndefined();
491
+ });
492
+ });
493
+
494
+ describe('event emission', () => {
495
+ it('emits events during loop lifecycle', async () => {
496
+ const session = makeSession();
497
+ const io = makeIo();
498
+ const client = makePassingClient();
499
+ const testRunner = makePassingTestRunner();
500
+ const scaffolder = makeFakeScaffolder(tmpDir);
501
+ const events: string[] = [];
502
+
503
+ const ralph = new RalphLoop({
504
+ client,
505
+ io,
506
+ session,
507
+ outputDir: tmpDir,
508
+ maxIterations: 3,
509
+ testRunner,
510
+ scaffolder,
511
+ onEvent: (e) => events.push(e.type),
512
+ });
513
+
514
+ await ralph.run();
515
+
516
+ expect(events).toContain('Activity');
517
+ });
518
+ });
519
+
520
+ describe('output directory', () => {
521
+ it('returns outputDir in result', async () => {
522
+ const session = makeSession();
523
+ const io = makeIo();
524
+ const client = makePassingClient();
525
+ const testRunner = makePassingTestRunner();
526
+ const scaffolder = makeFakeScaffolder(tmpDir);
527
+
528
+ const ralph = new RalphLoop({
529
+ client,
530
+ io,
531
+ session,
532
+ outputDir: tmpDir,
533
+ maxIterations: 3,
534
+ testRunner,
535
+ scaffolder,
536
+ });
537
+
538
+ const result = await ralph.run();
539
+ expect(result.outputDir).toBe(tmpDir);
540
+ });
541
+ });
542
+
543
+ describe('final test run after max iterations (F006)', () => {
544
+ it('runs a final test after the last LLM iteration and returns success when tests pass', async () => {
545
+ const session = makeSession();
546
+ const io = makeIo();
547
+ const client = makePassingClient();
548
+ const scaffolder = makeFakeScaffolder(tmpDir);
549
+
550
+ // First test run fails, second (final run after loop) passes
551
+ let runCount = 0;
552
+ const testRunner: TestRunner = {
553
+ run: vi.fn().mockImplementation(async (): Promise<TestResults> => {
554
+ runCount++;
555
+ if (runCount <= 1) {
556
+ return {
557
+ passed: 0,
558
+ failed: 1,
559
+ skipped: 0,
560
+ total: 1,
561
+ durationMs: 100,
562
+ failures: [{ testName: 'test A', message: 'fails' }],
563
+ rawOutput: 'FAIL',
564
+ };
565
+ }
566
+ return {
567
+ passed: 1,
568
+ failed: 0,
569
+ skipped: 0,
570
+ total: 1,
571
+ durationMs: 100,
572
+ failures: [],
573
+ rawOutput: 'PASS',
574
+ };
575
+ }),
576
+ } as unknown as TestRunner;
577
+
578
+ const ralph = new RalphLoop({
579
+ client,
580
+ io,
581
+ session,
582
+ outputDir: tmpDir,
583
+ maxIterations: 2, // scaffold + 1 iterate = 2 iterations, then final test
584
+ testRunner,
585
+ scaffolder,
586
+ });
587
+
588
+ const result = await ralph.run();
589
+
590
+ // The final test run detected the fix, so status should be success
591
+ expect(result.finalStatus).toBe('success');
592
+ expect(result.terminationReason).toBe('tests-passing');
593
+ });
594
+ });
595
+
596
+ describe('SIGINT handler stale session (F009)', () => {
597
+ it('persists latest session with iteration data when SIGINT fires after iterations', async () => {
598
+ const session = makeSession();
599
+ const io = makeIo();
600
+ const client = makePassingClient();
601
+ const scaffolder = makeFakeScaffolder(tmpDir);
602
+ let persistedSession: WorkshopSession | null = null;
603
+
604
+ // Slow test runner: yields after first call so SIGINT can fire
605
+ let runCount = 0;
606
+ const testRunner: TestRunner = {
607
+ run: vi.fn().mockImplementation(async (): Promise<TestResults> => {
608
+ runCount++;
609
+ // After first iteration completes, delay so SIGINT can fire
610
+ if (runCount >= 2) {
611
+ await new Promise((resolve) => setTimeout(resolve, 500));
612
+ }
613
+ return {
614
+ passed: 0,
615
+ failed: 1,
616
+ skipped: 0,
617
+ total: 1,
618
+ durationMs: 100,
619
+ failures: [{ testName: 'test A', message: 'fails' }],
620
+ rawOutput: 'FAIL',
621
+ };
622
+ }),
623
+ } as unknown as TestRunner;
624
+
625
+ const onSessionUpdate = vi.fn().mockImplementation(async (s: WorkshopSession) => {
626
+ persistedSession = s;
627
+ });
628
+
629
+ const ralph = new RalphLoop({
630
+ client,
631
+ io,
632
+ session,
633
+ outputDir: tmpDir,
634
+ maxIterations: 10,
635
+ testRunner,
636
+ scaffolder,
637
+ onSessionUpdate,
638
+ });
639
+
640
+ // Start the loop, then fire SIGINT after enough time for first iteration
641
+ const runPromise = ralph.run();
642
+
643
+ // Wait for at least scaffold + first test run iteration
644
+ await new Promise((resolve) => setTimeout(resolve, 300));
645
+ process.emit('SIGINT', 'SIGINT');
646
+
647
+ const result = await runPromise;
648
+
649
+ expect(result.terminationReason).toBe('user-stopped');
650
+ // The persisted session should have iteration data from completed iterations
651
+ expect(persistedSession).not.toBeNull();
652
+ expect(persistedSession!.poc).toBeDefined();
653
+ expect(persistedSession!.poc!.iterations.length).toBeGreaterThanOrEqual(1);
654
+ });
655
+ });
656
+
657
+ describe('user-stopped status (F011)', () => {
658
+ it('returns finalStatus=partial when user stops and some tests were passing', async () => {
659
+ const session = makeSession();
660
+ const io = makeIo();
661
+ const client = makePassingClient();
662
+ const scaffolder = makeFakeScaffolder(tmpDir);
663
+
664
+ // Partially passing test runner that delays so SIGINT can fire
665
+ let runCount = 0;
666
+ const testRunner: TestRunner = {
667
+ run: vi.fn().mockImplementation(async (): Promise<TestResults> => {
668
+ runCount++;
669
+ if (runCount >= 2) {
670
+ await new Promise((resolve) => setTimeout(resolve, 500));
671
+ }
672
+ return {
673
+ passed: 1,
674
+ failed: 1,
675
+ skipped: 0,
676
+ total: 2,
677
+ durationMs: 100,
678
+ failures: [{ testName: 'test B', message: 'fails' }],
679
+ rawOutput: 'PARTIAL',
680
+ };
681
+ }),
682
+ } as unknown as TestRunner;
683
+
684
+ const ralph = new RalphLoop({
685
+ client,
686
+ io,
687
+ session,
688
+ outputDir: tmpDir,
689
+ maxIterations: 10,
690
+ testRunner,
691
+ scaffolder,
692
+ });
693
+
694
+ const runPromise = ralph.run();
695
+
696
+ await new Promise((resolve) => setTimeout(resolve, 300));
697
+ process.emit('SIGINT', 'SIGINT');
698
+
699
+ const result = await runPromise;
700
+
701
+ expect(result.terminationReason).toBe('user-stopped');
702
+ expect(result.finalStatus).toBe('partial');
703
+ });
704
+ });
705
+
706
+ // SKIPPED: Auto-push to GitHub removed per user safety requirements
707
+ // sofIA now initializes git locally only — users push manually
708
+ describe.skip('GitHub MCP adapter integration', () => {
709
+ it('reads written files from disk and passes their content to pushFiles', async () => {
710
+ const session = makeSession();
711
+ const io = makeIo();
712
+
713
+ // LLM returns a file with known content
714
+ const knownContent = 'export function main() { return "hello"; }\n';
715
+ const client: CopilotClient = {
716
+ createSession: vi.fn().mockResolvedValue({
717
+ send: vi.fn().mockReturnValue({
718
+ async *[Symbol.asyncIterator]() {
719
+ yield {
720
+ type: 'TextDelta',
721
+ text: `\`\`\`typescript file=src/index.ts\n${knownContent}\`\`\``,
722
+ timestamp: '',
723
+ };
724
+ },
725
+ }),
726
+ getHistory: () => [],
727
+ }),
728
+ };
729
+
730
+ // Fail on first run so the LLM turn (and pushFiles) is reached; pass on second
731
+ let runCount = 0;
732
+ const testRunner: TestRunner = {
733
+ run: vi.fn().mockImplementation(async (): Promise<TestResults> => {
734
+ runCount++;
735
+ if (runCount === 1) {
736
+ return {
737
+ passed: 0,
738
+ failed: 1,
739
+ skipped: 0,
740
+ total: 1,
741
+ durationMs: 100,
742
+ failures: [{ testName: 'main > works', message: 'not implemented' }],
743
+ rawOutput: 'FAIL',
744
+ };
745
+ }
746
+ return {
747
+ passed: 1,
748
+ failed: 0,
749
+ skipped: 0,
750
+ total: 1,
751
+ durationMs: 100,
752
+ failures: [],
753
+ rawOutput: 'OK',
754
+ };
755
+ }),
756
+ } as unknown as TestRunner;
757
+
758
+ const scaffolder = makeFakeScaffolder(tmpDir);
759
+
760
+ // Create a mock GitHub adapter that captures pushFiles calls
761
+ const pushFilesMock = vi.fn().mockResolvedValue({ available: true, commitSha: 'abc123' });
762
+ const _githubAdapter = {
763
+ isAvailable: () => true,
764
+ getRepoUrl: () => 'https://github.com/acme/poc-test',
765
+ pushFiles: pushFilesMock,
766
+ createRepository: vi.fn().mockResolvedValue({
767
+ available: true,
768
+ repoUrl: 'https://github.com/acme/poc-test',
769
+ repoName: 'poc-test',
770
+ }),
771
+ } as unknown as GitHubMcpAdapter;
772
+
773
+ const ralph = new RalphLoop({
774
+ client,
775
+ io,
776
+ session,
777
+ outputDir: tmpDir,
778
+ maxIterations: 5,
779
+ testRunner,
780
+ scaffolder,
781
+ });
782
+
783
+ await ralph.run();
784
+
785
+ // pushFiles should have been called at least twice:
786
+ // call[0] = scaffold push, call[1+] = iteration pushes
787
+ expect(pushFilesMock.mock.calls.length).toBeGreaterThanOrEqual(2);
788
+ // The iteration push (call[1]) should contain the LLM-written file content
789
+ const callArgs = pushFilesMock.mock.calls[1][0] as {
790
+ files: Array<{ path: string; content: string }>;
791
+ };
792
+ const pushedFile = callArgs.files.find((f) => f.path === 'src/index.ts');
793
+ expect(pushedFile).toBeDefined();
794
+ expect(pushedFile!.content).toBe(knownContent);
795
+ expect(pushedFile!.content).not.toBe('');
796
+ });
797
+ });
798
+
799
+ describe('validatePocOutput integration (F027)', () => {
800
+ it('downgrades success to partial when validatePocOutput reports missing files', async () => {
801
+ // Mock validatePocOutput to fail
802
+ vi.mocked(validatePocOutput).mockResolvedValueOnce({
803
+ valid: false,
804
+ missingFiles: ['README.md'],
805
+ errors: [],
806
+ });
807
+
808
+ const session = makeSession();
809
+ const io = makeIo();
810
+ const testRunner = {
811
+ run: vi.fn().mockResolvedValue({
812
+ passed: 3,
813
+ failed: 0,
814
+ skipped: 0,
815
+ total: 3,
816
+ durationMs: 100,
817
+ failures: [],
818
+ rawOutput: 'ALL PASS',
819
+ }),
820
+ } as unknown as TestRunner;
821
+
822
+ const ralph = new RalphLoop({
823
+ client: makePassingClient(),
824
+ io,
825
+ session,
826
+ outputDir: tmpDir,
827
+ maxIterations: 5,
828
+ testRunner,
829
+ scaffolder: makeFakeScaffolder(tmpDir),
830
+ });
831
+
832
+ const result = await ralph.run();
833
+
834
+ expect(result.finalStatus).toBe('partial');
835
+ expect(result.terminationReason).toBe('tests-passing');
836
+ // validatePocOutput should have been called
837
+ expect(validatePocOutput).toHaveBeenCalledWith(tmpDir);
838
+ });
839
+
840
+ it('keeps success when validatePocOutput reports valid', async () => {
841
+ vi.mocked(validatePocOutput).mockResolvedValueOnce({
842
+ valid: true,
843
+ missingFiles: [],
844
+ errors: [],
845
+ });
846
+
847
+ const session = makeSession();
848
+ const io = makeIo();
849
+ const testRunner = {
850
+ run: vi.fn().mockResolvedValue({
851
+ passed: 3,
852
+ failed: 0,
853
+ skipped: 0,
854
+ total: 3,
855
+ durationMs: 100,
856
+ failures: [],
857
+ rawOutput: 'ALL PASS',
858
+ }),
859
+ } as unknown as TestRunner;
860
+
861
+ const ralph = new RalphLoop({
862
+ client: makePassingClient(),
863
+ io,
864
+ session,
865
+ outputDir: tmpDir,
866
+ maxIterations: 5,
867
+ testRunner,
868
+ scaffolder: makeFakeScaffolder(tmpDir),
869
+ });
870
+
871
+ const result = await ralph.run();
872
+
873
+ expect(result.finalStatus).toBe('success');
874
+ expect(validatePocOutput).toHaveBeenCalledWith(tmpDir);
875
+ });
876
+ });
877
+
878
+ // ── Resume iteration seeding (T013) ─────────────────────────────────────
879
+
880
+ describe('resume iteration seeding', () => {
881
+ it('seeds iterations from session.poc.iterations and starts from correct iterNum', async () => {
882
+ const io = makeIo();
883
+ const testRunner = makePassingTestRunner();
884
+ const session = makeSession({
885
+ poc: {
886
+ repoSource: 'local',
887
+ iterations: [
888
+ {
889
+ iteration: 1,
890
+ startedAt: new Date().toISOString(),
891
+ endedAt: new Date().toISOString(),
892
+ outcome: 'scaffold',
893
+ filesChanged: ['package.json'],
894
+ },
895
+ {
896
+ iteration: 2,
897
+ startedAt: new Date().toISOString(),
898
+ endedAt: new Date().toISOString(),
899
+ outcome: 'tests-failing',
900
+ filesChanged: ['src/index.ts'],
901
+ testResults: {
902
+ passed: 1,
903
+ failed: 1,
904
+ skipped: 0,
905
+ total: 2,
906
+ durationMs: 100,
907
+ failures: [{ testName: 'test1', message: 'fail' }],
908
+ },
909
+ },
910
+ ],
911
+ },
912
+ });
913
+
914
+ const ralph = new RalphLoop({
915
+ client: makePassingClient(),
916
+ io,
917
+ session,
918
+ outputDir: tmpDir,
919
+ maxIterations: 10,
920
+ testRunner,
921
+ scaffolder: makeFakeScaffolder(tmpDir),
922
+ checkpoint: {
923
+ hasPriorRun: true,
924
+ completedIterations: 2,
925
+ lastIterationIncomplete: false,
926
+ resumeFromIteration: 3,
927
+ canSkipScaffold: false,
928
+ priorFinalStatus: undefined,
929
+ priorIterations: session.poc!.iterations,
930
+ },
931
+ });
932
+
933
+ const result = await ralph.run();
934
+
935
+ // Iterations should include the 2 prior ones + scaffold (no skip) + tests-passing
936
+ expect(result.iterationsCompleted).toBeGreaterThanOrEqual(3);
937
+ expect(result.finalStatus).toBe('success');
938
+ });
939
+
940
+ it('skips scaffold when checkpoint says canSkipScaffold=true (T014)', async () => {
941
+ const io = makeIo();
942
+ const testRunner = makePassingTestRunner();
943
+ const scaffolder = makeFakeScaffolder(tmpDir);
944
+
945
+ const session = makeSession({
946
+ poc: {
947
+ repoSource: 'local',
948
+ iterations: [
949
+ {
950
+ iteration: 1,
951
+ startedAt: new Date().toISOString(),
952
+ endedAt: new Date().toISOString(),
953
+ outcome: 'scaffold',
954
+ filesChanged: [],
955
+ },
956
+ ],
957
+ },
958
+ });
959
+
960
+ const ralph = new RalphLoop({
961
+ client: makePassingClient(),
962
+ io,
963
+ session,
964
+ outputDir: tmpDir,
965
+ maxIterations: 10,
966
+ testRunner,
967
+ scaffolder,
968
+ checkpoint: {
969
+ hasPriorRun: true,
970
+ completedIterations: 1,
971
+ lastIterationIncomplete: false,
972
+ resumeFromIteration: 2,
973
+ canSkipScaffold: true,
974
+ priorFinalStatus: undefined,
975
+ priorIterations: session.poc!.iterations,
976
+ },
977
+ });
978
+
979
+ await ralph.run();
980
+
981
+ // Scaffold should NOT have been called — it was skipped
982
+ expect(scaffolder.scaffold).not.toHaveBeenCalled();
983
+ // Should log that scaffold was skipped
984
+ expect(io.writeActivity).toHaveBeenCalledWith(
985
+ expect.stringContaining('Skipping scaffold'),
986
+ );
987
+ });
988
+
989
+ it('pops incomplete last iteration and re-runs it (T015, FR-001a)', async () => {
990
+ const io = makeIo();
991
+ const testRunner = makePassingTestRunner();
992
+
993
+ const incompleteIter = {
994
+ iteration: 2,
995
+ startedAt: new Date().toISOString(),
996
+ outcome: 'tests-failing' as const,
997
+ filesChanged: [],
998
+ // No testResults — incomplete
999
+ };
1000
+
1001
+ const session = makeSession({
1002
+ poc: {
1003
+ repoSource: 'local',
1004
+ iterations: [
1005
+ {
1006
+ iteration: 1,
1007
+ startedAt: new Date().toISOString(),
1008
+ endedAt: new Date().toISOString(),
1009
+ outcome: 'scaffold',
1010
+ filesChanged: [],
1011
+ },
1012
+ incompleteIter,
1013
+ ],
1014
+ },
1015
+ });
1016
+
1017
+ const ralph = new RalphLoop({
1018
+ client: makePassingClient(),
1019
+ io,
1020
+ session,
1021
+ outputDir: tmpDir,
1022
+ maxIterations: 10,
1023
+ testRunner,
1024
+ scaffolder: makeFakeScaffolder(tmpDir),
1025
+ checkpoint: {
1026
+ hasPriorRun: true,
1027
+ completedIterations: 1,
1028
+ lastIterationIncomplete: true,
1029
+ resumeFromIteration: 2,
1030
+ canSkipScaffold: false,
1031
+ priorFinalStatus: undefined,
1032
+ priorIterations: [session.poc!.iterations[0]], // Only completed iters
1033
+ },
1034
+ });
1035
+
1036
+ const result = await ralph.run();
1037
+
1038
+ // Should log about re-running incomplete iteration
1039
+ expect(io.writeActivity).toHaveBeenCalledWith(
1040
+ expect.stringContaining('Re-running incomplete iteration'),
1041
+ );
1042
+ expect(result.finalStatus).toBe('success');
1043
+ });
1044
+
1045
+ it('re-scaffolds when output directory is missing but iterations exist (T018, FR-007)', async () => {
1046
+ const io = makeIo();
1047
+ const testRunner = makePassingTestRunner();
1048
+ const scaffolder = makeFakeScaffolder(tmpDir);
1049
+
1050
+ const session = makeSession({
1051
+ poc: {
1052
+ repoSource: 'local',
1053
+ iterations: [
1054
+ {
1055
+ iteration: 1,
1056
+ startedAt: new Date().toISOString(),
1057
+ endedAt: new Date().toISOString(),
1058
+ outcome: 'scaffold',
1059
+ filesChanged: [],
1060
+ },
1061
+ ],
1062
+ },
1063
+ });
1064
+
1065
+ const ralph = new RalphLoop({
1066
+ client: makePassingClient(),
1067
+ io,
1068
+ session,
1069
+ outputDir: tmpDir,
1070
+ maxIterations: 10,
1071
+ testRunner,
1072
+ scaffolder,
1073
+ checkpoint: {
1074
+ hasPriorRun: true,
1075
+ completedIterations: 1,
1076
+ lastIterationIncomplete: false,
1077
+ resumeFromIteration: 2,
1078
+ canSkipScaffold: false, // Output dir missing
1079
+ priorFinalStatus: undefined,
1080
+ priorIterations: session.poc!.iterations,
1081
+ },
1082
+ });
1083
+
1084
+ await ralph.run();
1085
+
1086
+ // Scaffold SHOULD have been called since canSkipScaffold is false
1087
+ expect(scaffolder.scaffold).toHaveBeenCalled();
1088
+ expect(io.writeActivity).toHaveBeenCalledWith(
1089
+ expect.stringContaining('re-scaffolding'),
1090
+ );
1091
+ });
1092
+ });
1093
+
1094
+ // SKIPPED: Auto-push to GitHub removed per user safety requirements
1095
+ // sofIA now initializes git locally only — users push manually
1096
+ describe.skip('post-scaffold push (T024 — US2)', () => {
1097
+ it('pushes scaffold files to GitHub after npm install, before first test iteration', async () => {
1098
+ const session = makeSession();
1099
+ const io = makeIo();
1100
+ const client = makePassingClient();
1101
+ const testRunner = makePassingTestRunner();
1102
+ const scaffolder = makeFakeScaffolder(tmpDir);
1103
+
1104
+ const pushOrder: string[] = [];
1105
+ const pushFilesMock = vi.fn().mockImplementation(async () => {
1106
+ pushOrder.push('pushFiles');
1107
+ return { available: true, commitSha: 'scaffold-sha' };
1108
+ });
1109
+ const _githubAdapter = {
1110
+ isAvailable: () => true,
1111
+ getRepoUrl: () => 'https://github.com/acme/poc-test',
1112
+ pushFiles: pushFilesMock,
1113
+ createRepository: vi.fn().mockResolvedValue({
1114
+ available: true,
1115
+ repoUrl: 'https://github.com/acme/poc-test',
1116
+ repoName: 'poc-test',
1117
+ }),
1118
+ } as unknown as GitHubMcpAdapter;
1119
+
1120
+ // Wrap testRunner.run to record ordering
1121
+ const originalRun = testRunner.run;
1122
+ (testRunner as { run: typeof testRunner.run }).run = vi
1123
+ .fn()
1124
+ .mockImplementation(async (...args: Parameters<typeof testRunner.run>) => {
1125
+ pushOrder.push('testRun');
1126
+ return originalRun(...args);
1127
+ });
1128
+
1129
+ const ralph = new RalphLoop({
1130
+ client,
1131
+ io,
1132
+ session,
1133
+ outputDir: tmpDir,
1134
+ maxIterations: 3,
1135
+ testRunner,
1136
+ scaffolder,
1137
+ });
1138
+
1139
+ await ralph.run();
1140
+
1141
+ // pushFiles should have been called at least once for the scaffold
1142
+ expect(pushFilesMock).toHaveBeenCalled();
1143
+ const firstPushArgs = pushFilesMock.mock.calls[0][0] as {
1144
+ repoUrl: string;
1145
+ files: Array<{ path: string; content: string }>;
1146
+ commitMessage: string;
1147
+ };
1148
+
1149
+ // Should push the scaffold files ('package.json', 'src/index.ts')
1150
+ expect(firstPushArgs.files.length).toBeGreaterThanOrEqual(1);
1151
+ const packageJson = firstPushArgs.files.find((f) => f.path === 'package.json');
1152
+ expect(packageJson).toBeDefined();
1153
+ expect(packageJson!.content).not.toBe('');
1154
+
1155
+ const indexTs = firstPushArgs.files.find((f) => f.path === 'src/index.ts');
1156
+ expect(indexTs).toBeDefined();
1157
+ expect(indexTs!.content).not.toBe('');
1158
+
1159
+ // Commit message should indicate scaffold
1160
+ expect(firstPushArgs.commitMessage).toContain('scaffold');
1161
+
1162
+ // The scaffold push should come BEFORE the first test run
1163
+ const firstPush = pushOrder.indexOf('pushFiles');
1164
+ const firstTest = pushOrder.indexOf('testRun');
1165
+ expect(firstPush).toBeLessThan(firstTest);
1166
+ });
1167
+
1168
+ it('always re-runs dependency install even when scaffolding is skipped (T065, FR-003)', async () => {
1169
+ const io = makeIo();
1170
+ const testRunner = makePassingTestRunner();
1171
+ const scaffolder = makeFakeScaffolder(tmpDir);
1172
+
1173
+ const session = makeSession({
1174
+ poc: {
1175
+ repoSource: 'local',
1176
+ iterations: [
1177
+ {
1178
+ iteration: 1,
1179
+ startedAt: new Date().toISOString(),
1180
+ endedAt: new Date().toISOString(),
1181
+ outcome: 'scaffold',
1182
+ filesChanged: [],
1183
+ },
1184
+ ],
1185
+ },
1186
+ });
1187
+
1188
+ const ralph = new RalphLoop({
1189
+ client: makePassingClient(),
1190
+ io,
1191
+ session,
1192
+ outputDir: tmpDir,
1193
+ maxIterations: 10,
1194
+ testRunner,
1195
+ scaffolder,
1196
+ checkpoint: {
1197
+ hasPriorRun: true,
1198
+ completedIterations: 1,
1199
+ lastIterationIncomplete: false,
1200
+ resumeFromIteration: 2,
1201
+ canSkipScaffold: true,
1202
+ priorFinalStatus: undefined,
1203
+ priorIterations: session.poc!.iterations,
1204
+ },
1205
+ });
1206
+
1207
+ await ralph.run();
1208
+
1209
+ // Scaffold should be skipped
1210
+ expect(scaffolder.scaffold).not.toHaveBeenCalled();
1211
+ // But install should still run
1212
+ expect(io.writeActivity).toHaveBeenCalledWith(
1213
+ expect.stringContaining('Re-running dependency installation'),
1214
+ );
1215
+ });
1216
+
1217
+ it('includes prior iteration history in LLM prompt context (T066, FR-004)', async () => {
1218
+ const io = makeIo();
1219
+ const testRunner = makeAlwaysFailingTestRunner();
1220
+
1221
+ const priorIters = [
1222
+ {
1223
+ iteration: 1,
1224
+ startedAt: new Date().toISOString(),
1225
+ endedAt: new Date().toISOString(),
1226
+ outcome: 'scaffold' as const,
1227
+ filesChanged: ['package.json'],
1228
+ },
1229
+ {
1230
+ iteration: 2,
1231
+ startedAt: new Date().toISOString(),
1232
+ endedAt: new Date().toISOString(),
1233
+ outcome: 'tests-failing' as const,
1234
+ filesChanged: ['src/index.ts'],
1235
+ testResults: {
1236
+ passed: 1,
1237
+ failed: 1,
1238
+ skipped: 0,
1239
+ total: 2,
1240
+ durationMs: 100,
1241
+ failures: [{ testName: 'test1', message: 'fail' }],
1242
+ },
1243
+ },
1244
+ ];
1245
+
1246
+ const session = makeSession({
1247
+ poc: {
1248
+ repoSource: 'local',
1249
+ iterations: priorIters,
1250
+ },
1251
+ });
1252
+
1253
+ // Track LLM prompts
1254
+ const capturedPrompts: string[] = [];
1255
+ const client: CopilotClient = {
1256
+ createSession: vi.fn().mockResolvedValue({
1257
+ send: vi.fn().mockImplementation((msg: { content: string }) => {
1258
+ capturedPrompts.push(msg.content);
1259
+ return {
1260
+ async *[Symbol.asyncIterator]() {
1261
+ yield {
1262
+ type: 'TextDelta',
1263
+ text: '```typescript file=src/index.ts\nexport const x = 1;\n```',
1264
+ timestamp: '',
1265
+ };
1266
+ },
1267
+ };
1268
+ }),
1269
+ getHistory: () => [],
1270
+ }),
1271
+ };
1272
+
1273
+ const ralph = new RalphLoop({
1274
+ client,
1275
+ io,
1276
+ session,
1277
+ outputDir: tmpDir,
1278
+ maxIterations: 4,
1279
+ testRunner,
1280
+ scaffolder: makeFakeScaffolder(tmpDir),
1281
+ checkpoint: {
1282
+ hasPriorRun: true,
1283
+ completedIterations: 2,
1284
+ lastIterationIncomplete: false,
1285
+ resumeFromIteration: 3,
1286
+ canSkipScaffold: false,
1287
+ priorFinalStatus: undefined,
1288
+ priorIterations: priorIters,
1289
+ },
1290
+ });
1291
+
1292
+ await ralph.run();
1293
+
1294
+ // The LLM should have received prior iteration history
1295
+ // The context enrichment path merges priorHistoryContext into mcpContext
1296
+ // This is hard to test directly without peeking at internals, but we can verify
1297
+ // that the prior iterations were seeded properly
1298
+ expect(io.writeActivity).toHaveBeenCalledWith(
1299
+ expect.stringContaining('Re-running dependency installation'),
1300
+ );
1301
+ });
1302
+
1303
+ it('resume decision logging emits info-level messages (T067, FR-007a)', async () => {
1304
+ const io = makeIo();
1305
+ const testRunner = makePassingTestRunner();
1306
+ const scaffolder = makeFakeScaffolder(tmpDir);
1307
+
1308
+ const session = makeSession({
1309
+ poc: {
1310
+ repoSource: 'local',
1311
+ iterations: [
1312
+ {
1313
+ iteration: 1,
1314
+ startedAt: new Date().toISOString(),
1315
+ endedAt: new Date().toISOString(),
1316
+ outcome: 'scaffold',
1317
+ filesChanged: [],
1318
+ },
1319
+ ],
1320
+ },
1321
+ });
1322
+
1323
+ const ralph = new RalphLoop({
1324
+ client: makePassingClient(),
1325
+ io,
1326
+ session,
1327
+ outputDir: tmpDir,
1328
+ maxIterations: 10,
1329
+ testRunner,
1330
+ scaffolder,
1331
+ checkpoint: {
1332
+ hasPriorRun: true,
1333
+ completedIterations: 1,
1334
+ lastIterationIncomplete: false,
1335
+ resumeFromIteration: 2,
1336
+ canSkipScaffold: true,
1337
+ priorFinalStatus: undefined,
1338
+ priorIterations: session.poc!.iterations,
1339
+ },
1340
+ });
1341
+
1342
+ await ralph.run();
1343
+
1344
+ // Should have logged skip scaffold, re-run install messages
1345
+ const calls = (io.writeActivity as ReturnType<typeof vi.fn>).mock.calls.flat();
1346
+ expect(calls.some((c: string) => c.includes('Skipping scaffold'))).toBe(true);
1347
+ expect(calls.some((c: string) => c.includes('Re-running dependency installation'))).toBe(
1348
+ true,
1349
+ );
1350
+ });
1351
+ });
1352
+
1353
+ // ── T073: TODO marker rescan after iteration updates .sofia-metadata.json ──
1354
+
1355
+ describe('TODO marker rescan after iteration (T073)', () => {
1356
+ it('calls scanAndRecordTodos after each failing iteration', async () => {
1357
+ const scanSpy = vi
1358
+ .spyOn(PocScaffolder, 'scanAndRecordTodos')
1359
+ .mockResolvedValue({ totalInitial: 3, remaining: 2, markers: [] });
1360
+
1361
+ const session = makeSession();
1362
+ const io: LoopIO = {
1363
+ write: vi.fn(),
1364
+ writeActivity: vi.fn(),
1365
+ writeToolSummary: vi.fn(),
1366
+ readInput: vi.fn().mockResolvedValue(null),
1367
+ showDecisionGate: vi.fn(),
1368
+ isJsonMode: false,
1369
+ isTTY: false,
1370
+ };
1371
+ const testRunner = makeAlwaysFailingTestRunner();
1372
+ const client = makePassingClient();
1373
+ const scaffolder = makeFakeScaffolder(tmpDir);
1374
+
1375
+ const ralph = new RalphLoop({
1376
+ client,
1377
+ io,
1378
+ session,
1379
+ outputDir: tmpDir,
1380
+ maxIterations: 2,
1381
+ testRunner,
1382
+ scaffolder,
1383
+ });
1384
+
1385
+ await ralph.run();
1386
+
1387
+ // scanAndRecordTodos should have been called for each failing iteration
1388
+ expect(scanSpy).toHaveBeenCalled();
1389
+ expect(scanSpy).toHaveBeenCalledWith(tmpDir);
1390
+
1391
+ scanSpy.mockRestore();
1392
+ });
1393
+ });
1394
+
1395
+ // ── T054: infiniteSessions config forwarding ──────────────────────────────
1396
+
1397
+ describe('infiniteSessions config (T054)', () => {
1398
+ it('passes infiniteSessions config to createSession for context management', async () => {
1399
+ const createSessionSpy = vi.fn().mockResolvedValue({
1400
+ send: vi.fn().mockReturnValue({
1401
+ async *[Symbol.asyncIterator]() {
1402
+ yield {
1403
+ type: 'TextDelta',
1404
+ text: '```typescript file=src/index.ts\nexport function main() { return "ok"; }\n```',
1405
+ timestamp: '',
1406
+ };
1407
+ },
1408
+ }),
1409
+ getHistory: () => [],
1410
+ });
1411
+ const client: CopilotClient = { createSession: createSessionSpy };
1412
+ const io = makeIo();
1413
+ const session = makeSession();
1414
+ const testRunner = makeAlwaysFailingTestRunner();
1415
+ const scaffolder = makeFakeScaffolder(tmpDir);
1416
+
1417
+ const ralph = new RalphLoop({
1418
+ client,
1419
+ io,
1420
+ session,
1421
+ outputDir: tmpDir,
1422
+ maxIterations: 2,
1423
+ testRunner,
1424
+ scaffolder,
1425
+ });
1426
+
1427
+ await ralph.run();
1428
+
1429
+ expect(createSessionSpy).toHaveBeenCalledWith(
1430
+ expect.objectContaining({
1431
+ infiniteSessions: {
1432
+ backgroundCompactionThreshold: 0.7,
1433
+ bufferExhaustionThreshold: 0.9,
1434
+ },
1435
+ }),
1436
+ );
1437
+ });
1438
+ });
1439
+ });