gsd-pi 2.51.0-dev.ae8f7cb → 2.52.0-dev.585e355
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/README.md +4 -4
- package/dist/headless-events.d.ts +18 -0
- package/dist/headless-events.js +36 -0
- package/dist/headless-types.d.ts +28 -0
- package/dist/headless-types.js +7 -0
- package/dist/headless.d.ts +8 -3
- package/dist/headless.js +47 -16
- package/dist/help-text.js +16 -5
- package/dist/onboarding.js +5 -4
- package/dist/remote-questions-config.js +1 -1
- package/dist/resources/extensions/async-jobs/async-bash-tool.js +29 -17
- package/dist/resources/extensions/async-jobs/job-manager.js +4 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +18 -19
- package/dist/resources/extensions/gsd/auto/phases.js +6 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +18 -0
- package/dist/resources/extensions/gsd/auto-start.js +2 -0
- package/dist/resources/extensions/gsd/auto-timers.js +24 -2
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +25 -7
- package/dist/resources/extensions/gsd/auto-worktree.js +21 -0
- package/dist/resources/extensions/gsd/auto.js +8 -4
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +105 -70
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +12 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
- package/dist/resources/extensions/gsd/claude-import.js +60 -9
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +69 -6
- package/dist/resources/extensions/gsd/commands-config.js +10 -5
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +4 -4
- package/dist/resources/extensions/gsd/detection.js +6 -6
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +5 -5
- package/dist/resources/extensions/gsd/error-classifier.js +105 -0
- package/dist/resources/extensions/gsd/git-service.js +4 -3
- package/dist/resources/extensions/gsd/gitignore.js +7 -7
- package/dist/resources/extensions/gsd/gsd-db.js +298 -45
- package/dist/resources/extensions/gsd/init-wizard.js +2 -2
- package/dist/resources/extensions/gsd/key-manager.js +7 -16
- package/dist/resources/extensions/gsd/markdown-renderer.js +5 -4
- package/dist/resources/extensions/gsd/memory-store.js +28 -13
- package/dist/resources/extensions/gsd/milestone-actions.js +19 -0
- package/dist/resources/extensions/gsd/preferences-models.js +1 -13
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/preferences.js +13 -13
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/provider-error-pause.js +0 -44
- package/dist/resources/extensions/gsd/rule-registry.js +1 -1
- package/dist/resources/extensions/gsd/service-tier.js +13 -2
- package/dist/resources/extensions/gsd/state.js +33 -19
- package/dist/resources/extensions/gsd/status-guards.js +12 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +7 -13
- package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -20
- package/dist/resources/extensions/gsd/tools/complete-task.js +11 -21
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +28 -29
- package/dist/resources/extensions/gsd/tools/plan-slice.js +27 -26
- package/dist/resources/extensions/gsd/tools/plan-task.js +23 -23
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +50 -41
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +4 -3
- package/dist/resources/extensions/gsd/tools/reopen-task.js +5 -4
- package/dist/resources/extensions/gsd/tools/replan-slice.js +51 -41
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +23 -16
- package/dist/resources/extensions/gsd/validation.js +21 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +0 -1
- package/dist/resources/extensions/remote-questions/config.js +1 -1
- package/dist/resources/extensions/remote-questions/remote-command.js +1 -1
- package/dist/resources/extensions/search-the-web/native-search.js +1 -1
- package/dist/resources/extensions/search-the-web/provider.js +1 -1
- package/dist/resources/extensions/shared/rtk.js +5 -3
- package/dist/rtk.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +2 -2
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/chunks/2229.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.21054f459af5cc78.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-cfc9a116e6450a6b.js → webpack-024d82be84800e52.js} +1 -1
- package/dist/web/standalone/.next/static/css/a58ef8a151aa0493.css +1 -0
- package/dist/wizard.js +4 -1
- package/package.json +2 -2
- package/packages/mcp-server/README.md +202 -0
- package/packages/mcp-server/package.json +36 -0
- package/packages/mcp-server/src/cli.ts +68 -0
- package/packages/mcp-server/src/index.ts +14 -0
- package/packages/mcp-server/src/mcp-server.test.ts +628 -0
- package/packages/mcp-server/src/server.ts +278 -0
- package/packages/mcp-server/src/session-manager.ts +328 -0
- package/packages/mcp-server/src/types.ts +107 -0
- package/packages/mcp-server/tsconfig.json +24 -0
- package/packages/pi-ai/dist/models.d.ts +14 -3
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.js +53 -10
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/models.test.js +102 -1
- package/packages/pi-ai/dist/models.test.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +30 -0
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/models.test.ts +114 -1
- package/packages/pi-ai/src/models.ts +70 -13
- package/packages/pi-ai/src/types.ts +31 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts +2 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +3 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.js +5 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +9 -4
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts +19 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js +83 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-spawn-windows.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +5 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +5 -3
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +0 -2
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts +28 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +49 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +114 -6
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts +9 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js +831 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-protocol-v2.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +66 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.js +0 -1
- package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +4 -0
- package/packages/pi-coding-agent/src/core/bash-executor.ts +5 -1
- package/packages/pi-coding-agent/src/core/model-registry.ts +10 -3
- package/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +101 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +5 -1
- package/packages/pi-coding-agent/src/index.ts +3 -0
- package/packages/pi-coding-agent/src/main.ts +5 -3
- package/packages/pi-coding-agent/src/modes/index.ts +8 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +0 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +54 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +124 -6
- package/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +971 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +61 -4
- package/packages/pi-coding-agent/src/utils/shell.ts +0 -1
- package/packages/rpc-client/package.json +20 -0
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +36 -8
- package/src/resources/extensions/async-jobs/async-bash-tool.ts +22 -11
- package/src/resources/extensions/async-jobs/job-manager.ts +4 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +19 -20
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +21 -0
- package/src/resources/extensions/gsd/auto/phases.ts +6 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +19 -0
- package/src/resources/extensions/gsd/auto-start.ts +2 -0
- package/src/resources/extensions/gsd/auto-timers.ts +25 -1
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +30 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +21 -0
- package/src/resources/extensions/gsd/auto.ts +10 -4
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -73
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +11 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
- package/src/resources/extensions/gsd/claude-import.ts +58 -9
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +73 -6
- package/src/resources/extensions/gsd/commands-config.ts +11 -5
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -4
- package/src/resources/extensions/gsd/detection.ts +6 -6
- package/src/resources/extensions/gsd/docs/preferences-reference.md +5 -5
- package/src/resources/extensions/gsd/error-classifier.ts +139 -0
- package/src/resources/extensions/gsd/git-service.ts +4 -3
- package/src/resources/extensions/gsd/gitignore.ts +7 -7
- package/src/resources/extensions/gsd/gsd-db.ts +355 -63
- package/src/resources/extensions/gsd/init-wizard.ts +2 -2
- package/src/resources/extensions/gsd/key-manager.ts +7 -16
- package/src/resources/extensions/gsd/markdown-renderer.ts +5 -4
- package/src/resources/extensions/gsd/memory-store.ts +29 -18
- package/src/resources/extensions/gsd/milestone-actions.ts +17 -0
- package/src/resources/extensions/gsd/preferences-models.ts +1 -13
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/preferences.ts +12 -13
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/provider-error-pause.ts +0 -57
- package/src/resources/extensions/gsd/rule-registry.ts +1 -1
- package/src/resources/extensions/gsd/service-tier.ts +14 -2
- package/src/resources/extensions/gsd/state.ts +34 -20
- package/src/resources/extensions/gsd/status-guards.ts +13 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts +191 -0
- package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/commands-config.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +35 -7
- package/src/resources/extensions/gsd/tests/detection.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/empty-db-reconciliation.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +37 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/interactive-tool-idle-exemption.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +16 -1
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +77 -70
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +42 -31
- package/src/resources/extensions/gsd/tests/token-cost-display.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/vacuous-truth-slices.test.ts +115 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/validation.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +81 -1
- package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +130 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -17
- package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -24
- package/src/resources/extensions/gsd/tools/complete-task.ts +13 -25
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +30 -32
- package/src/resources/extensions/gsd/tools/plan-slice.ts +30 -30
- package/src/resources/extensions/gsd/tools/plan-task.ts +26 -26
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +57 -46
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +4 -3
- package/src/resources/extensions/gsd/tools/reopen-task.ts +5 -4
- package/src/resources/extensions/gsd/tools/replan-slice.ts +55 -44
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +26 -20
- package/src/resources/extensions/gsd/validation.ts +23 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +0 -1
- package/src/resources/extensions/remote-questions/config.ts +1 -1
- package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
- package/src/resources/extensions/search-the-web/native-search.ts +1 -1
- package/src/resources/extensions/search-the-web/provider.ts +1 -1
- package/src/resources/extensions/shared/rtk.ts +12 -3
- package/dist/web/standalone/.next/static/chunks/4024.9ad5def014d90ce4.js +0 -9
- package/dist/web/standalone/.next/static/css/de141508b083f922.css +0 -1
- /package/dist/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
- /package/dist/web/standalone/.next/static/{5_KeZz1X0tXJK-d_4OhjB → KTe1kB5nPLQFIIFz2OcmI}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{5_KeZz1X0tXJK-d_4OhjB → KTe1kB5nPLQFIIFz2OcmI}/_ssgManifest.js +0 -0
- /package/src/resources/extensions/gsd/templates/{preferences.md → PREFERENCES.md} +0 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gsd/mcp-server — Integration and unit tests.
|
|
3
|
+
*
|
|
4
|
+
* Strategy: We cannot mock @gsd/rpc-client at the module level without
|
|
5
|
+
* --experimental-test-module-mocks. Instead we test by:
|
|
6
|
+
*
|
|
7
|
+
* 1. Subclassing SessionManager to inject a mock client factory
|
|
8
|
+
* 2. Testing event handling, state transitions, and error paths
|
|
9
|
+
* 3. Testing tool registration via createMcpServer
|
|
10
|
+
* 4. Testing CLI path resolution via static method
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import { EventEmitter } from 'node:events';
|
|
17
|
+
|
|
18
|
+
import { SessionManager } from './session-manager.js';
|
|
19
|
+
import { createMcpServer } from './server.js';
|
|
20
|
+
import { MAX_EVENTS } from './types.js';
|
|
21
|
+
import type { ManagedSession, CostAccumulator, PendingBlocker } from './types.js';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Mock RpcClient (duck-typed to match RpcClient interface)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
class MockRpcClient {
|
|
28
|
+
started = false;
|
|
29
|
+
stopped = false;
|
|
30
|
+
aborted = false;
|
|
31
|
+
prompted: string[] = [];
|
|
32
|
+
private eventListeners: Array<(event: Record<string, unknown>) => void> = [];
|
|
33
|
+
uiResponses: Array<{ requestId: string; response: Record<string, unknown> }> = [];
|
|
34
|
+
|
|
35
|
+
/** Control — set to make start() reject */
|
|
36
|
+
startError: Error | null = null;
|
|
37
|
+
/** Control — set to make init() reject */
|
|
38
|
+
initError: Error | null = null;
|
|
39
|
+
/** Control — override sessionId from init */
|
|
40
|
+
initSessionId = 'mock-session-001';
|
|
41
|
+
|
|
42
|
+
cwd: string;
|
|
43
|
+
args: string[];
|
|
44
|
+
|
|
45
|
+
constructor(options?: Record<string, unknown>) {
|
|
46
|
+
this.cwd = (options?.cwd as string) ?? '';
|
|
47
|
+
this.args = (options?.args as string[]) ?? [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async start(): Promise<void> {
|
|
51
|
+
if (this.startError) throw this.startError;
|
|
52
|
+
this.started = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async stop(): Promise<void> {
|
|
56
|
+
this.stopped = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async init(): Promise<{ sessionId: string; version: string }> {
|
|
60
|
+
if (this.initError) throw this.initError;
|
|
61
|
+
return { sessionId: this.initSessionId, version: '2.51.0' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onEvent(listener: (event: Record<string, unknown>) => void): () => void {
|
|
65
|
+
this.eventListeners.push(listener);
|
|
66
|
+
return () => {
|
|
67
|
+
const idx = this.eventListeners.indexOf(listener);
|
|
68
|
+
if (idx >= 0) this.eventListeners.splice(idx, 1);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async prompt(message: string): Promise<void> {
|
|
73
|
+
this.prompted.push(message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async abort(): Promise<void> {
|
|
77
|
+
this.aborted = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
sendUIResponse(requestId: string, response: Record<string, unknown>): void {
|
|
81
|
+
this.uiResponses.push({ requestId, response });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Test helper — emit an event to all listeners */
|
|
85
|
+
emitEvent(event: Record<string, unknown>): void {
|
|
86
|
+
for (const listener of this.eventListeners) {
|
|
87
|
+
listener(event);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// TestableSessionManager — injects mock clients without module mocking
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Subclass that overrides startSession to use MockRpcClient instead of the
|
|
98
|
+
* real RpcClient. We directly construct the session object, mirroring the
|
|
99
|
+
* parent's logic but with our mock.
|
|
100
|
+
*/
|
|
101
|
+
class TestableSessionManager extends SessionManager {
|
|
102
|
+
/** The last mock client created */
|
|
103
|
+
lastClient: MockRpcClient | null = null;
|
|
104
|
+
/** All mock clients */
|
|
105
|
+
allClients: MockRpcClient[] = [];
|
|
106
|
+
/** Counter for unique session IDs across multiple sessions */
|
|
107
|
+
private sessionCounter = 0;
|
|
108
|
+
/** Control: set to make startSession fail during init */
|
|
109
|
+
nextInitError: Error | null = null;
|
|
110
|
+
/** Control: set to make startSession fail during start */
|
|
111
|
+
nextStartError: Error | null = null;
|
|
112
|
+
|
|
113
|
+
override async startSession(projectDir: string, options: { cliPath?: string; command?: string; model?: string; bare?: boolean } = {}): Promise<string> {
|
|
114
|
+
if (!projectDir || projectDir.trim() === '') {
|
|
115
|
+
throw new Error('projectDir is required and cannot be empty');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const resolvedDir = resolve(projectDir);
|
|
119
|
+
|
|
120
|
+
// Check duplicate via getSessionByDir
|
|
121
|
+
const existing = this.getSessionByDir(resolvedDir);
|
|
122
|
+
if (existing) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Session already active for ${resolvedDir} (sessionId: ${existing.sessionId}, status: ${existing.status})`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const client = new MockRpcClient({ cwd: resolvedDir, args: [] });
|
|
129
|
+
if (this.nextStartError) {
|
|
130
|
+
client.startError = this.nextStartError;
|
|
131
|
+
this.nextStartError = null;
|
|
132
|
+
}
|
|
133
|
+
if (this.nextInitError) {
|
|
134
|
+
client.initError = this.nextInitError;
|
|
135
|
+
this.nextInitError = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.sessionCounter++;
|
|
139
|
+
client.initSessionId = `mock-session-${String(this.sessionCounter).padStart(3, '0')}`;
|
|
140
|
+
this.lastClient = client;
|
|
141
|
+
this.allClients.push(client);
|
|
142
|
+
|
|
143
|
+
// Create the session shell
|
|
144
|
+
const session: ManagedSession = {
|
|
145
|
+
sessionId: '',
|
|
146
|
+
projectDir: resolvedDir,
|
|
147
|
+
status: 'starting',
|
|
148
|
+
client: client as any, // duck-typed mock
|
|
149
|
+
events: [],
|
|
150
|
+
pendingBlocker: null,
|
|
151
|
+
cost: { totalCost: 0, tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
|
|
152
|
+
startTime: Date.now(),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Insert into internal sessions map — access via protected method
|
|
156
|
+
this._putSession(resolvedDir, session);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await client.start();
|
|
160
|
+
|
|
161
|
+
const initResult = await client.init();
|
|
162
|
+
session.sessionId = initResult.sessionId;
|
|
163
|
+
session.status = 'running';
|
|
164
|
+
|
|
165
|
+
// Wire event tracking using the same handleEvent logic as parent
|
|
166
|
+
session.unsubscribe = client.onEvent((event: Record<string, unknown>) => {
|
|
167
|
+
this._handleEvent(session, event);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Kick off auto-mode
|
|
171
|
+
const command = options.command ?? '/gsd auto';
|
|
172
|
+
await client.prompt(command);
|
|
173
|
+
|
|
174
|
+
return session.sessionId;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
session.status = 'error';
|
|
177
|
+
session.error = err instanceof Error ? err.message : String(err);
|
|
178
|
+
try { await client.stop(); } catch { /* swallow */ }
|
|
179
|
+
throw new Error(`Failed to start session for ${resolvedDir}: ${session.error}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Expose internal session map insertion for testing */
|
|
184
|
+
_putSession(key: string, session: ManagedSession): void {
|
|
185
|
+
// Access the private sessions map via any cast
|
|
186
|
+
(this as any).sessions.set(key, session);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Expose handleEvent for testing */
|
|
190
|
+
_handleEvent(session: ManagedSession, event: Record<string, unknown>): void {
|
|
191
|
+
(this as any).handleEvent(session, event);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Test helpers
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
let allManagers: TestableSessionManager[] = [];
|
|
200
|
+
|
|
201
|
+
function createManager(): TestableSessionManager {
|
|
202
|
+
const mgr = new TestableSessionManager();
|
|
203
|
+
allManagers.push(mgr);
|
|
204
|
+
return mgr;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// SessionManager unit tests
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
describe('SessionManager', () => {
|
|
212
|
+
let sm: TestableSessionManager;
|
|
213
|
+
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
sm = createManager();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
afterEach(async () => {
|
|
219
|
+
for (const mgr of allManagers) {
|
|
220
|
+
await mgr.cleanup();
|
|
221
|
+
}
|
|
222
|
+
allManagers = [];
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('startSession creates session and returns sessionId', async () => {
|
|
226
|
+
const sessionId = await sm.startSession('/tmp/test-project', { cliPath: '/usr/bin/gsd' });
|
|
227
|
+
assert.equal(sessionId, 'mock-session-001');
|
|
228
|
+
|
|
229
|
+
const session = sm.getSession(sessionId);
|
|
230
|
+
assert.ok(session);
|
|
231
|
+
assert.equal(session.status, 'running');
|
|
232
|
+
assert.equal(session.projectDir, resolve('/tmp/test-project'));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('startSession sends /gsd auto by default', async () => {
|
|
236
|
+
await sm.startSession('/tmp/test-prompt', { cliPath: '/usr/bin/gsd' });
|
|
237
|
+
assert.ok(sm.lastClient);
|
|
238
|
+
assert.deepEqual(sm.lastClient.prompted, ['/gsd auto']);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('startSession sends custom command when provided', async () => {
|
|
242
|
+
await sm.startSession('/tmp/test-cmd', { cliPath: '/usr/bin/gsd', command: '/gsd auto --resume' });
|
|
243
|
+
assert.ok(sm.lastClient);
|
|
244
|
+
assert.deepEqual(sm.lastClient.prompted, ['/gsd auto --resume']);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('startSession rejects duplicate projectDir', async () => {
|
|
248
|
+
await sm.startSession('/tmp/dup-test', { cliPath: '/usr/bin/gsd' });
|
|
249
|
+
await assert.rejects(
|
|
250
|
+
() => sm.startSession('/tmp/dup-test', { cliPath: '/usr/bin/gsd' }),
|
|
251
|
+
(err: Error) => {
|
|
252
|
+
assert.ok(err.message.includes('Session already active'));
|
|
253
|
+
return true;
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('startSession rejects empty projectDir', async () => {
|
|
259
|
+
await assert.rejects(
|
|
260
|
+
() => sm.startSession('', { cliPath: '/usr/bin/gsd' }),
|
|
261
|
+
(err: Error) => {
|
|
262
|
+
assert.ok(err.message.includes('projectDir is required'));
|
|
263
|
+
return true;
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('startSession sets error status on start() failure', async () => {
|
|
269
|
+
sm.nextStartError = new Error('spawn failed');
|
|
270
|
+
|
|
271
|
+
await assert.rejects(
|
|
272
|
+
() => sm.startSession('/tmp/fail-start', { cliPath: '/usr/bin/gsd' }),
|
|
273
|
+
(err: Error) => {
|
|
274
|
+
assert.ok(err.message.includes('Failed to start session'));
|
|
275
|
+
assert.ok(err.message.includes('spawn failed'));
|
|
276
|
+
return true;
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('startSession sets error status on init() failure', async () => {
|
|
282
|
+
sm.nextInitError = new Error('handshake failed');
|
|
283
|
+
|
|
284
|
+
await assert.rejects(
|
|
285
|
+
() => sm.startSession('/tmp/fail-init', { cliPath: '/usr/bin/gsd' }),
|
|
286
|
+
(err: Error) => {
|
|
287
|
+
assert.ok(err.message.includes('Failed to start session'));
|
|
288
|
+
assert.ok(err.message.includes('handshake failed'));
|
|
289
|
+
return true;
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('getSession returns undefined for unknown sessionId', () => {
|
|
295
|
+
const result = sm.getSession('nonexistent-id');
|
|
296
|
+
assert.equal(result, undefined);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('getSessionByDir returns session for known dir', async () => {
|
|
300
|
+
await sm.startSession('/tmp/by-dir', { cliPath: '/usr/bin/gsd' });
|
|
301
|
+
const session = sm.getSessionByDir('/tmp/by-dir');
|
|
302
|
+
assert.ok(session);
|
|
303
|
+
assert.equal(session.sessionId, 'mock-session-001');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('resolveBlocker errors when no pending blocker', async () => {
|
|
307
|
+
const sessionId = await sm.startSession('/tmp/no-blocker', { cliPath: '/usr/bin/gsd' });
|
|
308
|
+
await assert.rejects(
|
|
309
|
+
() => sm.resolveBlocker(sessionId, 'some response'),
|
|
310
|
+
(err: Error) => {
|
|
311
|
+
assert.ok(err.message.includes('No pending blocker'));
|
|
312
|
+
return true;
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('resolveBlocker errors for unknown session', async () => {
|
|
318
|
+
await assert.rejects(
|
|
319
|
+
() => sm.resolveBlocker('unknown-session', 'some response'),
|
|
320
|
+
(err: Error) => {
|
|
321
|
+
assert.ok(err.message.includes('Session not found'));
|
|
322
|
+
return true;
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('resolveBlocker clears pendingBlocker and sends UI response', async () => {
|
|
328
|
+
const sessionId = await sm.startSession('/tmp/blocker-resolve', { cliPath: '/usr/bin/gsd' });
|
|
329
|
+
const client = sm.lastClient!;
|
|
330
|
+
|
|
331
|
+
// Simulate a blocking UI request event
|
|
332
|
+
client.emitEvent({
|
|
333
|
+
type: 'extension_ui_request',
|
|
334
|
+
id: 'req-42',
|
|
335
|
+
method: 'select',
|
|
336
|
+
title: 'Pick an option',
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const session = sm.getSession(sessionId)!;
|
|
340
|
+
assert.ok(session.pendingBlocker);
|
|
341
|
+
assert.equal(session.status, 'blocked');
|
|
342
|
+
|
|
343
|
+
// Resolve the blocker
|
|
344
|
+
await sm.resolveBlocker(sessionId, 'option-a');
|
|
345
|
+
|
|
346
|
+
assert.equal(session.pendingBlocker, null);
|
|
347
|
+
assert.equal(session.status, 'running');
|
|
348
|
+
assert.equal(client.uiResponses.length, 1);
|
|
349
|
+
assert.equal(client.uiResponses[0].requestId, 'req-42');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('cancelSession calls abort + stop on client', async () => {
|
|
353
|
+
const sessionId = await sm.startSession('/tmp/cancel-test', { cliPath: '/usr/bin/gsd' });
|
|
354
|
+
const client = sm.lastClient!;
|
|
355
|
+
|
|
356
|
+
await sm.cancelSession(sessionId);
|
|
357
|
+
|
|
358
|
+
assert.ok(client.aborted);
|
|
359
|
+
assert.ok(client.stopped);
|
|
360
|
+
|
|
361
|
+
const session = sm.getSession(sessionId)!;
|
|
362
|
+
assert.equal(session.status, 'cancelled');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('cancelSession errors for unknown session', async () => {
|
|
366
|
+
await assert.rejects(
|
|
367
|
+
() => sm.cancelSession('unknown'),
|
|
368
|
+
(err: Error) => {
|
|
369
|
+
assert.ok(err.message.includes('Session not found'));
|
|
370
|
+
return true;
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('cleanup stops all active sessions', async () => {
|
|
376
|
+
await sm.startSession('/tmp/cleanup-1', { cliPath: '/usr/bin/gsd' });
|
|
377
|
+
await sm.startSession('/tmp/cleanup-2', { cliPath: '/usr/bin/gsd' });
|
|
378
|
+
|
|
379
|
+
assert.equal(sm.allClients.length, 2);
|
|
380
|
+
|
|
381
|
+
await sm.cleanup();
|
|
382
|
+
|
|
383
|
+
for (const client of sm.allClients) {
|
|
384
|
+
assert.ok(client.stopped, 'Client should be stopped after cleanup');
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('event ring buffer caps at MAX_EVENTS', async () => {
|
|
389
|
+
const sessionId = await sm.startSession('/tmp/ring-buffer', { cliPath: '/usr/bin/gsd' });
|
|
390
|
+
const client = sm.lastClient!;
|
|
391
|
+
|
|
392
|
+
for (let i = 0; i < MAX_EVENTS + 20; i++) {
|
|
393
|
+
client.emitEvent({ type: 'tool_use', index: i });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const session = sm.getSession(sessionId)!;
|
|
397
|
+
assert.equal(session.events.length, MAX_EVENTS);
|
|
398
|
+
// Oldest events trimmed — first event index should be 20
|
|
399
|
+
assert.equal((session.events[0] as Record<string, unknown>).index, 20);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('blocker detection: non-fire-and-forget extension_ui_request sets pendingBlocker', async () => {
|
|
403
|
+
const sessionId = await sm.startSession('/tmp/blocker-detect', { cliPath: '/usr/bin/gsd' });
|
|
404
|
+
const client = sm.lastClient!;
|
|
405
|
+
|
|
406
|
+
// 'select' is not in FIRE_AND_FORGET_METHODS
|
|
407
|
+
client.emitEvent({
|
|
408
|
+
type: 'extension_ui_request',
|
|
409
|
+
id: 'req-99',
|
|
410
|
+
method: 'select',
|
|
411
|
+
title: 'Choose wisely',
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const session = sm.getSession(sessionId)!;
|
|
415
|
+
assert.equal(session.status, 'blocked');
|
|
416
|
+
assert.ok(session.pendingBlocker);
|
|
417
|
+
assert.equal(session.pendingBlocker.id, 'req-99');
|
|
418
|
+
assert.equal(session.pendingBlocker.method, 'select');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('fire-and-forget methods do not set pendingBlocker', async () => {
|
|
422
|
+
const sessionId = await sm.startSession('/tmp/fire-forget', { cliPath: '/usr/bin/gsd' });
|
|
423
|
+
const client = sm.lastClient!;
|
|
424
|
+
|
|
425
|
+
// 'notify' is fire-and-forget — on its own (no terminal prefix) should not block
|
|
426
|
+
client.emitEvent({
|
|
427
|
+
type: 'extension_ui_request',
|
|
428
|
+
id: 'req-100',
|
|
429
|
+
method: 'notify',
|
|
430
|
+
message: 'Just a notification',
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const session = sm.getSession(sessionId)!;
|
|
434
|
+
assert.equal(session.status, 'running');
|
|
435
|
+
assert.equal(session.pendingBlocker, null);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('terminal detection: auto-mode stopped sets status to completed', async () => {
|
|
439
|
+
const sessionId = await sm.startSession('/tmp/terminal', { cliPath: '/usr/bin/gsd' });
|
|
440
|
+
const client = sm.lastClient!;
|
|
441
|
+
|
|
442
|
+
client.emitEvent({
|
|
443
|
+
type: 'extension_ui_request',
|
|
444
|
+
method: 'notify',
|
|
445
|
+
message: 'Auto-mode stopped — all tasks complete',
|
|
446
|
+
id: 'term-1',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const session = sm.getSession(sessionId)!;
|
|
450
|
+
assert.equal(session.status, 'completed');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('terminal detection with blocked: message sets status to blocked', async () => {
|
|
454
|
+
const sessionId = await sm.startSession('/tmp/terminal-blocked', { cliPath: '/usr/bin/gsd' });
|
|
455
|
+
const client = sm.lastClient!;
|
|
456
|
+
|
|
457
|
+
client.emitEvent({
|
|
458
|
+
type: 'extension_ui_request',
|
|
459
|
+
method: 'notify',
|
|
460
|
+
message: 'Auto-mode stopped — blocked: needs user input',
|
|
461
|
+
id: 'block-1',
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const session = sm.getSession(sessionId)!;
|
|
465
|
+
assert.equal(session.status, 'blocked');
|
|
466
|
+
assert.ok(session.pendingBlocker);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('cost tracking: cumulative-max from cost_update events', async () => {
|
|
470
|
+
const sessionId = await sm.startSession('/tmp/cost-track', { cliPath: '/usr/bin/gsd' });
|
|
471
|
+
const client = sm.lastClient!;
|
|
472
|
+
|
|
473
|
+
client.emitEvent({
|
|
474
|
+
type: 'cost_update',
|
|
475
|
+
cumulativeCost: 0.05,
|
|
476
|
+
tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100 },
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
client.emitEvent({
|
|
480
|
+
type: 'cost_update',
|
|
481
|
+
cumulativeCost: 0.12,
|
|
482
|
+
tokens: { input: 2500, output: 800, cacheRead: 150, cacheWrite: 300 },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const session = sm.getSession(sessionId)!;
|
|
486
|
+
assert.equal(session.cost.totalCost, 0.12);
|
|
487
|
+
assert.equal(session.cost.tokens.input, 2500);
|
|
488
|
+
assert.equal(session.cost.tokens.output, 800);
|
|
489
|
+
assert.equal(session.cost.tokens.cacheRead, 200); // First was higher
|
|
490
|
+
assert.equal(session.cost.tokens.cacheWrite, 300); // Second was higher
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('getResult returns HeadlessJsonResult-shaped object', async () => {
|
|
494
|
+
const sessionId = await sm.startSession('/tmp/result-shape', { cliPath: '/usr/bin/gsd' });
|
|
495
|
+
const result = sm.getResult(sessionId);
|
|
496
|
+
|
|
497
|
+
assert.equal(result.sessionId, sessionId);
|
|
498
|
+
assert.equal(result.projectDir, resolve('/tmp/result-shape'));
|
|
499
|
+
assert.equal(result.status, 'running');
|
|
500
|
+
assert.equal(typeof result.durationMs, 'number');
|
|
501
|
+
assert.ok(result.cost);
|
|
502
|
+
assert.ok(Array.isArray(result.recentEvents));
|
|
503
|
+
assert.equal(result.pendingBlocker, null);
|
|
504
|
+
assert.equal(result.error, null);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('getResult errors for unknown session', () => {
|
|
508
|
+
assert.throws(
|
|
509
|
+
() => sm.getResult('unknown'),
|
|
510
|
+
(err: Error) => {
|
|
511
|
+
assert.ok(err.message.includes('Session not found'));
|
|
512
|
+
return true;
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// CLI path resolution tests
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
describe('SessionManager.resolveCLIPath', () => {
|
|
523
|
+
const originalGsdPath = process.env['GSD_CLI_PATH'];
|
|
524
|
+
const originalPath = process.env['PATH'];
|
|
525
|
+
|
|
526
|
+
afterEach(() => {
|
|
527
|
+
if (originalGsdPath !== undefined) {
|
|
528
|
+
process.env['GSD_CLI_PATH'] = originalGsdPath;
|
|
529
|
+
} else {
|
|
530
|
+
delete process.env['GSD_CLI_PATH'];
|
|
531
|
+
}
|
|
532
|
+
if (originalPath !== undefined) {
|
|
533
|
+
process.env['PATH'] = originalPath;
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('GSD_CLI_PATH env var takes precedence', () => {
|
|
538
|
+
process.env['GSD_CLI_PATH'] = '/custom/path/to/gsd';
|
|
539
|
+
const result = SessionManager.resolveCLIPath();
|
|
540
|
+
assert.equal(result, resolve('/custom/path/to/gsd'));
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('throws when GSD_CLI_PATH not set and which fails', () => {
|
|
544
|
+
delete process.env['GSD_CLI_PATH'];
|
|
545
|
+
process.env['PATH'] = '/nonexistent';
|
|
546
|
+
assert.throws(
|
|
547
|
+
() => SessionManager.resolveCLIPath(),
|
|
548
|
+
(err: Error) => {
|
|
549
|
+
assert.ok(err.message.includes('Cannot find GSD CLI'));
|
|
550
|
+
return true;
|
|
551
|
+
},
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// Tool registration tests (via createMcpServer)
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
describe('createMcpServer tool registration', () => {
|
|
561
|
+
let sm: TestableSessionManager;
|
|
562
|
+
|
|
563
|
+
beforeEach(() => {
|
|
564
|
+
sm = createManager();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
afterEach(async () => {
|
|
568
|
+
for (const mgr of allManagers) {
|
|
569
|
+
await mgr.cleanup();
|
|
570
|
+
}
|
|
571
|
+
allManagers = [];
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('creates server successfully with all required methods', async () => {
|
|
575
|
+
const { server } = await createMcpServer(sm);
|
|
576
|
+
assert.ok(server);
|
|
577
|
+
assert.ok(typeof server.connect === 'function');
|
|
578
|
+
assert.ok(typeof server.close === 'function');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('gsd_execute flow returns sessionId on success', async () => {
|
|
582
|
+
const sessionId = await sm.startSession('/tmp/tool-exec', { cliPath: '/usr/bin/gsd' });
|
|
583
|
+
assert.equal(typeof sessionId, 'string');
|
|
584
|
+
assert.ok(sessionId.length > 0);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('gsd_status flow returns correct shape', async () => {
|
|
588
|
+
const sessionId = await sm.startSession('/tmp/tool-status', { cliPath: '/usr/bin/gsd' });
|
|
589
|
+
const session = sm.getSession(sessionId)!;
|
|
590
|
+
|
|
591
|
+
assert.equal(typeof session.status, 'string');
|
|
592
|
+
assert.ok(Array.isArray(session.events));
|
|
593
|
+
assert.ok(session.cost);
|
|
594
|
+
assert.equal(typeof session.startTime, 'number');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('gsd_resolve_blocker flow returns error when no blocker', async () => {
|
|
598
|
+
const sessionId = await sm.startSession('/tmp/tool-resolve', { cliPath: '/usr/bin/gsd' });
|
|
599
|
+
await assert.rejects(
|
|
600
|
+
() => sm.resolveBlocker(sessionId, 'fix'),
|
|
601
|
+
(err: Error) => {
|
|
602
|
+
assert.ok(err.message.includes('No pending blocker'));
|
|
603
|
+
return true;
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('gsd_result flow returns HeadlessJsonResult shape', async () => {
|
|
609
|
+
const sessionId = await sm.startSession('/tmp/tool-result', { cliPath: '/usr/bin/gsd' });
|
|
610
|
+
const result = sm.getResult(sessionId);
|
|
611
|
+
|
|
612
|
+
assert.ok('sessionId' in result);
|
|
613
|
+
assert.ok('projectDir' in result);
|
|
614
|
+
assert.ok('status' in result);
|
|
615
|
+
assert.ok('durationMs' in result);
|
|
616
|
+
assert.ok('cost' in result);
|
|
617
|
+
assert.ok('recentEvents' in result);
|
|
618
|
+
assert.ok('pendingBlocker' in result);
|
|
619
|
+
assert.ok('error' in result);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('gsd_cancel flow marks session as cancelled', async () => {
|
|
623
|
+
const sessionId = await sm.startSession('/tmp/tool-cancel', { cliPath: '/usr/bin/gsd' });
|
|
624
|
+
await sm.cancelSession(sessionId);
|
|
625
|
+
const session = sm.getSession(sessionId)!;
|
|
626
|
+
assert.equal(session.status, 'cancelled');
|
|
627
|
+
});
|
|
628
|
+
});
|