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