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,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for deriveCheckpointState.
|
|
3
|
+
*
|
|
4
|
+
* T012: Verify correct state for no-poc, completed, partial, interrupted sessions
|
|
5
|
+
* T068: Corrupted iterations cause safe fallback
|
|
6
|
+
* T069: Metadata integrity mismatch triggers warning
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { deriveCheckpointState } from '../../../src/develop/checkpointState.js';
|
|
13
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
14
|
+
function makeSession(overrides) {
|
|
15
|
+
const now = new Date().toISOString();
|
|
16
|
+
return {
|
|
17
|
+
sessionId: 'cp-test-session',
|
|
18
|
+
schemaVersion: '1.0.0',
|
|
19
|
+
createdAt: now,
|
|
20
|
+
updatedAt: now,
|
|
21
|
+
phase: 'Develop',
|
|
22
|
+
status: 'Active',
|
|
23
|
+
participants: [],
|
|
24
|
+
artifacts: { generatedFiles: [] },
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
29
|
+
describe('deriveCheckpointState', () => {
|
|
30
|
+
let tmpDir;
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'checkpoint-'));
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
it('returns fresh state when session has no poc', () => {
|
|
38
|
+
const session = makeSession();
|
|
39
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
40
|
+
expect(state.hasPriorRun).toBe(false);
|
|
41
|
+
expect(state.completedIterations).toBe(0);
|
|
42
|
+
expect(state.lastIterationIncomplete).toBe(false);
|
|
43
|
+
expect(state.resumeFromIteration).toBe(1);
|
|
44
|
+
expect(state.canSkipScaffold).toBe(false);
|
|
45
|
+
expect(state.priorFinalStatus).toBeUndefined();
|
|
46
|
+
expect(state.priorIterations).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
it('returns fresh state when poc has empty iterations', () => {
|
|
49
|
+
const session = makeSession({
|
|
50
|
+
poc: { repoSource: 'local', iterations: [] },
|
|
51
|
+
});
|
|
52
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
53
|
+
expect(state.hasPriorRun).toBe(false);
|
|
54
|
+
expect(state.resumeFromIteration).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
it('returns completed state for sessions with all iterations having testResults', () => {
|
|
57
|
+
const session = makeSession({
|
|
58
|
+
poc: {
|
|
59
|
+
repoSource: 'local',
|
|
60
|
+
iterations: [
|
|
61
|
+
{
|
|
62
|
+
iteration: 1,
|
|
63
|
+
startedAt: new Date().toISOString(),
|
|
64
|
+
endedAt: new Date().toISOString(),
|
|
65
|
+
outcome: 'scaffold',
|
|
66
|
+
filesChanged: [],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
iteration: 2,
|
|
70
|
+
startedAt: new Date().toISOString(),
|
|
71
|
+
endedAt: new Date().toISOString(),
|
|
72
|
+
outcome: 'tests-failing',
|
|
73
|
+
filesChanged: ['src/index.ts'],
|
|
74
|
+
testResults: {
|
|
75
|
+
passed: 1,
|
|
76
|
+
failed: 1,
|
|
77
|
+
skipped: 0,
|
|
78
|
+
total: 2,
|
|
79
|
+
durationMs: 100,
|
|
80
|
+
failures: [],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
finalStatus: 'partial',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
88
|
+
expect(state.hasPriorRun).toBe(true);
|
|
89
|
+
expect(state.completedIterations).toBe(2);
|
|
90
|
+
expect(state.lastIterationIncomplete).toBe(false);
|
|
91
|
+
expect(state.resumeFromIteration).toBe(3);
|
|
92
|
+
expect(state.priorFinalStatus).toBe('partial');
|
|
93
|
+
expect(state.priorIterations).toHaveLength(2);
|
|
94
|
+
});
|
|
95
|
+
it('detects incomplete last iteration (no testResults, not scaffold)', () => {
|
|
96
|
+
const session = makeSession({
|
|
97
|
+
poc: {
|
|
98
|
+
repoSource: 'local',
|
|
99
|
+
iterations: [
|
|
100
|
+
{
|
|
101
|
+
iteration: 1,
|
|
102
|
+
startedAt: new Date().toISOString(),
|
|
103
|
+
endedAt: new Date().toISOString(),
|
|
104
|
+
outcome: 'scaffold',
|
|
105
|
+
filesChanged: [],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
iteration: 2,
|
|
109
|
+
startedAt: new Date().toISOString(),
|
|
110
|
+
outcome: 'tests-failing',
|
|
111
|
+
filesChanged: [],
|
|
112
|
+
// No testResults — interrupted
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
118
|
+
expect(state.hasPriorRun).toBe(true);
|
|
119
|
+
expect(state.lastIterationIncomplete).toBe(true);
|
|
120
|
+
expect(state.completedIterations).toBe(1);
|
|
121
|
+
expect(state.resumeFromIteration).toBe(2);
|
|
122
|
+
expect(state.priorIterations).toHaveLength(1);
|
|
123
|
+
});
|
|
124
|
+
it('sets canSkipScaffold when metadata file exists with matching sessionId', () => {
|
|
125
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
126
|
+
writeFileSync(join(tmpDir, '.sofia-metadata.json'), JSON.stringify({ sessionId: 'cp-test-session' }));
|
|
127
|
+
const session = makeSession({
|
|
128
|
+
poc: {
|
|
129
|
+
repoSource: 'local',
|
|
130
|
+
iterations: [
|
|
131
|
+
{
|
|
132
|
+
iteration: 1,
|
|
133
|
+
startedAt: new Date().toISOString(),
|
|
134
|
+
endedAt: new Date().toISOString(),
|
|
135
|
+
outcome: 'scaffold',
|
|
136
|
+
filesChanged: [],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
142
|
+
expect(state.canSkipScaffold).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
it('sets canSkipScaffold false when metadata file has mismatched sessionId (T069)', () => {
|
|
145
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
146
|
+
writeFileSync(join(tmpDir, '.sofia-metadata.json'), JSON.stringify({ sessionId: 'different-session' }));
|
|
147
|
+
const session = makeSession({
|
|
148
|
+
poc: {
|
|
149
|
+
repoSource: 'local',
|
|
150
|
+
iterations: [
|
|
151
|
+
{
|
|
152
|
+
iteration: 1,
|
|
153
|
+
startedAt: new Date().toISOString(),
|
|
154
|
+
endedAt: new Date().toISOString(),
|
|
155
|
+
outcome: 'scaffold',
|
|
156
|
+
filesChanged: [],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
162
|
+
expect(state.canSkipScaffold).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
it('sets canSkipScaffold false when metadata file is corrupt JSON (T069)', () => {
|
|
165
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
166
|
+
writeFileSync(join(tmpDir, '.sofia-metadata.json'), '{invalid json');
|
|
167
|
+
const session = makeSession({
|
|
168
|
+
poc: {
|
|
169
|
+
repoSource: 'local',
|
|
170
|
+
iterations: [
|
|
171
|
+
{
|
|
172
|
+
iteration: 1,
|
|
173
|
+
startedAt: new Date().toISOString(),
|
|
174
|
+
endedAt: new Date().toISOString(),
|
|
175
|
+
outcome: 'scaffold',
|
|
176
|
+
filesChanged: [],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
182
|
+
expect(state.canSkipScaffold).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
it('falls back to fresh run when iterations have corrupt entries (T068)', () => {
|
|
185
|
+
const session = makeSession({
|
|
186
|
+
poc: {
|
|
187
|
+
repoSource: 'local',
|
|
188
|
+
iterations: [
|
|
189
|
+
{
|
|
190
|
+
iteration: 1,
|
|
191
|
+
startedAt: '', // Empty string — invalid
|
|
192
|
+
outcome: 'scaffold',
|
|
193
|
+
filesChanged: [],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
199
|
+
expect(state.hasPriorRun).toBe(false);
|
|
200
|
+
expect(state.resumeFromIteration).toBe(1);
|
|
201
|
+
});
|
|
202
|
+
it('falls back to fresh run when iteration has non-number iteration field (T068)', () => {
|
|
203
|
+
const session = makeSession({
|
|
204
|
+
poc: {
|
|
205
|
+
repoSource: 'local',
|
|
206
|
+
iterations: [
|
|
207
|
+
{
|
|
208
|
+
iteration: 'not a number',
|
|
209
|
+
startedAt: new Date().toISOString(),
|
|
210
|
+
outcome: 'scaffold',
|
|
211
|
+
filesChanged: [],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
217
|
+
expect(state.hasPriorRun).toBe(false);
|
|
218
|
+
expect(state.resumeFromIteration).toBe(1);
|
|
219
|
+
});
|
|
220
|
+
it('returns success priorFinalStatus for successful sessions', () => {
|
|
221
|
+
const session = makeSession({
|
|
222
|
+
poc: {
|
|
223
|
+
repoSource: 'local',
|
|
224
|
+
iterations: [
|
|
225
|
+
{
|
|
226
|
+
iteration: 1,
|
|
227
|
+
startedAt: new Date().toISOString(),
|
|
228
|
+
endedAt: new Date().toISOString(),
|
|
229
|
+
outcome: 'tests-passing',
|
|
230
|
+
filesChanged: [],
|
|
231
|
+
testResults: {
|
|
232
|
+
passed: 3,
|
|
233
|
+
failed: 0,
|
|
234
|
+
skipped: 0,
|
|
235
|
+
total: 3,
|
|
236
|
+
durationMs: 100,
|
|
237
|
+
failures: [],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
finalStatus: 'success',
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
245
|
+
expect(state.priorFinalStatus).toBe('success');
|
|
246
|
+
});
|
|
247
|
+
// ── T075: Validation — fresh vs resumed run quality comparison ────────────
|
|
248
|
+
describe('fresh vs resumed run quality (T075)', () => {
|
|
249
|
+
it('preserves iteration count consistency across fresh and resumed checkpoint derivations', () => {
|
|
250
|
+
// Fresh session (no prior run)
|
|
251
|
+
const freshSession = makeSession();
|
|
252
|
+
const freshState = deriveCheckpointState(freshSession, tmpDir);
|
|
253
|
+
expect(freshState.hasPriorRun).toBe(false);
|
|
254
|
+
expect(freshState.resumeFromIteration).toBe(1);
|
|
255
|
+
expect(freshState.completedIterations).toBe(0);
|
|
256
|
+
// Session with 3 completed iterations (simulating prior run)
|
|
257
|
+
const resumedSession = makeSession({
|
|
258
|
+
poc: {
|
|
259
|
+
iterations: [
|
|
260
|
+
{ iteration: 1, startedAt: '2026-01-01T12:00:00Z', endedAt: '2026-01-01T12:01:00Z', outcome: 'tests-failing', filesChanged: ['src/index.ts'], testResults: { passed: 0, failed: 1, skipped: 0, total: 1, durationMs: 100, failures: [{ testName: 'a', message: 'fail' }] } },
|
|
261
|
+
{ iteration: 2, startedAt: '2026-01-01T12:01:00Z', endedAt: '2026-01-01T12:02:00Z', outcome: 'tests-failing', filesChanged: ['src/index.ts'], testResults: { passed: 1, failed: 1, skipped: 0, total: 2, durationMs: 100, failures: [{ testName: 'b', message: 'fail' }] } },
|
|
262
|
+
{ iteration: 3, startedAt: '2026-01-01T12:02:00Z', endedAt: '2026-01-01T12:03:00Z', outcome: 'tests-passing', filesChanged: ['src/index.ts'], testResults: { passed: 2, failed: 0, skipped: 0, total: 2, durationMs: 100, failures: [] } },
|
|
263
|
+
],
|
|
264
|
+
finalStatus: 'success',
|
|
265
|
+
repoSource: 'local',
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
writeFileSync(join(tmpDir, '.sofia-metadata.json'), JSON.stringify({ sessionId: 'cp-test-session' }));
|
|
269
|
+
const resumeState = deriveCheckpointState(resumedSession, tmpDir);
|
|
270
|
+
expect(resumeState.hasPriorRun).toBe(true);
|
|
271
|
+
expect(resumeState.completedIterations).toBe(3);
|
|
272
|
+
expect(resumeState.resumeFromIteration).toBe(4);
|
|
273
|
+
// Quality validation: resumed state preserves iteration history
|
|
274
|
+
expect(resumeState.priorIterations).toHaveLength(3);
|
|
275
|
+
// Test pass counts increase across iterations (quality signal)
|
|
276
|
+
expect(resumeState.priorIterations[0].testResults.passed).toBe(0);
|
|
277
|
+
expect(resumeState.priorIterations[2].testResults.passed).toBe(2);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
// ── T076: Benchmark — resume detection overhead <500ms ────────────────────
|
|
281
|
+
describe('resume detection performance (T076)', () => {
|
|
282
|
+
it('deriveCheckpointState completes within 500ms even with many iterations', () => {
|
|
283
|
+
// Create a session with 50 iterations to stress-test derivation
|
|
284
|
+
const iterations = Array.from({ length: 50 }, (_, i) => ({
|
|
285
|
+
iteration: i + 1,
|
|
286
|
+
startedAt: '2026-01-01T12:00:00Z',
|
|
287
|
+
endedAt: '2026-01-01T12:01:00Z',
|
|
288
|
+
outcome: 'tests-failing',
|
|
289
|
+
filesChanged: ['src/index.ts'],
|
|
290
|
+
testResults: {
|
|
291
|
+
passed: i,
|
|
292
|
+
failed: 50 - i,
|
|
293
|
+
skipped: 0,
|
|
294
|
+
total: 50,
|
|
295
|
+
durationMs: 100,
|
|
296
|
+
failures: [{ testName: `test-${i}`, message: 'fail' }],
|
|
297
|
+
},
|
|
298
|
+
}));
|
|
299
|
+
const session = makeSession({
|
|
300
|
+
poc: {
|
|
301
|
+
iterations,
|
|
302
|
+
finalStatus: 'failed',
|
|
303
|
+
repoSource: 'local',
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
writeFileSync(join(tmpDir, '.sofia-metadata.json'), JSON.stringify({ sessionId: 'cp-test-session' }));
|
|
307
|
+
const start = performance.now();
|
|
308
|
+
const state = deriveCheckpointState(session, tmpDir);
|
|
309
|
+
const elapsed = performance.now() - start;
|
|
310
|
+
expect(elapsed).toBeLessThan(500);
|
|
311
|
+
expect(state.completedIterations).toBe(50);
|
|
312
|
+
expect(state.resumeFromIteration).toBe(51);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T013: Unit tests for CodeGenerator.
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - Parses fenced code blocks with `file=path` from LLM response
|
|
6
|
+
* - Writes files to outputDir
|
|
7
|
+
* - Handles empty response gracefully
|
|
8
|
+
* - Builds iteration prompt with test failures context
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { mkdtemp, rm, readFile } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { CodeGenerator, parseFencedCodeBlocks, buildFileTree, isUnsafePath, isPathWithinDirectory, } from '../../../src/develop/codeGenerator.js';
|
|
15
|
+
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
16
|
+
function makeTestResults(overrides) {
|
|
17
|
+
return {
|
|
18
|
+
passed: 1,
|
|
19
|
+
failed: 2,
|
|
20
|
+
skipped: 0,
|
|
21
|
+
total: 3,
|
|
22
|
+
durationMs: 500,
|
|
23
|
+
failures: [
|
|
24
|
+
{
|
|
25
|
+
testName: 'suite > test A',
|
|
26
|
+
message: 'Expected 3 but got 5',
|
|
27
|
+
file: 'tests/a.test.ts',
|
|
28
|
+
line: 10,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
testName: 'suite > test B',
|
|
32
|
+
message: 'Cannot read properties of undefined',
|
|
33
|
+
file: 'tests/b.test.ts',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
rawOutput: 'FAIL tests/a.test.ts\nFAIL tests/b.test.ts\n',
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// ── parseFencedCodeBlocks ─────────────────────────────────────────────────────
|
|
41
|
+
describe('parseFencedCodeBlocks', () => {
|
|
42
|
+
it('parses a single typescript code block with file=', () => {
|
|
43
|
+
const response = `Here are the changes:\n\n\`\`\`typescript file=src/index.ts\nexport function main() {\n return 42;\n}\n\`\`\`\n`;
|
|
44
|
+
const files = parseFencedCodeBlocks(response);
|
|
45
|
+
expect(files).toHaveLength(1);
|
|
46
|
+
expect(files[0].path).toBe('src/index.ts');
|
|
47
|
+
expect(files[0].content).toContain('export function main()');
|
|
48
|
+
expect(files[0].language).toBe('typescript');
|
|
49
|
+
});
|
|
50
|
+
it('parses multiple code blocks', () => {
|
|
51
|
+
const response = `\`\`\`typescript file=src/index.ts\nexport function a() {}\n\`\`\`\n\n\`\`\`typescript file=tests/index.test.ts\nimport { test } from "vitest";\n\`\`\`\n`;
|
|
52
|
+
const files = parseFencedCodeBlocks(response);
|
|
53
|
+
expect(files).toHaveLength(2);
|
|
54
|
+
expect(files[0].path).toBe('src/index.ts');
|
|
55
|
+
expect(files[1].path).toBe('tests/index.test.ts');
|
|
56
|
+
});
|
|
57
|
+
it('handles code block with ./prefix in path', () => {
|
|
58
|
+
const response = `\`\`\`typescript file=./src/index.ts\nexport const x = 1;\n\`\`\`\n`;
|
|
59
|
+
const files = parseFencedCodeBlocks(response);
|
|
60
|
+
expect(files).toHaveLength(1);
|
|
61
|
+
expect(files[0].path).toBe('src/index.ts'); // normalized
|
|
62
|
+
});
|
|
63
|
+
it('handles empty LLM response gracefully', () => {
|
|
64
|
+
const files = parseFencedCodeBlocks('');
|
|
65
|
+
expect(files).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
it('ignores code blocks without file= annotation', () => {
|
|
68
|
+
const response = `\`\`\`typescript\nexport function main() {}\n\`\`\`\n`;
|
|
69
|
+
const files = parseFencedCodeBlocks(response);
|
|
70
|
+
expect(files).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
it('handles ts shorthand language tag', () => {
|
|
73
|
+
const response = `\`\`\`ts file=src/index.ts\nexport const x = 1;\n\`\`\`\n`;
|
|
74
|
+
const files = parseFencedCodeBlocks(response);
|
|
75
|
+
expect(files).toHaveLength(1);
|
|
76
|
+
expect(files[0].language).toBe('ts');
|
|
77
|
+
});
|
|
78
|
+
it('handles json file type', () => {
|
|
79
|
+
const response = `\`\`\`json file=package.json\n{"name":"test"}\n\`\`\`\n`;
|
|
80
|
+
const files = parseFencedCodeBlocks(response);
|
|
81
|
+
expect(files).toHaveLength(1);
|
|
82
|
+
expect(files[0].path).toBe('package.json');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// ── buildFileTree ─────────────────────────────────────────────────────────────
|
|
86
|
+
describe('buildFileTree', () => {
|
|
87
|
+
let tmpDir;
|
|
88
|
+
beforeEach(async () => {
|
|
89
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'sofia-filetree-test-'));
|
|
90
|
+
});
|
|
91
|
+
afterEach(async () => {
|
|
92
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
93
|
+
});
|
|
94
|
+
it('returns empty array for non-existent directory', () => {
|
|
95
|
+
const tree = buildFileTree('/non/existent/path');
|
|
96
|
+
expect(tree).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
it('lists files in directory', async () => {
|
|
99
|
+
const { writeFile } = await import('node:fs/promises');
|
|
100
|
+
await writeFile(join(tmpDir, 'index.ts'), '', 'utf-8');
|
|
101
|
+
await writeFile(join(tmpDir, 'helper.ts'), '', 'utf-8');
|
|
102
|
+
const tree = buildFileTree(tmpDir);
|
|
103
|
+
expect(tree).toContain('helper.ts');
|
|
104
|
+
expect(tree).toContain('index.ts');
|
|
105
|
+
});
|
|
106
|
+
it('excludes node_modules and dist', async () => {
|
|
107
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
108
|
+
await mkdir(join(tmpDir, 'node_modules'), { recursive: true });
|
|
109
|
+
await writeFile(join(tmpDir, 'node_modules', 'pkg.js'), '', 'utf-8');
|
|
110
|
+
await mkdir(join(tmpDir, 'dist'), { recursive: true });
|
|
111
|
+
await writeFile(join(tmpDir, 'dist', 'index.js'), '', 'utf-8');
|
|
112
|
+
await writeFile(join(tmpDir, 'index.ts'), '', 'utf-8');
|
|
113
|
+
const tree = buildFileTree(tmpDir);
|
|
114
|
+
expect(tree).toContain('index.ts');
|
|
115
|
+
expect(tree.some((f) => f.includes('node_modules'))).toBe(false);
|
|
116
|
+
expect(tree.some((f) => f.includes('dist'))).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it('recursively lists subdirectories', async () => {
|
|
119
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
120
|
+
await mkdir(join(tmpDir, 'src'), { recursive: true });
|
|
121
|
+
await writeFile(join(tmpDir, 'src', 'index.ts'), '', 'utf-8');
|
|
122
|
+
const tree = buildFileTree(tmpDir);
|
|
123
|
+
expect(tree).toContain('src/');
|
|
124
|
+
expect(tree).toContain(' index.ts');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ── isUnsafePath ──────────────────────────────────────────────────────────────
|
|
128
|
+
describe('isUnsafePath', () => {
|
|
129
|
+
it('returns true for POSIX absolute paths', () => {
|
|
130
|
+
expect(isUnsafePath('/etc/passwd')).toBe(true);
|
|
131
|
+
expect(isUnsafePath('/tmp/evil')).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
it('returns true for Windows drive-letter paths', () => {
|
|
134
|
+
expect(isUnsafePath('C:\\Windows\\System32\\evil.ts')).toBe(true);
|
|
135
|
+
expect(isUnsafePath('c:/Windows/evil.ts')).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
it('returns true for UNC paths (backslash and forward-slash)', () => {
|
|
138
|
+
expect(isUnsafePath('\\\\server\\share\\evil.ts')).toBe(true);
|
|
139
|
+
expect(isUnsafePath('//server/share/evil.ts')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
it('returns true for path traversal segments', () => {
|
|
142
|
+
expect(isUnsafePath('../../etc/passwd')).toBe(true);
|
|
143
|
+
expect(isUnsafePath('src/../../../evil')).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
it('returns false for normal relative paths', () => {
|
|
146
|
+
expect(isUnsafePath('src/index.ts')).toBe(false);
|
|
147
|
+
expect(isUnsafePath('tests/index.test.ts')).toBe(false);
|
|
148
|
+
expect(isUnsafePath('package.json')).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
// ── isPathWithinDirectory ─────────────────────────────────────────────────────
|
|
152
|
+
describe('isPathWithinDirectory', () => {
|
|
153
|
+
it('returns true for relative paths inside the directory', () => {
|
|
154
|
+
expect(isPathWithinDirectory('src/index.ts', '/tmp/poc')).toBe(true);
|
|
155
|
+
expect(isPathWithinDirectory('package.json', '/tmp/poc')).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
it('returns false for paths that resolve outside the directory', () => {
|
|
158
|
+
// On POSIX, path.resolve('/tmp/poc', '../../etc/passwd') → '/etc/passwd'
|
|
159
|
+
expect(isPathWithinDirectory('../../etc/passwd', '/tmp/poc')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
// ── CodeGenerator ─────────────────────────────────────────────────────────────
|
|
163
|
+
describe('CodeGenerator', () => {
|
|
164
|
+
let tmpDir;
|
|
165
|
+
let generator;
|
|
166
|
+
beforeEach(async () => {
|
|
167
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'sofia-codegen-test-'));
|
|
168
|
+
generator = new CodeGenerator(tmpDir);
|
|
169
|
+
});
|
|
170
|
+
afterEach(async () => {
|
|
171
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
172
|
+
});
|
|
173
|
+
describe('buildIterationPrompt', () => {
|
|
174
|
+
it('includes iteration number and max', () => {
|
|
175
|
+
const prompt = generator.buildIterationPrompt({
|
|
176
|
+
iteration: 3,
|
|
177
|
+
maxIterations: 10,
|
|
178
|
+
previousOutcome: 'tests-failing',
|
|
179
|
+
testResults: makeTestResults(),
|
|
180
|
+
filesInPoc: ['src/index.ts'],
|
|
181
|
+
});
|
|
182
|
+
expect(prompt).toContain('Iteration: 3 of 10');
|
|
183
|
+
expect(prompt).toContain('tests-failing');
|
|
184
|
+
});
|
|
185
|
+
it('includes failing test details', () => {
|
|
186
|
+
const prompt = generator.buildIterationPrompt({
|
|
187
|
+
iteration: 2,
|
|
188
|
+
maxIterations: 5,
|
|
189
|
+
previousOutcome: 'scaffold',
|
|
190
|
+
testResults: makeTestResults(),
|
|
191
|
+
filesInPoc: ['src/index.ts'],
|
|
192
|
+
});
|
|
193
|
+
expect(prompt).toContain('suite > test A');
|
|
194
|
+
expect(prompt).toContain('Expected 3 but got 5');
|
|
195
|
+
expect(prompt).toContain('tests/a.test.ts');
|
|
196
|
+
});
|
|
197
|
+
it('includes files in PoC', () => {
|
|
198
|
+
const prompt = generator.buildIterationPrompt({
|
|
199
|
+
iteration: 2,
|
|
200
|
+
maxIterations: 5,
|
|
201
|
+
previousOutcome: 'scaffold',
|
|
202
|
+
testResults: makeTestResults({ failed: 0, failures: [], total: 1, passed: 1 }),
|
|
203
|
+
filesInPoc: ['src/index.ts', 'tests/index.test.ts', 'package.json'],
|
|
204
|
+
});
|
|
205
|
+
expect(prompt).toContain('src/index.ts');
|
|
206
|
+
expect(prompt).toContain('tests/index.test.ts');
|
|
207
|
+
});
|
|
208
|
+
it('includes MCP context when provided', () => {
|
|
209
|
+
const prompt = generator.buildIterationPrompt({
|
|
210
|
+
iteration: 2,
|
|
211
|
+
maxIterations: 5,
|
|
212
|
+
previousOutcome: 'tests-failing',
|
|
213
|
+
testResults: makeTestResults(),
|
|
214
|
+
filesInPoc: [],
|
|
215
|
+
mcpContext: 'express@5.0.0 API docs: use app.get()',
|
|
216
|
+
});
|
|
217
|
+
expect(prompt).toContain('express@5.0.0 API docs');
|
|
218
|
+
expect(prompt).toContain('MCP Context');
|
|
219
|
+
});
|
|
220
|
+
it('includes raw test output', () => {
|
|
221
|
+
const prompt = generator.buildIterationPrompt({
|
|
222
|
+
iteration: 2,
|
|
223
|
+
maxIterations: 5,
|
|
224
|
+
previousOutcome: 'tests-failing',
|
|
225
|
+
testResults: makeTestResults({ rawOutput: 'FAIL tests/a.test.ts\n' }),
|
|
226
|
+
filesInPoc: [],
|
|
227
|
+
});
|
|
228
|
+
expect(prompt).toContain('FAIL tests/a.test.ts');
|
|
229
|
+
});
|
|
230
|
+
it('shows "0 failing tests" task when no failures', () => {
|
|
231
|
+
const prompt = generator.buildIterationPrompt({
|
|
232
|
+
iteration: 3,
|
|
233
|
+
maxIterations: 10,
|
|
234
|
+
previousOutcome: 'tests-passing',
|
|
235
|
+
testResults: makeTestResults({ passed: 3, failed: 0, failures: [], total: 3 }),
|
|
236
|
+
filesInPoc: [],
|
|
237
|
+
});
|
|
238
|
+
expect(prompt).toContain('0 failing tests');
|
|
239
|
+
});
|
|
240
|
+
it('includes file contents in ## Current Code section when fileContents provided', () => {
|
|
241
|
+
const prompt = generator.buildIterationPrompt({
|
|
242
|
+
iteration: 2,
|
|
243
|
+
maxIterations: 5,
|
|
244
|
+
previousOutcome: 'tests-failing',
|
|
245
|
+
testResults: makeTestResults(),
|
|
246
|
+
filesInPoc: ['src/index.ts'],
|
|
247
|
+
fileContents: [
|
|
248
|
+
{ path: 'src/index.ts', content: 'export function main() { return 42; }' },
|
|
249
|
+
{ path: 'tests/index.test.ts', content: 'import { test } from "vitest";' },
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
expect(prompt).toContain('## Current Code');
|
|
253
|
+
expect(prompt).toContain('### src/index.ts');
|
|
254
|
+
expect(prompt).toContain('export function main() { return 42; }');
|
|
255
|
+
expect(prompt).toContain('### tests/index.test.ts');
|
|
256
|
+
});
|
|
257
|
+
it('omits ## Current Code section when fileContents is empty or absent', () => {
|
|
258
|
+
const prompt = generator.buildIterationPrompt({
|
|
259
|
+
iteration: 2,
|
|
260
|
+
maxIterations: 5,
|
|
261
|
+
previousOutcome: 'tests-failing',
|
|
262
|
+
testResults: makeTestResults(),
|
|
263
|
+
filesInPoc: ['src/index.ts'],
|
|
264
|
+
});
|
|
265
|
+
expect(prompt).not.toContain('## Current Code');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe('buildPromptContextSummary', () => {
|
|
269
|
+
it('returns a compact summary for auditability', () => {
|
|
270
|
+
const summary = generator.buildPromptContextSummary({
|
|
271
|
+
iteration: 3,
|
|
272
|
+
maxIterations: 10,
|
|
273
|
+
previousOutcome: 'tests-failing',
|
|
274
|
+
testResults: makeTestResults({ failed: 2 }),
|
|
275
|
+
filesInPoc: ['src/index.ts', 'tests/index.test.ts'],
|
|
276
|
+
});
|
|
277
|
+
expect(summary).toContain('Iteration 3 of 10');
|
|
278
|
+
expect(summary).toContain('2 failures');
|
|
279
|
+
expect(summary).toContain('src/index.ts');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('applyChanges', () => {
|
|
283
|
+
it('writes parsed files to outputDir', async () => {
|
|
284
|
+
const llmResponse = `\`\`\`typescript file=src/index.ts\nexport function hello() { return 'hello'; }\n\`\`\`\n`;
|
|
285
|
+
await generator.applyChanges(llmResponse);
|
|
286
|
+
const content = await readFile(join(tmpDir, 'src', 'index.ts'), 'utf-8');
|
|
287
|
+
expect(content).toContain('export function hello()');
|
|
288
|
+
});
|
|
289
|
+
it('handles empty LLM response gracefully', async () => {
|
|
290
|
+
const result = await generator.applyChanges('');
|
|
291
|
+
expect(result.writtenFiles).toHaveLength(0);
|
|
292
|
+
expect(result.dependenciesChanged).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
it('creates parent directories for nested files', async () => {
|
|
295
|
+
const llmResponse = `\`\`\`typescript file=src/utils/helper.ts\nexport function help() {}\n\`\`\`\n`;
|
|
296
|
+
const result = await generator.applyChanges(llmResponse);
|
|
297
|
+
expect(result.writtenFiles).toContain('src/utils/helper.ts');
|
|
298
|
+
const content = await readFile(join(tmpDir, 'src', 'utils', 'helper.ts'), 'utf-8');
|
|
299
|
+
expect(content).toContain('export function help()');
|
|
300
|
+
});
|
|
301
|
+
it('rejects paths with path traversal', async () => {
|
|
302
|
+
const llmResponse = `\`\`\`typescript file=../../etc/passwd\nmalicious content\n\`\`\`\n`;
|
|
303
|
+
const result = await generator.applyChanges(llmResponse);
|
|
304
|
+
expect(result.writtenFiles).toHaveLength(0);
|
|
305
|
+
});
|
|
306
|
+
it('rejects Windows absolute paths (drive-letter)', async () => {
|
|
307
|
+
const llmResponse = `\`\`\`typescript file=C:\\Windows\\System32\\malicious.ts\nevil\n\`\`\`\n`;
|
|
308
|
+
const result = await generator.applyChanges(llmResponse);
|
|
309
|
+
expect(result.writtenFiles).toHaveLength(0);
|
|
310
|
+
});
|
|
311
|
+
it('rejects Windows UNC paths', async () => {
|
|
312
|
+
const llmResponse = `\`\`\`typescript file=\\\\server\\share\\malicious.ts\nevil\n\`\`\`\n`;
|
|
313
|
+
const result = await generator.applyChanges(llmResponse);
|
|
314
|
+
expect(result.writtenFiles).toHaveLength(0);
|
|
315
|
+
});
|
|
316
|
+
it('detects dependency changes when package.json is updated', async () => {
|
|
317
|
+
const { writeFile } = await import('node:fs/promises');
|
|
318
|
+
// Create initial package.json
|
|
319
|
+
await writeFile(join(tmpDir, 'package.json'), JSON.stringify({ dependencies: { express: '^4.0.0' }, devDependencies: {} }), 'utf-8');
|
|
320
|
+
// Update with new dependency
|
|
321
|
+
const llmResponse = `\`\`\`json file=package.json\n{"dependencies":{"express":"^4.0.0","axios":"^1.0.0"},"devDependencies":{}}\n\`\`\`\n`;
|
|
322
|
+
const result = await generator.applyChanges(llmResponse);
|
|
323
|
+
expect(result.dependenciesChanged).toBe(true);
|
|
324
|
+
expect(result.newDependencies['axios']).toBe('^1.0.0');
|
|
325
|
+
});
|
|
326
|
+
it('returns dependenciesChanged=false when package.json unchanged', async () => {
|
|
327
|
+
const { writeFile } = await import('node:fs/promises');
|
|
328
|
+
await writeFile(join(tmpDir, 'package.json'), JSON.stringify({ dependencies: { express: '^4.0.0' }, devDependencies: {} }), 'utf-8');
|
|
329
|
+
// Same package.json
|
|
330
|
+
const llmResponse = `\`\`\`json file=package.json\n{"dependencies":{"express":"^4.0.0"},"devDependencies":{}}\n\`\`\`\n`;
|
|
331
|
+
const result = await generator.applyChanges(llmResponse);
|
|
332
|
+
expect(result.dependenciesChanged).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
it('returns writtenFiles list', async () => {
|
|
335
|
+
const llmResponse = [
|
|
336
|
+
'```typescript file=src/index.ts\nexport const x = 1;\n```',
|
|
337
|
+
'```typescript file=tests/index.test.ts\nimport { test } from "vitest";\n```',
|
|
338
|
+
].join('\n');
|
|
339
|
+
const result = await generator.applyChanges(llmResponse);
|
|
340
|
+
expect(result.writtenFiles).toContain('src/index.ts');
|
|
341
|
+
expect(result.writtenFiles).toContain('tests/index.test.ts');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
describe('getFilesInPoc', () => {
|
|
345
|
+
it('returns list of files in the output directory', async () => {
|
|
346
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
347
|
+
await mkdir(join(tmpDir, 'src'), { recursive: true });
|
|
348
|
+
await writeFile(join(tmpDir, 'src', 'index.ts'), '', 'utf-8');
|
|
349
|
+
await writeFile(join(tmpDir, 'package.json'), '{}', 'utf-8');
|
|
350
|
+
const files = generator.getFilesInPoc();
|
|
351
|
+
expect(files.some((f) => f.includes('index.ts'))).toBe(true);
|
|
352
|
+
expect(files.some((f) => f.includes('package.json'))).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|