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,689 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T004/T005: Unit tests for McpTransport implementations.
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - HttpMcpTransport: JSON-RPC framing, auth headers, timeout, error classification
|
|
6
|
+
* - StdioMcpTransport: subprocess spawn, initialize handshake, pending request lifecycle
|
|
7
|
+
* - createTransport() factory: creates correct implementation per config type
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import type { ChildProcess } from 'node:child_process';
|
|
11
|
+
import { EventEmitter, PassThrough } from 'node:stream';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
HttpMcpTransport,
|
|
15
|
+
StdioMcpTransport,
|
|
16
|
+
McpTransportError,
|
|
17
|
+
createTransport,
|
|
18
|
+
} from '../../../src/mcp/mcpTransport.js';
|
|
19
|
+
import type { StdioServerConfig, HttpServerConfig } from '../../../src/mcp/mcpManager.js';
|
|
20
|
+
|
|
21
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function makeLogger(): import('pino').Logger {
|
|
24
|
+
return {
|
|
25
|
+
info: vi.fn(),
|
|
26
|
+
warn: vi.fn(),
|
|
27
|
+
error: vi.fn(),
|
|
28
|
+
debug: vi.fn(),
|
|
29
|
+
trace: vi.fn(),
|
|
30
|
+
fatal: vi.fn(),
|
|
31
|
+
child: vi.fn().mockReturnThis(),
|
|
32
|
+
level: 'silent',
|
|
33
|
+
} as unknown as import('pino').Logger;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeHttpConfig(overrides?: Partial<HttpServerConfig>): HttpServerConfig {
|
|
37
|
+
return {
|
|
38
|
+
name: 'test-http',
|
|
39
|
+
type: 'http',
|
|
40
|
+
url: 'https://api.example.com/mcp',
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeStdioConfig(overrides?: Partial<StdioServerConfig>): StdioServerConfig {
|
|
46
|
+
return {
|
|
47
|
+
name: 'test-stdio',
|
|
48
|
+
type: 'stdio',
|
|
49
|
+
command: 'node',
|
|
50
|
+
args: ['server.js'],
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── HttpMcpTransport Tests (T004) ────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
describe('HttpMcpTransport', () => {
|
|
58
|
+
let originalFetch: typeof globalThis.fetch;
|
|
59
|
+
let originalToken: string | undefined;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
originalFetch = globalThis.fetch;
|
|
63
|
+
originalToken = process.env.GITHUB_TOKEN;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
globalThis.fetch = originalFetch;
|
|
68
|
+
if (originalToken !== undefined) {
|
|
69
|
+
process.env.GITHUB_TOKEN = originalToken;
|
|
70
|
+
} else {
|
|
71
|
+
delete process.env.GITHUB_TOKEN;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('sends correct JSON-RPC tools/call framing', async () => {
|
|
76
|
+
let capturedBody: unknown;
|
|
77
|
+
globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => {
|
|
78
|
+
capturedBody = JSON.parse(init.body as string);
|
|
79
|
+
return new Response(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
jsonrpc: '2.0',
|
|
82
|
+
id: 1,
|
|
83
|
+
result: { content: [{ type: 'text', text: '{"ok":true}' }] },
|
|
84
|
+
}),
|
|
85
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
86
|
+
);
|
|
87
|
+
}) as unknown as typeof fetch;
|
|
88
|
+
|
|
89
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
90
|
+
await transport.callTool('search', { query: 'test' }, 5000);
|
|
91
|
+
|
|
92
|
+
expect(capturedBody).toEqual({
|
|
93
|
+
jsonrpc: '2.0',
|
|
94
|
+
id: expect.any(Number),
|
|
95
|
+
method: 'tools/call',
|
|
96
|
+
params: { name: 'search', arguments: { query: 'test' } },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('sets Authorization Bearer header from GITHUB_TOKEN', async () => {
|
|
101
|
+
process.env.GITHUB_TOKEN = 'test-token-abc';
|
|
102
|
+
let capturedHeaders: Record<string, string> = {};
|
|
103
|
+
|
|
104
|
+
globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => {
|
|
105
|
+
capturedHeaders = Object.fromEntries(Object.entries(init.headers as Record<string, string>));
|
|
106
|
+
return new Response(
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
jsonrpc: '2.0',
|
|
109
|
+
id: 1,
|
|
110
|
+
result: { data: 'ok' },
|
|
111
|
+
}),
|
|
112
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
113
|
+
);
|
|
114
|
+
}) as unknown as typeof fetch;
|
|
115
|
+
|
|
116
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
117
|
+
await transport.callTool('test-tool', {}, 5000);
|
|
118
|
+
|
|
119
|
+
expect(capturedHeaders['Authorization']).toBe('Bearer test-token-abc');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not set Authorization header when GITHUB_TOKEN is unset', async () => {
|
|
123
|
+
delete process.env.GITHUB_TOKEN;
|
|
124
|
+
let capturedHeaders: Record<string, string> = {};
|
|
125
|
+
|
|
126
|
+
globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => {
|
|
127
|
+
capturedHeaders = Object.fromEntries(Object.entries(init.headers as Record<string, string>));
|
|
128
|
+
return new Response(
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
jsonrpc: '2.0',
|
|
131
|
+
id: 1,
|
|
132
|
+
result: {},
|
|
133
|
+
}),
|
|
134
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
135
|
+
);
|
|
136
|
+
}) as unknown as typeof fetch;
|
|
137
|
+
|
|
138
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
139
|
+
await transport.callTool('test-tool', {}, 5000);
|
|
140
|
+
|
|
141
|
+
expect(capturedHeaders['Authorization']).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('throws McpTransportError with timeout classification on AbortController timeout', async () => {
|
|
145
|
+
globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => {
|
|
146
|
+
// Simulate slow server — wait for abort signal
|
|
147
|
+
return new Promise<Response>((_resolve, reject) => {
|
|
148
|
+
const signal = init.signal;
|
|
149
|
+
if (signal) {
|
|
150
|
+
signal.addEventListener('abort', () => {
|
|
151
|
+
const err = new Error('The operation was aborted');
|
|
152
|
+
err.name = 'AbortError';
|
|
153
|
+
reject(err);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}) as unknown as typeof fetch;
|
|
158
|
+
|
|
159
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
160
|
+
|
|
161
|
+
await expect(transport.callTool('slow-tool', {}, 50)).rejects.toThrow(McpTransportError);
|
|
162
|
+
try {
|
|
163
|
+
await transport.callTool('slow-tool', {}, 50);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
166
|
+
expect((err as McpTransportError).serverName).toBe('test-http');
|
|
167
|
+
expect((err as Error & { code?: string }).code).toBe('ETIMEDOUT');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('classifies HTTP 401 as auth-failure error', async () => {
|
|
172
|
+
globalThis.fetch = vi.fn(async () => {
|
|
173
|
+
return new Response('Unauthorized', { status: 401 });
|
|
174
|
+
}) as unknown as typeof fetch;
|
|
175
|
+
|
|
176
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
177
|
+
|
|
178
|
+
await expect(transport.callTool('secure-tool', {}, 5000)).rejects.toThrow(McpTransportError);
|
|
179
|
+
try {
|
|
180
|
+
await transport.callTool('secure-tool', {}, 5000);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
183
|
+
expect((err as McpTransportError).message).toContain('authentication failed');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('classifies HTTP 403 as auth-failure error', async () => {
|
|
188
|
+
globalThis.fetch = vi.fn(async () => {
|
|
189
|
+
return new Response('Forbidden', { status: 403 });
|
|
190
|
+
}) as unknown as typeof fetch;
|
|
191
|
+
|
|
192
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
193
|
+
|
|
194
|
+
await expect(transport.callTool('forbidden-tool', {}, 5000)).rejects.toThrow(McpTransportError);
|
|
195
|
+
try {
|
|
196
|
+
await transport.callTool('forbidden-tool', {}, 5000);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
199
|
+
expect((err as McpTransportError).message).toContain('authentication failed');
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('throws McpTransportError on HTTP 5xx server error', async () => {
|
|
204
|
+
globalThis.fetch = vi.fn(async () => {
|
|
205
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
206
|
+
}) as unknown as typeof fetch;
|
|
207
|
+
|
|
208
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
209
|
+
|
|
210
|
+
await expect(transport.callTool('error-tool', {}, 5000)).rejects.toThrow(McpTransportError);
|
|
211
|
+
try {
|
|
212
|
+
await transport.callTool('error-tool', {}, 5000);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
215
|
+
expect((err as McpTransportError).message).toContain('server error');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('throws McpTransportError on non-JSON response body', async () => {
|
|
220
|
+
globalThis.fetch = vi.fn(async () => {
|
|
221
|
+
return new Response('not json at all', {
|
|
222
|
+
status: 200,
|
|
223
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
224
|
+
});
|
|
225
|
+
}) as unknown as typeof fetch;
|
|
226
|
+
|
|
227
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
228
|
+
|
|
229
|
+
await expect(transport.callTool('bad-format', {}, 5000)).rejects.toThrow(McpTransportError);
|
|
230
|
+
try {
|
|
231
|
+
await transport.callTool('bad-format', {}, 5000);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
234
|
+
expect((err as McpTransportError).message).toContain('Non-JSON response');
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('throws McpTransportError on JSON-RPC error in response', async () => {
|
|
239
|
+
globalThis.fetch = vi.fn(async () => {
|
|
240
|
+
return new Response(
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
jsonrpc: '2.0',
|
|
243
|
+
id: 1,
|
|
244
|
+
error: { code: -32601, message: 'Method not found' },
|
|
245
|
+
}),
|
|
246
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
247
|
+
);
|
|
248
|
+
}) as unknown as typeof fetch;
|
|
249
|
+
|
|
250
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
251
|
+
|
|
252
|
+
await expect(transport.callTool('missing-tool', {}, 5000)).rejects.toThrow(McpTransportError);
|
|
253
|
+
try {
|
|
254
|
+
await transport.callTool('missing-tool', {}, 5000);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
257
|
+
expect((err as McpTransportError).message).toContain('Method not found');
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('normalizes JSON-RPC validation errors with JSON details', async () => {
|
|
262
|
+
globalThis.fetch = vi.fn(async () => {
|
|
263
|
+
return new Response(
|
|
264
|
+
JSON.stringify({
|
|
265
|
+
jsonrpc: '2.0',
|
|
266
|
+
id: 1,
|
|
267
|
+
error: {
|
|
268
|
+
code: -32602,
|
|
269
|
+
message:
|
|
270
|
+
'MCP error -32602: Input validation error: Invalid arguments for tool resolve-library-id: [{"path":["query"],"message":"Invalid input: expected string, received undefined"}]',
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
274
|
+
);
|
|
275
|
+
}) as unknown as typeof fetch;
|
|
276
|
+
|
|
277
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
278
|
+
|
|
279
|
+
await expect(transport.callTool('resolve-library-id', {}, 5000)).rejects.toThrow(
|
|
280
|
+
McpTransportError,
|
|
281
|
+
);
|
|
282
|
+
try {
|
|
283
|
+
await transport.callTool('resolve-library-id', {}, 5000);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
286
|
+
expect((err as McpTransportError).message).not.toContain('[{');
|
|
287
|
+
expect((err as McpTransportError).message).toContain('query: Invalid input');
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('extracts content from result.content[0].text', async () => {
|
|
292
|
+
globalThis.fetch = vi.fn(async () => {
|
|
293
|
+
return new Response(
|
|
294
|
+
JSON.stringify({
|
|
295
|
+
jsonrpc: '2.0',
|
|
296
|
+
id: 1,
|
|
297
|
+
result: { content: [{ type: 'text', text: '{"found": true}' }] },
|
|
298
|
+
}),
|
|
299
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
300
|
+
);
|
|
301
|
+
}) as unknown as typeof fetch;
|
|
302
|
+
|
|
303
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
304
|
+
const response = await transport.callTool('search', {}, 5000);
|
|
305
|
+
|
|
306
|
+
expect(response.content).toBe('{"found": true}');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('returns result directly when no content array', async () => {
|
|
310
|
+
globalThis.fetch = vi.fn(async () => {
|
|
311
|
+
return new Response(
|
|
312
|
+
JSON.stringify({
|
|
313
|
+
jsonrpc: '2.0',
|
|
314
|
+
id: 1,
|
|
315
|
+
result: { key: 'value', nested: { a: 1 } },
|
|
316
|
+
}),
|
|
317
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
318
|
+
);
|
|
319
|
+
}) as unknown as typeof fetch;
|
|
320
|
+
|
|
321
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
322
|
+
const response = await transport.callTool('data', {}, 5000);
|
|
323
|
+
|
|
324
|
+
expect(response.content).toEqual({ key: 'value', nested: { a: 1 } });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('includes config headers in request', async () => {
|
|
328
|
+
let capturedHeaders: Record<string, string> = {};
|
|
329
|
+
|
|
330
|
+
globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => {
|
|
331
|
+
capturedHeaders = Object.fromEntries(Object.entries(init.headers as Record<string, string>));
|
|
332
|
+
return new Response(
|
|
333
|
+
JSON.stringify({
|
|
334
|
+
jsonrpc: '2.0',
|
|
335
|
+
id: 1,
|
|
336
|
+
result: {},
|
|
337
|
+
}),
|
|
338
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
339
|
+
);
|
|
340
|
+
}) as unknown as typeof fetch;
|
|
341
|
+
|
|
342
|
+
delete process.env.GITHUB_TOKEN;
|
|
343
|
+
const transport = new HttpMcpTransport(
|
|
344
|
+
makeHttpConfig({ headers: { 'X-Custom': 'value' } }),
|
|
345
|
+
makeLogger(),
|
|
346
|
+
);
|
|
347
|
+
await transport.callTool('tool', {}, 5000);
|
|
348
|
+
|
|
349
|
+
expect(capturedHeaders['X-Custom']).toBe('value');
|
|
350
|
+
expect(capturedHeaders['Content-Type']).toBe('application/json');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('isConnected() always returns true (HTTP is stateless)', () => {
|
|
354
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
355
|
+
expect(transport.isConnected()).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('disconnect() is a no-op', async () => {
|
|
359
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
360
|
+
await expect(transport.disconnect()).resolves.toBeUndefined();
|
|
361
|
+
expect(transport.isConnected()).toBe(true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('includes raw response in ToolCallResponse', async () => {
|
|
365
|
+
const rawResult = { content: [{ type: 'text', text: 'hello' }] };
|
|
366
|
+
globalThis.fetch = vi.fn(async () => {
|
|
367
|
+
return new Response(
|
|
368
|
+
JSON.stringify({
|
|
369
|
+
jsonrpc: '2.0',
|
|
370
|
+
id: 1,
|
|
371
|
+
result: rawResult,
|
|
372
|
+
}),
|
|
373
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
374
|
+
);
|
|
375
|
+
}) as unknown as typeof fetch;
|
|
376
|
+
|
|
377
|
+
const transport = new HttpMcpTransport(makeHttpConfig(), makeLogger());
|
|
378
|
+
const response = await transport.callTool('tool', {}, 5000);
|
|
379
|
+
|
|
380
|
+
expect(response.raw).toBeDefined();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ── StdioMcpTransport Tests (T005) ──────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
// We mock child_process.spawn to avoid real subprocess spawning
|
|
387
|
+
vi.mock('node:child_process', () => {
|
|
388
|
+
return {
|
|
389
|
+
spawn: vi.fn(),
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('StdioMcpTransport', () => {
|
|
394
|
+
let mockSpawn: ReturnType<typeof vi.fn>;
|
|
395
|
+
|
|
396
|
+
beforeEach(async () => {
|
|
397
|
+
const cp = await import('node:child_process');
|
|
398
|
+
mockSpawn = vi.mocked(cp.spawn);
|
|
399
|
+
mockSpawn.mockReset();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Create a mock child process with stdin/stdout/stderr streams
|
|
404
|
+
* and event emitter capabilities.
|
|
405
|
+
*/
|
|
406
|
+
function createMockProcess(): ChildProcess & {
|
|
407
|
+
_stdout: import('node:stream').PassThrough;
|
|
408
|
+
_stdin: { write: ReturnType<typeof vi.fn> };
|
|
409
|
+
} {
|
|
410
|
+
const stdout = new PassThrough();
|
|
411
|
+
const stdin = { write: vi.fn(), end: vi.fn() };
|
|
412
|
+
const stderr = new PassThrough();
|
|
413
|
+
|
|
414
|
+
const proc = new EventEmitter() as unknown as ChildProcess & {
|
|
415
|
+
_stdout: import('node:stream').PassThrough;
|
|
416
|
+
_stdin: { write: ReturnType<typeof vi.fn> };
|
|
417
|
+
};
|
|
418
|
+
Object.assign(proc, {
|
|
419
|
+
stdout,
|
|
420
|
+
stdin,
|
|
421
|
+
stderr,
|
|
422
|
+
pid: 12345,
|
|
423
|
+
kill: vi.fn(),
|
|
424
|
+
_stdout: stdout,
|
|
425
|
+
_stdin: stdin,
|
|
426
|
+
});
|
|
427
|
+
return proc;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
it('spawns subprocess with correct command and args', async () => {
|
|
431
|
+
const proc = createMockProcess();
|
|
432
|
+
mockSpawn.mockReturnValue(proc);
|
|
433
|
+
|
|
434
|
+
const config = makeStdioConfig({ command: 'npx', args: ['-y', 'my-mcp-server'] });
|
|
435
|
+
const transport = new StdioMcpTransport(config, makeLogger());
|
|
436
|
+
|
|
437
|
+
// Start connect but don't await — we need to send the response
|
|
438
|
+
const connectPromise = transport.connect();
|
|
439
|
+
|
|
440
|
+
// Simulate init response
|
|
441
|
+
setTimeout(() => {
|
|
442
|
+
proc._stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: { ok: true } }) + '\n');
|
|
443
|
+
}, 10);
|
|
444
|
+
|
|
445
|
+
await connectPromise;
|
|
446
|
+
|
|
447
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
448
|
+
'npx',
|
|
449
|
+
['-y', 'my-mcp-server'],
|
|
450
|
+
expect.objectContaining({
|
|
451
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('sends initialize JSON-RPC request during connect', async () => {
|
|
457
|
+
const proc = createMockProcess();
|
|
458
|
+
mockSpawn.mockReturnValue(proc);
|
|
459
|
+
|
|
460
|
+
const transport = new StdioMcpTransport(makeStdioConfig(), makeLogger());
|
|
461
|
+
const connectPromise = transport.connect();
|
|
462
|
+
|
|
463
|
+
setTimeout(() => {
|
|
464
|
+
proc._stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} }) + '\n');
|
|
465
|
+
}, 10);
|
|
466
|
+
|
|
467
|
+
await connectPromise;
|
|
468
|
+
|
|
469
|
+
expect(proc._stdin.write).toHaveBeenCalled();
|
|
470
|
+
const written = proc._stdin.write.mock.calls[0][0] as string;
|
|
471
|
+
const parsed = JSON.parse(written.trim());
|
|
472
|
+
expect(parsed).toEqual({
|
|
473
|
+
jsonrpc: '2.0',
|
|
474
|
+
id: 1,
|
|
475
|
+
method: 'initialize',
|
|
476
|
+
params: {
|
|
477
|
+
protocolVersion: '1.0',
|
|
478
|
+
clientInfo: { name: 'sofIA', version: '0.1.0' },
|
|
479
|
+
capabilities: {},
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('resolves callTool on matching response id', async () => {
|
|
485
|
+
const proc = createMockProcess();
|
|
486
|
+
mockSpawn.mockReturnValue(proc);
|
|
487
|
+
|
|
488
|
+
const transport = new StdioMcpTransport(makeStdioConfig(), makeLogger());
|
|
489
|
+
const connectPromise = transport.connect();
|
|
490
|
+
|
|
491
|
+
setTimeout(() => {
|
|
492
|
+
proc._stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} }) + '\n');
|
|
493
|
+
}, 10);
|
|
494
|
+
|
|
495
|
+
await connectPromise;
|
|
496
|
+
|
|
497
|
+
// Now callTool — the next id will be 2
|
|
498
|
+
const callPromise = transport.callTool('test-tool', { arg: 'value' }, 5000);
|
|
499
|
+
|
|
500
|
+
setTimeout(() => {
|
|
501
|
+
proc._stdout.write(
|
|
502
|
+
JSON.stringify({
|
|
503
|
+
jsonrpc: '2.0',
|
|
504
|
+
id: 2,
|
|
505
|
+
result: { content: [{ type: 'text', text: '{"result":"ok"}' }] },
|
|
506
|
+
}) + '\n',
|
|
507
|
+
);
|
|
508
|
+
}, 10);
|
|
509
|
+
|
|
510
|
+
const response = await callPromise;
|
|
511
|
+
expect(response.content).toBe('{"result":"ok"}');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('rejects all pending requests when subprocess exits', async () => {
|
|
515
|
+
const proc = createMockProcess();
|
|
516
|
+
mockSpawn.mockReturnValue(proc);
|
|
517
|
+
|
|
518
|
+
const transport = new StdioMcpTransport(makeStdioConfig(), makeLogger());
|
|
519
|
+
const connectPromise = transport.connect();
|
|
520
|
+
|
|
521
|
+
setTimeout(() => {
|
|
522
|
+
proc._stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} }) + '\n');
|
|
523
|
+
}, 10);
|
|
524
|
+
|
|
525
|
+
await connectPromise;
|
|
526
|
+
|
|
527
|
+
// Start a call that will hang
|
|
528
|
+
const callPromise = transport.callTool('will-fail', {}, 30000);
|
|
529
|
+
|
|
530
|
+
// Simulate subprocess exit
|
|
531
|
+
setTimeout(() => {
|
|
532
|
+
proc.emit('exit', 1);
|
|
533
|
+
}, 10);
|
|
534
|
+
|
|
535
|
+
await expect(callPromise).rejects.toThrow(McpTransportError);
|
|
536
|
+
try {
|
|
537
|
+
await callPromise;
|
|
538
|
+
} catch (err) {
|
|
539
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
540
|
+
expect((err as McpTransportError).message).toContain('exited');
|
|
541
|
+
expect((err as Error & { code?: string }).code).toBe('ECONNREFUSED');
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('rejects with timeout on failed handshake (5 second timeout)', async () => {
|
|
546
|
+
const proc = createMockProcess();
|
|
547
|
+
mockSpawn.mockReturnValue(proc);
|
|
548
|
+
|
|
549
|
+
const transport = new StdioMcpTransport(makeStdioConfig(), makeLogger());
|
|
550
|
+
// Don't send any response — let it time out
|
|
551
|
+
// We'll use a much shorter timeout for the test by checking the error
|
|
552
|
+
|
|
553
|
+
// We can't wait 5 real seconds, so we'll use fake timers
|
|
554
|
+
vi.useFakeTimers();
|
|
555
|
+
const connectPromise = transport.connect();
|
|
556
|
+
|
|
557
|
+
// Advance past the 5-second timeout
|
|
558
|
+
vi.advanceTimersByTime(5100);
|
|
559
|
+
|
|
560
|
+
await expect(connectPromise).rejects.toThrow(McpTransportError);
|
|
561
|
+
try {
|
|
562
|
+
await connectPromise;
|
|
563
|
+
} catch (err) {
|
|
564
|
+
expect(err).toBeInstanceOf(McpTransportError);
|
|
565
|
+
expect((err as McpTransportError).message).toContain('timed out');
|
|
566
|
+
expect((err as McpTransportError).message).toContain('5 seconds');
|
|
567
|
+
expect((err as Error & { code?: string }).code).toBe('ETIMEDOUT');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
vi.useRealTimers();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('throws when callTool is called before connect', async () => {
|
|
574
|
+
const transport = new StdioMcpTransport(makeStdioConfig(), makeLogger());
|
|
575
|
+
|
|
576
|
+
await expect(transport.callTool('tool', {}, 5000)).rejects.toThrow(McpTransportError);
|
|
577
|
+
try {
|
|
578
|
+
await transport.callTool('tool', {}, 5000);
|
|
579
|
+
} catch (err) {
|
|
580
|
+
expect((err as McpTransportError).message).toContain('not connected');
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('rejects pending request on callTool timeout', async () => {
|
|
585
|
+
const proc = createMockProcess();
|
|
586
|
+
mockSpawn.mockReturnValue(proc);
|
|
587
|
+
|
|
588
|
+
const transport = new StdioMcpTransport(makeStdioConfig(), makeLogger());
|
|
589
|
+
const connectPromise = transport.connect();
|
|
590
|
+
setTimeout(() => {
|
|
591
|
+
proc._stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} }) + '\n');
|
|
592
|
+
}, 10);
|
|
593
|
+
await connectPromise;
|
|
594
|
+
|
|
595
|
+
// callTool with very short timeout — process never responds
|
|
596
|
+
vi.useFakeTimers();
|
|
597
|
+
const callPromise = transport.callTool('slow-tool', {}, 100);
|
|
598
|
+
vi.advanceTimersByTime(150);
|
|
599
|
+
|
|
600
|
+
await expect(callPromise).rejects.toThrow(McpTransportError);
|
|
601
|
+
try {
|
|
602
|
+
await callPromise;
|
|
603
|
+
} catch (err) {
|
|
604
|
+
expect((err as McpTransportError).message).toContain('timed out');
|
|
605
|
+
expect((err as Error & { code?: string }).code).toBe('ETIMEDOUT');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
vi.useRealTimers();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('disconnect rejects pending and kills subprocess', async () => {
|
|
612
|
+
const proc = createMockProcess();
|
|
613
|
+
mockSpawn.mockReturnValue(proc);
|
|
614
|
+
|
|
615
|
+
const transport = new StdioMcpTransport(makeStdioConfig(), makeLogger());
|
|
616
|
+
const connectPromise = transport.connect();
|
|
617
|
+
setTimeout(() => {
|
|
618
|
+
proc._stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} }) + '\n');
|
|
619
|
+
}, 10);
|
|
620
|
+
await connectPromise;
|
|
621
|
+
|
|
622
|
+
expect(transport.isConnected()).toBe(true);
|
|
623
|
+
|
|
624
|
+
await transport.disconnect();
|
|
625
|
+
|
|
626
|
+
expect(transport.isConnected()).toBe(false);
|
|
627
|
+
expect((proc as unknown as { kill: ReturnType<typeof vi.fn> }).kill).toHaveBeenCalledWith(
|
|
628
|
+
'SIGTERM',
|
|
629
|
+
);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('passes env variables to subprocess', async () => {
|
|
633
|
+
const proc = createMockProcess();
|
|
634
|
+
mockSpawn.mockReturnValue(proc);
|
|
635
|
+
|
|
636
|
+
const config = makeStdioConfig({
|
|
637
|
+
env: { MY_VAR: 'test_value' },
|
|
638
|
+
});
|
|
639
|
+
const transport = new StdioMcpTransport(config, makeLogger());
|
|
640
|
+
const connectPromise = transport.connect();
|
|
641
|
+
setTimeout(() => {
|
|
642
|
+
proc._stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} }) + '\n');
|
|
643
|
+
}, 10);
|
|
644
|
+
await connectPromise;
|
|
645
|
+
|
|
646
|
+
const spawnCall = mockSpawn.mock.calls[0];
|
|
647
|
+
const spawnOptions = spawnCall[2] as { env: Record<string, string> };
|
|
648
|
+
expect(spawnOptions.env).toHaveProperty('MY_VAR', 'test_value');
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('skips non-JSON stdout lines without error', async () => {
|
|
652
|
+
const proc = createMockProcess();
|
|
653
|
+
mockSpawn.mockReturnValue(proc);
|
|
654
|
+
const logger = makeLogger();
|
|
655
|
+
|
|
656
|
+
const transport = new StdioMcpTransport(makeStdioConfig(), logger);
|
|
657
|
+
const connectPromise = transport.connect();
|
|
658
|
+
|
|
659
|
+
// Send garbage then the real response
|
|
660
|
+
proc._stdout.write('some garbage output\n');
|
|
661
|
+
setTimeout(() => {
|
|
662
|
+
proc._stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: {} }) + '\n');
|
|
663
|
+
}, 10);
|
|
664
|
+
|
|
665
|
+
await connectPromise;
|
|
666
|
+
expect(logger.debug).toHaveBeenCalled();
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// ── createTransport() Factory Tests ─────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
describe('createTransport()', () => {
|
|
673
|
+
it('creates HttpMcpTransport for http type config', () => {
|
|
674
|
+
const transport = createTransport(makeHttpConfig(), makeLogger());
|
|
675
|
+
expect(transport).toBeInstanceOf(HttpMcpTransport);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('creates StdioMcpTransport for stdio type config', () => {
|
|
679
|
+
const transport = createTransport(makeStdioConfig(), makeLogger());
|
|
680
|
+
expect(transport).toBeInstanceOf(StdioMcpTransport);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('throws for unsupported transport type', () => {
|
|
684
|
+
const badConfig = { name: 'bad', type: 'grpc' as 'http' } as HttpServerConfig;
|
|
685
|
+
expect(() => createTransport(badConfig, makeLogger())).toThrow(
|
|
686
|
+
'Unsupported MCP transport type',
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
});
|