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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session manager tests (T043).
|
|
3
|
+
*
|
|
4
|
+
* Validates backtracking and artifact invalidation:
|
|
5
|
+
* - Moving to an earlier phase invalidates downstream artifacts
|
|
6
|
+
* - Session status is correctly updated on backtrack
|
|
7
|
+
* - Backtrack to same phase is a no-op
|
|
8
|
+
* - Cannot backtrack to a phase after the current one
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { backtrackSession } from '../../../src/sessions/sessionManager.js';
|
|
12
|
+
function createPopulatedSession() {
|
|
13
|
+
const now = new Date().toISOString();
|
|
14
|
+
return {
|
|
15
|
+
sessionId: 'bt-session',
|
|
16
|
+
schemaVersion: '1.0.0',
|
|
17
|
+
createdAt: now,
|
|
18
|
+
updatedAt: now,
|
|
19
|
+
phase: 'Plan',
|
|
20
|
+
status: 'Active',
|
|
21
|
+
participants: [],
|
|
22
|
+
artifacts: { generatedFiles: [] },
|
|
23
|
+
turns: [
|
|
24
|
+
{ phase: 'Discover', sequence: 1, role: 'user', content: 'Business info', timestamp: now },
|
|
25
|
+
{ phase: 'Discover', sequence: 2, role: 'assistant', content: 'Got it', timestamp: now },
|
|
26
|
+
{ phase: 'Ideate', sequence: 3, role: 'user', content: 'Ideas please', timestamp: now },
|
|
27
|
+
{ phase: 'Ideate', sequence: 4, role: 'assistant', content: 'Here are ideas', timestamp: now },
|
|
28
|
+
{ phase: 'Design', sequence: 5, role: 'user', content: 'Evaluate', timestamp: now },
|
|
29
|
+
{ phase: 'Design', sequence: 6, role: 'assistant', content: 'Evaluation done', timestamp: now },
|
|
30
|
+
{ phase: 'Select', sequence: 7, role: 'user', content: 'Pick one', timestamp: now },
|
|
31
|
+
{ phase: 'Select', sequence: 8, role: 'assistant', content: 'Selected', timestamp: now },
|
|
32
|
+
],
|
|
33
|
+
businessContext: {
|
|
34
|
+
businessDescription: 'Test Corp',
|
|
35
|
+
challenges: ['Growth'],
|
|
36
|
+
},
|
|
37
|
+
workflow: {
|
|
38
|
+
activities: [{ id: 'a1', name: 'Activity' }],
|
|
39
|
+
edges: [],
|
|
40
|
+
},
|
|
41
|
+
ideas: [
|
|
42
|
+
{ id: 'i1', title: 'Idea 1', description: 'First idea', workflowStepIds: ['a1'] },
|
|
43
|
+
],
|
|
44
|
+
evaluation: {
|
|
45
|
+
method: 'feasibility-value-matrix',
|
|
46
|
+
ideas: [{ ideaId: 'i1', feasibility: 8, value: 9 }],
|
|
47
|
+
},
|
|
48
|
+
selection: {
|
|
49
|
+
ideaId: 'i1',
|
|
50
|
+
selectionRationale: 'Best fit',
|
|
51
|
+
confirmedByUser: true,
|
|
52
|
+
confirmedAt: now,
|
|
53
|
+
},
|
|
54
|
+
plan: {
|
|
55
|
+
milestones: [
|
|
56
|
+
{ id: 'm1', title: 'M1', items: ['First milestone'] },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
describe('sessionManager', () => {
|
|
62
|
+
describe('backtrackSession', () => {
|
|
63
|
+
it('backtracking to Discover invalidates all data including Discover', () => {
|
|
64
|
+
const session = createPopulatedSession();
|
|
65
|
+
const result = backtrackSession(session, 'Discover');
|
|
66
|
+
expect(result.success).toBe(true);
|
|
67
|
+
expect(result.session.phase).toBe('Discover');
|
|
68
|
+
// Backtracking clears the target phase data too (will be re-run)
|
|
69
|
+
expect(result.session.businessContext).toBeUndefined();
|
|
70
|
+
expect(result.session.workflow).toBeUndefined();
|
|
71
|
+
expect(result.session.ideas).toBeUndefined();
|
|
72
|
+
expect(result.session.evaluation).toBeUndefined();
|
|
73
|
+
expect(result.session.selection).toBeUndefined();
|
|
74
|
+
expect(result.session.plan).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
it('backtracking to Ideate preserves Discover data, clears Ideate+', () => {
|
|
77
|
+
const session = createPopulatedSession();
|
|
78
|
+
const result = backtrackSession(session, 'Ideate');
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
expect(result.session.phase).toBe('Ideate');
|
|
81
|
+
// Discover data preserved
|
|
82
|
+
expect(result.session.businessContext).toBeDefined();
|
|
83
|
+
expect(result.session.workflow).toBeDefined();
|
|
84
|
+
// Ideate and downstream cleared (Ideate is the target, so it's re-run)
|
|
85
|
+
expect(result.session.ideas).toBeUndefined();
|
|
86
|
+
expect(result.session.evaluation).toBeUndefined();
|
|
87
|
+
expect(result.session.selection).toBeUndefined();
|
|
88
|
+
expect(result.session.plan).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
it('backtracking to Design clears evaluation and downstream', () => {
|
|
91
|
+
const session = createPopulatedSession();
|
|
92
|
+
const result = backtrackSession(session, 'Design');
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
expect(result.session.phase).toBe('Design');
|
|
95
|
+
// Preserve Discover + Ideate
|
|
96
|
+
expect(result.session.businessContext).toBeDefined();
|
|
97
|
+
expect(result.session.ideas).toBeDefined();
|
|
98
|
+
// Design target + downstream cleared
|
|
99
|
+
expect(result.session.evaluation).toBeUndefined();
|
|
100
|
+
expect(result.session.selection).toBeUndefined();
|
|
101
|
+
expect(result.session.plan).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
it('backtracking to Select preserves evaluation', () => {
|
|
104
|
+
const session = createPopulatedSession();
|
|
105
|
+
const result = backtrackSession(session, 'Select');
|
|
106
|
+
expect(result.success).toBe(true);
|
|
107
|
+
expect(result.session.phase).toBe('Select');
|
|
108
|
+
expect(result.session.evaluation).toBeDefined();
|
|
109
|
+
expect(result.session.selection).toBeUndefined();
|
|
110
|
+
expect(result.session.plan).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
it('backtracking to same phase is a no-op', () => {
|
|
113
|
+
const session = createPopulatedSession();
|
|
114
|
+
const result = backtrackSession(session, 'Plan');
|
|
115
|
+
expect(result.success).toBe(true);
|
|
116
|
+
expect(result.session.phase).toBe('Plan');
|
|
117
|
+
// All data preserved
|
|
118
|
+
expect(result.session.plan).toBeDefined();
|
|
119
|
+
expect(result.session.selection).toBeDefined();
|
|
120
|
+
expect(result.invalidatedPhases).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
it('backtracking forward fails', () => {
|
|
123
|
+
const session = createPopulatedSession();
|
|
124
|
+
session.phase = 'Ideate';
|
|
125
|
+
const result = backtrackSession(session, 'Plan');
|
|
126
|
+
expect(result.success).toBe(false);
|
|
127
|
+
expect(result.error).toContain('Cannot backtrack forward');
|
|
128
|
+
});
|
|
129
|
+
it('removes turns from invalidated phases', () => {
|
|
130
|
+
const session = createPopulatedSession();
|
|
131
|
+
const result = backtrackSession(session, 'Ideate');
|
|
132
|
+
// Only Discover turns should remain
|
|
133
|
+
const turns = result.session.turns;
|
|
134
|
+
expect(turns.every(t => t.phase === 'Discover')).toBe(true);
|
|
135
|
+
expect(turns.length).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
it('updates session status and timestamp on backtrack', () => {
|
|
138
|
+
const session = createPopulatedSession();
|
|
139
|
+
// Force an old timestamp
|
|
140
|
+
session.updatedAt = '2020-01-01T00:00:00Z';
|
|
141
|
+
const result = backtrackSession(session, 'Discover');
|
|
142
|
+
expect(result.session.status).toBe('Active');
|
|
143
|
+
expect(result.session.updatedAt).not.toBe('2020-01-01T00:00:00Z');
|
|
144
|
+
});
|
|
145
|
+
it('reports which phases were invalidated', () => {
|
|
146
|
+
const session = createPopulatedSession();
|
|
147
|
+
const result = backtrackSession(session, 'Ideate');
|
|
148
|
+
expect(result.invalidatedPhases).toEqual(expect.arrayContaining(['Ideate', 'Design', 'Select', 'Plan']));
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for session persistence adapter.
|
|
3
|
+
*
|
|
4
|
+
* Contract: .sofia/sessions/<sessionId>.json
|
|
5
|
+
* - Atomic write (write-then-rename)
|
|
6
|
+
* - Persists after every turn
|
|
7
|
+
* - Preserves unknown fields
|
|
8
|
+
* - Never persists secrets
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { mkdtemp, rm, readdir, readFile } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { SessionStore } from '../../../src/sessions/sessionStore.js';
|
|
15
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
|
+
function minimalSession(id = 'sess-001') {
|
|
17
|
+
return {
|
|
18
|
+
sessionId: id,
|
|
19
|
+
schemaVersion: '1',
|
|
20
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
21
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
22
|
+
phase: 'Discover',
|
|
23
|
+
status: 'Active',
|
|
24
|
+
participants: [],
|
|
25
|
+
artifacts: { generatedFiles: [] },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
29
|
+
describe('SessionStore', () => {
|
|
30
|
+
let tmpDir;
|
|
31
|
+
let store;
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'sofia-test-'));
|
|
34
|
+
store = new SessionStore(tmpDir);
|
|
35
|
+
});
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
it('saves and loads a session', async () => {
|
|
40
|
+
const session = minimalSession();
|
|
41
|
+
await store.save(session);
|
|
42
|
+
const loaded = await store.load('sess-001');
|
|
43
|
+
expect(loaded.sessionId).toBe('sess-001');
|
|
44
|
+
expect(loaded.phase).toBe('Discover');
|
|
45
|
+
});
|
|
46
|
+
it('creates the sessions directory on first save', async () => {
|
|
47
|
+
const nestedDir = join(tmpDir, 'nested', 'sessions');
|
|
48
|
+
const nestedStore = new SessionStore(nestedDir);
|
|
49
|
+
await nestedStore.save(minimalSession());
|
|
50
|
+
const files = await readdir(nestedDir);
|
|
51
|
+
expect(files).toContain('sess-001.json');
|
|
52
|
+
});
|
|
53
|
+
it('overwrites existing session file on re-save', async () => {
|
|
54
|
+
const session = minimalSession();
|
|
55
|
+
await store.save(session);
|
|
56
|
+
const updated = { ...session, updatedAt: '2026-06-01T00:00:00Z', phase: 'Ideate' };
|
|
57
|
+
await store.save(updated);
|
|
58
|
+
const loaded = await store.load('sess-001');
|
|
59
|
+
expect(loaded.phase).toBe('Ideate');
|
|
60
|
+
expect(loaded.updatedAt).toBe('2026-06-01T00:00:00Z');
|
|
61
|
+
});
|
|
62
|
+
it('preserves unknown fields (forward compatibility)', async () => {
|
|
63
|
+
const session = minimalSession();
|
|
64
|
+
session.futureField = 'hello-future';
|
|
65
|
+
await store.save(session);
|
|
66
|
+
const loaded = await store.load('sess-001');
|
|
67
|
+
expect(loaded.futureField).toBe('hello-future');
|
|
68
|
+
});
|
|
69
|
+
it('throws when loading a non-existent session', async () => {
|
|
70
|
+
await expect(store.load('nonexistent')).rejects.toThrow();
|
|
71
|
+
});
|
|
72
|
+
it('lists sessions', async () => {
|
|
73
|
+
await store.save(minimalSession('a'));
|
|
74
|
+
await store.save(minimalSession('b'));
|
|
75
|
+
const ids = await store.list();
|
|
76
|
+
expect(ids.sort()).toEqual(['a', 'b']);
|
|
77
|
+
});
|
|
78
|
+
it('returns empty list when no sessions exist', async () => {
|
|
79
|
+
const ids = await store.list();
|
|
80
|
+
expect(ids).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
it('checks existence of a session', async () => {
|
|
83
|
+
await store.save(minimalSession());
|
|
84
|
+
expect(await store.exists('sess-001')).toBe(true);
|
|
85
|
+
expect(await store.exists('nope')).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it('writes valid JSON that can be parsed independently', async () => {
|
|
88
|
+
await store.save(minimalSession());
|
|
89
|
+
const filePath = join(tmpDir, 'sess-001.json');
|
|
90
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
expect(parsed.sessionId).toBe('sess-001');
|
|
93
|
+
});
|
|
94
|
+
it('deletes a session', async () => {
|
|
95
|
+
await store.save(minimalSession());
|
|
96
|
+
expect(await store.exists('sess-001')).toBe(true);
|
|
97
|
+
await store.delete('sess-001');
|
|
98
|
+
expect(await store.exists('sess-001')).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
it('handles concurrent saves to different sessions', async () => {
|
|
101
|
+
await Promise.all([
|
|
102
|
+
store.save(minimalSession('c1')),
|
|
103
|
+
store.save(minimalSession('c2')),
|
|
104
|
+
store.save(minimalSession('c3')),
|
|
105
|
+
]);
|
|
106
|
+
const ids = await store.list();
|
|
107
|
+
expect(ids.sort()).toEqual(['c1', 'c2', 'c3']);
|
|
108
|
+
});
|
|
109
|
+
it('rejects session with invalid schema on load', async () => {
|
|
110
|
+
// Write an invalid file directly
|
|
111
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
112
|
+
await mkdir(tmpDir, { recursive: true });
|
|
113
|
+
await writeFile(join(tmpDir, 'bad.json'), JSON.stringify({ invalid: true }));
|
|
114
|
+
await expect(store.load('bad')).rejects.toThrow();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ActivitySpinner module (T084).
|
|
3
|
+
*
|
|
4
|
+
* Verifies spinner lifecycle methods: startThinking, startToolCall,
|
|
5
|
+
* completeToolCall, stop, isActive, TTY/JSON suppression, and
|
|
6
|
+
* that ora is configured with discardStdin: false to avoid
|
|
7
|
+
* conflicting with the app's readline on process.stdin.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
+
import { Writable } from 'node:stream';
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { ActivitySpinner, createNoOpSpinner } from '../../../src/shared/activitySpinner.js';
|
|
15
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
16
|
+
/** Create a writable stream that captures output. */
|
|
17
|
+
function createCaptureStream() {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
const stream = new Writable({
|
|
20
|
+
write(chunk, _encoding, callback) {
|
|
21
|
+
chunks.push(chunk.toString());
|
|
22
|
+
callback();
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
stream.getOutput = () => chunks.join('');
|
|
26
|
+
return stream;
|
|
27
|
+
}
|
|
28
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
29
|
+
describe('ActivitySpinner (T084)', () => {
|
|
30
|
+
describe('TTY mode (enabled)', () => {
|
|
31
|
+
let stream;
|
|
32
|
+
let spinner;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
stream = createCaptureStream();
|
|
35
|
+
spinner = new ActivitySpinner({
|
|
36
|
+
isTTY: true,
|
|
37
|
+
isJsonMode: false,
|
|
38
|
+
stream,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it('startThinking() starts a spinner', () => {
|
|
42
|
+
spinner.startThinking();
|
|
43
|
+
expect(spinner.isActive()).toBe(true);
|
|
44
|
+
spinner.stop();
|
|
45
|
+
});
|
|
46
|
+
it('startToolCall() starts/updates spinner with tool name', () => {
|
|
47
|
+
spinner.startToolCall('WorkIQ');
|
|
48
|
+
expect(spinner.isActive()).toBe(true);
|
|
49
|
+
spinner.stop();
|
|
50
|
+
});
|
|
51
|
+
it('startThinking() then startToolCall() transitions spinner text', () => {
|
|
52
|
+
spinner.startThinking();
|
|
53
|
+
expect(spinner.isActive()).toBe(true);
|
|
54
|
+
spinner.startToolCall('Context7');
|
|
55
|
+
expect(spinner.isActive()).toBe(true);
|
|
56
|
+
spinner.stop();
|
|
57
|
+
});
|
|
58
|
+
it('completeToolCall() stops spinner and prints summary', () => {
|
|
59
|
+
spinner.startToolCall('WorkIQ');
|
|
60
|
+
spinner.completeToolCall('WorkIQ', 'Found 12 processes');
|
|
61
|
+
expect(spinner.isActive()).toBe(false);
|
|
62
|
+
// Summary output is now handled by io.writeToolSummary(),
|
|
63
|
+
// not by the spinner itself.
|
|
64
|
+
});
|
|
65
|
+
it('stop() clears any active spinner', () => {
|
|
66
|
+
spinner.startThinking();
|
|
67
|
+
expect(spinner.isActive()).toBe(true);
|
|
68
|
+
spinner.stop();
|
|
69
|
+
expect(spinner.isActive()).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
it('isActive() returns false when no spinner is running', () => {
|
|
72
|
+
expect(spinner.isActive()).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
it('stop() is safe to call when no spinner is active', () => {
|
|
75
|
+
expect(() => spinner.stop()).not.toThrow();
|
|
76
|
+
});
|
|
77
|
+
it('completeToolCall() works even if spinner was already stopped', () => {
|
|
78
|
+
spinner.completeToolCall('GitHub', '3 repos found');
|
|
79
|
+
// Should not throw; summary output handled by io.writeToolSummary()
|
|
80
|
+
});
|
|
81
|
+
it('handles multiple sequential tool calls', () => {
|
|
82
|
+
// First tool
|
|
83
|
+
spinner.startToolCall('WorkIQ');
|
|
84
|
+
spinner.completeToolCall('WorkIQ', 'Found 5 processes');
|
|
85
|
+
// Second tool
|
|
86
|
+
spinner.startToolCall('Context7');
|
|
87
|
+
spinner.completeToolCall('Context7', '12 docs retrieved');
|
|
88
|
+
// Spinner should be inactive after all tools complete
|
|
89
|
+
expect(spinner.isActive()).toBe(false);
|
|
90
|
+
// Summary output handled by io.writeToolSummary(), not the spinner
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('non-TTY mode (disabled)', () => {
|
|
94
|
+
let stream;
|
|
95
|
+
let spinner;
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
stream = createCaptureStream();
|
|
98
|
+
spinner = new ActivitySpinner({
|
|
99
|
+
isTTY: false,
|
|
100
|
+
isJsonMode: false,
|
|
101
|
+
stream,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it('startThinking() is a no-op', () => {
|
|
105
|
+
spinner.startThinking();
|
|
106
|
+
expect(spinner.isActive()).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it('startToolCall() is a no-op', () => {
|
|
109
|
+
spinner.startToolCall('WorkIQ');
|
|
110
|
+
expect(spinner.isActive()).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
it('completeToolCall() is a no-op (no output)', () => {
|
|
113
|
+
spinner.completeToolCall('WorkIQ', 'Found stuff');
|
|
114
|
+
expect(stream.getOutput()).toBe('');
|
|
115
|
+
});
|
|
116
|
+
it('stop() is safe and a no-op', () => {
|
|
117
|
+
expect(() => spinner.stop()).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('JSON mode (disabled)', () => {
|
|
121
|
+
let stream;
|
|
122
|
+
let spinner;
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
stream = createCaptureStream();
|
|
125
|
+
spinner = new ActivitySpinner({
|
|
126
|
+
isTTY: true,
|
|
127
|
+
isJsonMode: true,
|
|
128
|
+
stream,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
it('all operations are no-ops in JSON mode', () => {
|
|
132
|
+
spinner.startThinking();
|
|
133
|
+
expect(spinner.isActive()).toBe(false);
|
|
134
|
+
spinner.startToolCall('WorkIQ');
|
|
135
|
+
expect(spinner.isActive()).toBe(false);
|
|
136
|
+
spinner.completeToolCall('WorkIQ', 'data');
|
|
137
|
+
expect(stream.getOutput()).toBe('');
|
|
138
|
+
spinner.stop();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe('createNoOpSpinner', () => {
|
|
142
|
+
it('returns a spinner where all operations are no-ops', () => {
|
|
143
|
+
const noop = createNoOpSpinner();
|
|
144
|
+
expect(noop.isActive()).toBe(false);
|
|
145
|
+
noop.startThinking();
|
|
146
|
+
expect(noop.isActive()).toBe(false);
|
|
147
|
+
noop.startToolCall('TestTool');
|
|
148
|
+
expect(noop.isActive()).toBe(false);
|
|
149
|
+
noop.stop();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('ora configuration', () => {
|
|
153
|
+
it('creates ora with discardStdin: false to avoid stdin conflicts', () => {
|
|
154
|
+
const stream = createCaptureStream();
|
|
155
|
+
const spinner = new ActivitySpinner({ isTTY: true, isJsonMode: false, stream });
|
|
156
|
+
// Start thinking to trigger ora creation
|
|
157
|
+
spinner.startThinking();
|
|
158
|
+
// Access the internal ora instance to verify the option.
|
|
159
|
+
// ora stores options in a private field, but we can verify behaviour
|
|
160
|
+
// by checking that the spinner was created (isActive) and that our
|
|
161
|
+
// code explicitly passes discardStdin: false in the source.
|
|
162
|
+
expect(spinner.isActive()).toBe(true);
|
|
163
|
+
// Verify via source inspection: read the activitySpinner source and
|
|
164
|
+
// confirm discardStdin: false is present in all ora() calls
|
|
165
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
166
|
+
const source = fs.readFileSync(path.resolve(__dirname, '../../../src/shared/activitySpinner.ts'), 'utf8');
|
|
167
|
+
// Count ora constructor calls vs discardStdin: false occurrences
|
|
168
|
+
const oraCallCount = (source.match(/ora\(\{/g) || []).length;
|
|
169
|
+
const discardFalseCount = (source.match(/discardStdin:\s*false/g) || []).length;
|
|
170
|
+
expect(oraCallCount).toBeGreaterThan(0);
|
|
171
|
+
expect(discardFalseCount).toBe(oraCallCount);
|
|
172
|
+
spinner.stop();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cards loader tests.
|
|
3
|
+
*
|
|
4
|
+
* Validates that the AI Discovery Cards dataset:
|
|
5
|
+
* - Loads and validates against the Zod schema
|
|
6
|
+
* - Filters cards by category
|
|
7
|
+
* - Searches cards by keyword
|
|
8
|
+
* - Caches the dataset after first load
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { loadCardsDataset, getCardsByCategory, searchCards } from '../../../src/shared/data/cardsLoader.js';
|
|
12
|
+
describe('cardsLoader', () => {
|
|
13
|
+
it('loads the cards dataset successfully', async () => {
|
|
14
|
+
const dataset = await loadCardsDataset();
|
|
15
|
+
expect(dataset).toBeDefined();
|
|
16
|
+
expect(dataset.categories).toBeInstanceOf(Array);
|
|
17
|
+
expect(dataset.categories.length).toBeGreaterThan(0);
|
|
18
|
+
expect(dataset.cards).toBeInstanceOf(Array);
|
|
19
|
+
expect(dataset.cards.length).toBeGreaterThan(0);
|
|
20
|
+
});
|
|
21
|
+
it('cards have required fields', async () => {
|
|
22
|
+
const dataset = await loadCardsDataset();
|
|
23
|
+
for (const card of dataset.cards) {
|
|
24
|
+
expect(card.cardId).toBeDefined();
|
|
25
|
+
expect(card.category).toBeDefined();
|
|
26
|
+
expect(card.title).toBeDefined();
|
|
27
|
+
expect(card.description).toBeDefined();
|
|
28
|
+
expect(card.typicalScenarios).toBeInstanceOf(Array);
|
|
29
|
+
expect(card.azureServices).toBeInstanceOf(Array);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
it('categories are non-empty strings', async () => {
|
|
33
|
+
const dataset = await loadCardsDataset();
|
|
34
|
+
for (const cat of dataset.categories) {
|
|
35
|
+
expect(typeof cat).toBe('string');
|
|
36
|
+
expect(cat.length).toBeGreaterThan(0);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
it('filters cards by category', async () => {
|
|
40
|
+
const dataset = await loadCardsDataset();
|
|
41
|
+
const firstCategory = dataset.categories[0];
|
|
42
|
+
const filtered = await getCardsByCategory(firstCategory);
|
|
43
|
+
expect(filtered.length).toBeGreaterThan(0);
|
|
44
|
+
expect(filtered.every(c => c.category === firstCategory)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('returns empty array for unknown category', async () => {
|
|
47
|
+
const filtered = await getCardsByCategory('NonexistentCategory123');
|
|
48
|
+
expect(filtered).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
it('searches cards by keyword in title', async () => {
|
|
51
|
+
const dataset = await loadCardsDataset();
|
|
52
|
+
const firstCard = dataset.cards[0];
|
|
53
|
+
// Search for a word from the first card's title
|
|
54
|
+
const keyword = firstCard.title.split(' ')[0];
|
|
55
|
+
const results = await searchCards(keyword);
|
|
56
|
+
expect(results.length).toBeGreaterThan(0);
|
|
57
|
+
expect(results.some(c => c.cardId === firstCard.cardId)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
it('searches cards case-insensitively', async () => {
|
|
60
|
+
const dataset = await loadCardsDataset();
|
|
61
|
+
const firstCard = dataset.cards[0];
|
|
62
|
+
const keyword = firstCard.title.split(' ')[0].toLowerCase();
|
|
63
|
+
const results = await searchCards(keyword);
|
|
64
|
+
expect(results.length).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
it('returns empty for unmatched search', async () => {
|
|
67
|
+
const results = await searchCards('xyzzy_nonexistent_query_99999');
|
|
68
|
+
expect(results).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
it('caches dataset on subsequent loads', async () => {
|
|
71
|
+
const first = await loadCardsDataset();
|
|
72
|
+
const second = await loadCardsDataset();
|
|
73
|
+
// Same object reference (cached)
|
|
74
|
+
expect(first).toBe(second);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for CopilotClient abstraction.
|
|
3
|
+
*
|
|
4
|
+
* T050: SDK mcpServers forwarding
|
|
5
|
+
* T052: SDK hooks integration
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
8
|
+
import { createFakeCopilotClient } from '../../../src/shared/copilotClient.js';
|
|
9
|
+
describe('CopilotClient', () => {
|
|
10
|
+
it('createFakeCopilotClient returns a client', () => {
|
|
11
|
+
const client = createFakeCopilotClient([{ role: 'assistant', content: 'Hello!' }]);
|
|
12
|
+
expect(client).toBeDefined();
|
|
13
|
+
expect(typeof client.createSession).toBe('function');
|
|
14
|
+
});
|
|
15
|
+
it('fake client session sends messages and gets responses', async () => {
|
|
16
|
+
const client = createFakeCopilotClient([{ role: 'assistant', content: 'I understand.' }]);
|
|
17
|
+
const session = await client.createSession({ systemPrompt: 'You are a facilitator.' });
|
|
18
|
+
const events = [];
|
|
19
|
+
for await (const event of session.send({ role: 'user', content: 'Tell me about AI' })) {
|
|
20
|
+
if (event.type === 'TextDelta') {
|
|
21
|
+
events.push(event.text);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
expect(events.join('')).toBe('I understand.');
|
|
25
|
+
});
|
|
26
|
+
it('fake client supports multi-turn conversation', async () => {
|
|
27
|
+
const client = createFakeCopilotClient([
|
|
28
|
+
{ role: 'assistant', content: 'First response' },
|
|
29
|
+
{ role: 'assistant', content: 'Second response' },
|
|
30
|
+
]);
|
|
31
|
+
const session = await client.createSession({ systemPrompt: 'Test' });
|
|
32
|
+
const first = [];
|
|
33
|
+
for await (const event of session.send({ role: 'user', content: 'Turn 1' })) {
|
|
34
|
+
if (event.type === 'TextDelta')
|
|
35
|
+
first.push(event.text);
|
|
36
|
+
}
|
|
37
|
+
expect(first.join('')).toBe('First response');
|
|
38
|
+
const second = [];
|
|
39
|
+
for await (const event of session.send({ role: 'user', content: 'Turn 2' })) {
|
|
40
|
+
if (event.type === 'TextDelta')
|
|
41
|
+
second.push(event.text);
|
|
42
|
+
}
|
|
43
|
+
expect(second.join('')).toBe('Second response');
|
|
44
|
+
});
|
|
45
|
+
it('fake client can simulate tool calls', async () => {
|
|
46
|
+
const client = createFakeCopilotClient([{ role: 'assistant', content: 'Let me search for that.' }], {
|
|
47
|
+
tools: [{ name: 'web.search', description: 'Search the web' }],
|
|
48
|
+
});
|
|
49
|
+
const session = await client.createSession({ systemPrompt: 'Test' });
|
|
50
|
+
expect(session).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
// ── T050: SessionOptions.mcpServers forwarding ──────────────────────────
|
|
53
|
+
describe('SessionOptions.mcpServers (T050)', () => {
|
|
54
|
+
it('accepts mcpServers as a Record and forwards to createSession', async () => {
|
|
55
|
+
const mcpServers = {
|
|
56
|
+
github: {
|
|
57
|
+
type: 'http',
|
|
58
|
+
url: 'https://api.githubcopilot.com/mcp/',
|
|
59
|
+
tools: ['search_repositories'],
|
|
60
|
+
timeout: 60_000,
|
|
61
|
+
},
|
|
62
|
+
context7: {
|
|
63
|
+
type: 'stdio',
|
|
64
|
+
command: 'npx',
|
|
65
|
+
args: ['-y', '@upstash/context7-mcp'],
|
|
66
|
+
tools: ['resolve-library-id', 'query-docs'],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
// Spy on createSession to capture the options passed
|
|
70
|
+
const createSessionSpy = vi.fn();
|
|
71
|
+
const client = createFakeCopilotClient([{ role: 'assistant', content: 'OK' }]);
|
|
72
|
+
const originalCreateSession = client.createSession.bind(client);
|
|
73
|
+
client.createSession = async (opts) => {
|
|
74
|
+
createSessionSpy(opts);
|
|
75
|
+
return originalCreateSession(opts);
|
|
76
|
+
};
|
|
77
|
+
const session = await client.createSession({
|
|
78
|
+
systemPrompt: 'Test',
|
|
79
|
+
mcpServers,
|
|
80
|
+
});
|
|
81
|
+
expect(createSessionSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
82
|
+
mcpServers: expect.objectContaining({
|
|
83
|
+
github: expect.objectContaining({
|
|
84
|
+
type: 'http',
|
|
85
|
+
url: 'https://api.githubcopilot.com/mcp/',
|
|
86
|
+
}),
|
|
87
|
+
context7: expect.objectContaining({ type: 'stdio', command: 'npx' }),
|
|
88
|
+
}),
|
|
89
|
+
}));
|
|
90
|
+
expect(session).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
it('does not include mcpServers when omitted', async () => {
|
|
93
|
+
const createSessionSpy = vi.fn();
|
|
94
|
+
const client = createFakeCopilotClient([{ role: 'assistant', content: 'OK' }]);
|
|
95
|
+
const originalCreateSession = client.createSession.bind(client);
|
|
96
|
+
client.createSession = async (opts) => {
|
|
97
|
+
createSessionSpy(opts);
|
|
98
|
+
return originalCreateSession(opts);
|
|
99
|
+
};
|
|
100
|
+
await client.createSession({ systemPrompt: 'Test' });
|
|
101
|
+
const passedOpts = createSessionSpy.mock.calls[0][0];
|
|
102
|
+
expect(passedOpts.mcpServers).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
it('does not include mcpServers when empty object', async () => {
|
|
105
|
+
const createSessionSpy = vi.fn();
|
|
106
|
+
const client = createFakeCopilotClient([{ role: 'assistant', content: 'OK' }]);
|
|
107
|
+
const originalCreateSession = client.createSession.bind(client);
|
|
108
|
+
client.createSession = async (opts) => {
|
|
109
|
+
createSessionSpy(opts);
|
|
110
|
+
return originalCreateSession(opts);
|
|
111
|
+
};
|
|
112
|
+
await client.createSession({ systemPrompt: 'Test', mcpServers: {} });
|
|
113
|
+
const passedOpts = createSessionSpy.mock.calls[0][0];
|
|
114
|
+
expect(passedOpts.mcpServers).toEqual({});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
// ── T052: SDK hooks integration ─────────────────────────────────────────
|
|
118
|
+
describe('SessionOptions hooks (T052)', () => {
|
|
119
|
+
it('accepts a hooks object with onPreToolUse and onPostToolUse callbacks', async () => {
|
|
120
|
+
const hooks = {
|
|
121
|
+
onPreToolUse: vi.fn(),
|
|
122
|
+
onPostToolUse: vi.fn(),
|
|
123
|
+
onErrorOccurred: vi.fn(),
|
|
124
|
+
};
|
|
125
|
+
const createSessionSpy = vi.fn();
|
|
126
|
+
const client = createFakeCopilotClient([{ role: 'assistant', content: 'OK' }]);
|
|
127
|
+
const originalCreateSession = client.createSession.bind(client);
|
|
128
|
+
client.createSession = async (opts) => {
|
|
129
|
+
createSessionSpy(opts);
|
|
130
|
+
return originalCreateSession(opts);
|
|
131
|
+
};
|
|
132
|
+
await client.createSession({
|
|
133
|
+
systemPrompt: 'Test',
|
|
134
|
+
hooks,
|
|
135
|
+
});
|
|
136
|
+
const passedOpts = createSessionSpy.mock.calls[0][0];
|
|
137
|
+
expect(passedOpts.hooks).toBeDefined();
|
|
138
|
+
expect(passedOpts.hooks.onPreToolUse).toBe(hooks.onPreToolUse);
|
|
139
|
+
expect(passedOpts.hooks.onPostToolUse).toBe(hooks.onPostToolUse);
|
|
140
|
+
expect(passedOpts.hooks.onErrorOccurred).toBe(hooks.onErrorOccurred);
|
|
141
|
+
});
|
|
142
|
+
it('does not include hooks when omitted', async () => {
|
|
143
|
+
const createSessionSpy = vi.fn();
|
|
144
|
+
const client = createFakeCopilotClient([{ role: 'assistant', content: 'OK' }]);
|
|
145
|
+
const originalCreateSession = client.createSession.bind(client);
|
|
146
|
+
client.createSession = async (opts) => {
|
|
147
|
+
createSessionSpy(opts);
|
|
148
|
+
return originalCreateSession(opts);
|
|
149
|
+
};
|
|
150
|
+
await client.createSession({ systemPrompt: 'Test' });
|
|
151
|
+
const passedOpts = createSessionSpy.mock.calls[0][0];
|
|
152
|
+
expect(passedOpts.hooks).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|