gsd-pi 2.10.12 → 2.12.0
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/dist/bundled-extension-paths.d.ts +2 -0
- package/dist/bundled-extension-paths.js +10 -0
- package/dist/loader.js +45 -28
- package/dist/onboarding.js +100 -0
- package/dist/resource-loader.js +12 -3
- package/dist/resources/GSD-WORKFLOW.md +661 -0
- package/dist/resources/agents/researcher.md +29 -0
- package/dist/resources/agents/scout.md +56 -0
- package/dist/resources/agents/worker.md +31 -0
- package/dist/resources/extensions/ask-user-questions.ts +289 -0
- package/dist/resources/extensions/async-jobs/async-bash-tool.ts +211 -0
- package/dist/resources/extensions/async-jobs/await-tool.ts +101 -0
- package/dist/resources/extensions/async-jobs/cancel-job-tool.ts +34 -0
- package/dist/resources/extensions/async-jobs/index.ts +133 -0
- package/dist/resources/extensions/async-jobs/job-manager.ts +211 -0
- package/dist/resources/extensions/bg-shell/index.ts +3179 -0
- package/dist/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/dist/resources/extensions/browser-tools/capture.ts +165 -0
- package/dist/resources/extensions/browser-tools/core.d.ts +205 -0
- package/dist/resources/extensions/browser-tools/core.js +1057 -0
- package/dist/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
- package/dist/resources/extensions/browser-tools/index.ts +51 -0
- package/dist/resources/extensions/browser-tools/lifecycle.ts +270 -0
- package/dist/resources/extensions/browser-tools/package.json +24 -0
- package/dist/resources/extensions/browser-tools/refs.ts +264 -0
- package/dist/resources/extensions/browser-tools/settle.ts +197 -0
- package/dist/resources/extensions/browser-tools/state.ts +408 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
- package/dist/resources/extensions/browser-tools/tools/assertions.ts +342 -0
- package/dist/resources/extensions/browser-tools/tools/forms.ts +801 -0
- package/dist/resources/extensions/browser-tools/tools/inspection.ts +492 -0
- package/dist/resources/extensions/browser-tools/tools/intent.ts +614 -0
- package/dist/resources/extensions/browser-tools/tools/interaction.ts +865 -0
- package/dist/resources/extensions/browser-tools/tools/navigation.ts +232 -0
- package/dist/resources/extensions/browser-tools/tools/pages.ts +303 -0
- package/dist/resources/extensions/browser-tools/tools/refs.ts +541 -0
- package/dist/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
- package/dist/resources/extensions/browser-tools/tools/session.ts +400 -0
- package/dist/resources/extensions/browser-tools/tools/wait.ts +247 -0
- package/dist/resources/extensions/browser-tools/utils.ts +660 -0
- package/dist/resources/extensions/context7/index.ts +428 -0
- package/dist/resources/extensions/context7/package.json +11 -0
- package/dist/resources/extensions/get-secrets-from-user.ts +625 -0
- package/dist/resources/extensions/google-search/index.ts +336 -0
- package/dist/resources/extensions/google-search/package.json +9 -0
- package/dist/resources/extensions/gsd/activity-log.ts +116 -0
- package/dist/resources/extensions/gsd/auto.ts +3732 -0
- package/dist/resources/extensions/gsd/commands.ts +544 -0
- package/dist/resources/extensions/gsd/crash-recovery.ts +105 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +564 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +78 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +278 -0
- package/dist/resources/extensions/gsd/doctor.ts +766 -0
- package/dist/resources/extensions/gsd/exit-command.ts +18 -0
- package/dist/resources/extensions/gsd/files.ts +857 -0
- package/dist/resources/extensions/gsd/git-service.ts +904 -0
- package/dist/resources/extensions/gsd/gitignore.ts +200 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +1054 -0
- package/dist/resources/extensions/gsd/index.ts +735 -0
- package/dist/resources/extensions/gsd/metrics.ts +374 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +218 -0
- package/dist/resources/extensions/gsd/migrate/index.ts +42 -0
- package/dist/resources/extensions/gsd/migrate/parser.ts +323 -0
- package/dist/resources/extensions/gsd/migrate/parsers.ts +624 -0
- package/dist/resources/extensions/gsd/migrate/preview.ts +48 -0
- package/dist/resources/extensions/gsd/migrate/transformer.ts +346 -0
- package/dist/resources/extensions/gsd/migrate/types.ts +370 -0
- package/dist/resources/extensions/gsd/migrate/validator.ts +55 -0
- package/dist/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/dist/resources/extensions/gsd/native-git-bridge.ts +181 -0
- package/dist/resources/extensions/gsd/native-parser-bridge.ts +135 -0
- package/dist/resources/extensions/gsd/observability-validator.ts +408 -0
- package/dist/resources/extensions/gsd/package.json +11 -0
- package/dist/resources/extensions/gsd/paths.ts +339 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/dist/resources/extensions/gsd/preferences.ts +988 -0
- package/dist/resources/extensions/gsd/prompt-loader.ts +92 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +27 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +237 -0
- package/dist/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +63 -0
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +61 -0
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -0
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +29 -0
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -0
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +13 -0
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +78 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +55 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +100 -0
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +39 -0
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +37 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +28 -0
- package/dist/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/dist/resources/extensions/gsd/prompts/system.md +190 -0
- package/dist/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +50 -0
- package/dist/resources/extensions/gsd/session-forensics.ts +500 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +137 -0
- package/dist/resources/extensions/gsd/state.ts +569 -0
- package/dist/resources/extensions/gsd/templates/context.md +76 -0
- package/dist/resources/extensions/gsd/templates/decisions.md +8 -0
- package/dist/resources/extensions/gsd/templates/milestone-summary.md +73 -0
- package/dist/resources/extensions/gsd/templates/plan.md +131 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +23 -0
- package/dist/resources/extensions/gsd/templates/project.md +31 -0
- package/dist/resources/extensions/gsd/templates/reassessment.md +28 -0
- package/dist/resources/extensions/gsd/templates/requirements.md +81 -0
- package/dist/resources/extensions/gsd/templates/research.md +46 -0
- package/dist/resources/extensions/gsd/templates/roadmap.md +118 -0
- package/dist/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
- package/dist/resources/extensions/gsd/templates/slice-context.md +58 -0
- package/dist/resources/extensions/gsd/templates/slice-summary.md +99 -0
- package/dist/resources/extensions/gsd/templates/state.md +19 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +52 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +57 -0
- package/dist/resources/extensions/gsd/templates/uat.md +54 -0
- package/dist/resources/extensions/gsd/tests/activity-log-prune.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/activity-log-save.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/auto-draft-pause.test.ts +109 -0
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +45 -0
- package/dist/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +196 -0
- package/dist/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
- package/dist/resources/extensions/gsd/tests/collect-from-manifest.test.ts +469 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +200 -0
- package/dist/resources/extensions/gsd/tests/cost-projection.test.ts +134 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +312 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +299 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +660 -0
- package/dist/resources/extensions/gsd/tests/discuss-prompt.test.ts +27 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +91 -0
- package/dist/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +170 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +594 -0
- package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +169 -0
- package/dist/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +1788 -0
- package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +577 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +580 -0
- package/dist/resources/extensions/gsd/tests/manifest-status.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/metrics-io.test.ts +176 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +188 -0
- package/dist/resources/extensions/gsd/tests/migrate-command.test.ts +369 -0
- package/dist/resources/extensions/gsd/tests/migrate-parser.test.ts +757 -0
- package/dist/resources/extensions/gsd/tests/migrate-transformer.test.ts +635 -0
- package/dist/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +414 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer.test.ts +398 -0
- package/dist/resources/extensions/gsd/tests/must-have-parser.test.ts +291 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +54 -0
- package/dist/resources/extensions/gsd/tests/next-milestone-id.test.ts +66 -0
- package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +1664 -0
- package/dist/resources/extensions/gsd/tests/plan-milestone.test.ts +133 -0
- package/dist/resources/extensions/gsd/tests/plan-quality-validator.test.ts +363 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/dist/resources/extensions/gsd/tests/queue-draft-detection.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/reassess-prompt.test.ts +142 -0
- package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
- package/dist/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +493 -0
- package/dist/resources/extensions/gsd/tests/requirements.test.ts +106 -0
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +35 -0
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +41 -0
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/test-helpers.ts +61 -0
- package/dist/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +219 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/workspace-index.test.ts +75 -0
- package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/worktree-manager.test.ts +141 -0
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +597 -0
- package/dist/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
- package/dist/resources/extensions/gsd/types.ts +296 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +184 -0
- package/dist/resources/extensions/gsd/workspace-index.ts +189 -0
- package/dist/resources/extensions/gsd/worktree-command.ts +845 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +459 -0
- package/dist/resources/extensions/gsd/{worktree.js → worktree.ts} +92 -61
- package/dist/resources/extensions/mac-tools/index.ts +852 -0
- package/dist/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/dist/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/dist/resources/extensions/mcporter/index.ts +429 -0
- package/dist/resources/extensions/remote-questions/config.ts +81 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +128 -0
- package/dist/resources/extensions/remote-questions/format.ts +163 -0
- package/dist/resources/extensions/remote-questions/manager.ts +192 -0
- package/dist/resources/extensions/remote-questions/remote-command.ts +361 -0
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +92 -0
- package/dist/resources/extensions/remote-questions/status.ts +31 -0
- package/dist/resources/extensions/remote-questions/store.ts +77 -0
- package/dist/resources/extensions/remote-questions/types.ts +75 -0
- package/dist/resources/extensions/search-the-web/cache.ts +78 -0
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +99 -0
- package/dist/resources/extensions/search-the-web/format.ts +258 -0
- package/dist/resources/extensions/search-the-web/http.ts +238 -0
- package/dist/resources/extensions/search-the-web/index.ts +65 -0
- package/dist/resources/extensions/search-the-web/native-search.ts +165 -0
- package/dist/resources/extensions/search-the-web/provider.ts +136 -0
- package/dist/resources/extensions/search-the-web/tavily.ts +116 -0
- package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +581 -0
- package/dist/resources/extensions/search-the-web/tool-llm-context.ts +620 -0
- package/dist/resources/extensions/search-the-web/tool-search.ts +635 -0
- package/dist/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/dist/resources/extensions/shared/bundled-extension-paths.ts +11 -0
- package/dist/resources/extensions/shared/confirm-ui.ts +126 -0
- package/dist/resources/extensions/shared/interview-ui.ts +613 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +197 -0
- package/dist/resources/extensions/shared/progress-widget.ts +282 -0
- package/dist/resources/extensions/shared/terminal.ts +23 -0
- package/dist/resources/extensions/shared/thinking-widget.ts +107 -0
- package/dist/resources/extensions/shared/ui.ts +400 -0
- package/dist/resources/extensions/shared/wizard-ui.ts +551 -0
- package/dist/resources/extensions/slash-commands/audit.ts +88 -0
- package/dist/resources/extensions/slash-commands/clear.ts +10 -0
- package/dist/resources/extensions/slash-commands/create-extension.ts +297 -0
- package/dist/resources/extensions/slash-commands/create-slash-command.ts +234 -0
- package/dist/resources/extensions/slash-commands/index.ts +12 -0
- package/dist/resources/extensions/subagent/agents.ts +126 -0
- package/dist/resources/extensions/subagent/index.ts +1072 -0
- package/dist/resources/extensions/subagent/isolation.ts +498 -0
- package/dist/resources/extensions/ttsr/index.ts +163 -0
- package/dist/resources/extensions/ttsr/rule-loader.ts +121 -0
- package/dist/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +430 -0
- package/dist/resources/extensions/universal-config/discovery.ts +78 -0
- package/dist/resources/extensions/universal-config/format.ts +160 -0
- package/dist/resources/extensions/universal-config/index.ts +118 -0
- package/dist/resources/extensions/universal-config/package.json +11 -0
- package/dist/resources/extensions/universal-config/scanners.ts +579 -0
- package/dist/resources/extensions/universal-config/tests/discovery.test.ts +111 -0
- package/dist/resources/extensions/universal-config/tests/format.test.ts +111 -0
- package/dist/resources/extensions/universal-config/tests/scanners.test.ts +438 -0
- package/dist/resources/extensions/universal-config/tools.ts +60 -0
- package/dist/resources/extensions/universal-config/types.ts +116 -0
- package/dist/resources/extensions/voice/index.ts +271 -0
- package/dist/resources/extensions/voice/speech-recognizer.py +504 -0
- package/dist/resources/extensions/voice/speech-recognizer.swift +154 -0
- package/dist/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/dist/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/dist/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/dist/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/dist/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/dist/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/dist/resources/skills/frontend-design/SKILL.md +45 -0
- package/dist/resources/skills/github-workflows/SKILL.md +92 -0
- package/dist/resources/skills/github-workflows/references/gh/SKILL.md +255 -0
- package/dist/resources/skills/github-workflows/references/gh/references/issue-stories.md +204 -0
- package/dist/resources/skills/github-workflows/references/gh/references/labels.md +170 -0
- package/dist/resources/skills/github-workflows/references/gh/references/milestones.md +158 -0
- package/dist/resources/skills/github-workflows/references/gh/references/projects-v2.md +177 -0
- package/dist/resources/skills/github-workflows/references/gh/scripts/experiment_cleanup.py +191 -0
- package/dist/resources/skills/github-workflows/references/gh/scripts/github_project_setup.py +799 -0
- package/dist/resources/skills/github-workflows/references/gh/tests/__init__.py +0 -0
- package/dist/resources/skills/github-workflows/references/gh/tests/test_github_project_setup.py +608 -0
- package/dist/resources/skills/swiftui/SKILL.md +208 -0
- package/dist/resources/skills/swiftui/references/animations.md +921 -0
- package/dist/resources/skills/swiftui/references/architecture.md +1561 -0
- package/dist/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/dist/resources/skills/swiftui/references/navigation.md +1492 -0
- package/dist/resources/skills/swiftui/references/networking-async.md +214 -0
- package/dist/resources/skills/swiftui/references/performance.md +1706 -0
- package/dist/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/dist/resources/skills/swiftui/references/state-management.md +1443 -0
- package/dist/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/dist/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/dist/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/dist/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/dist/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/dist/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/dist/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/wizard.js +1 -0
- package/package.json +8 -6
- package/packages/native/dist/fd/index.d.ts +1 -0
- package/packages/native/dist/fd/index.js +1 -0
- package/packages/native/dist/grep/index.d.ts +2 -1
- package/packages/native/dist/grep/index.js +1 -0
- package/packages/native/src/__tests__/fd.test.mjs +35 -0
- package/packages/native/src/__tests__/grep.test.mjs +26 -9
- package/packages/native/src/fd/index.ts +1 -0
- package/packages/native/src/grep/index.ts +3 -2
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +169 -55
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +13 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +16 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +91 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +273 -63
- package/packages/pi-agent-core/src/agent.ts +24 -0
- package/packages/pi-agent-core/src/types.ts +98 -0
- package/packages/pi-ai/dist/env-api-keys.js +2 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +314 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +236 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.js +6 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +2 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -1
- 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/env-api-keys.ts +2 -0
- package/packages/pi-ai/src/models.generated.ts +236 -0
- package/packages/pi-ai/src/providers/google-gemini-cli.ts +8 -0
- package/packages/pi-ai/src/providers/openai-completions.ts +3 -0
- package/packages/pi-ai/src/types.ts +2 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +1 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +137 -22
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +43 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/fallback-resolver.d.ts +51 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.js +119 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js +183 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/index.js +1 -0
- package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +12 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +37 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/session-manager.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +61 -0
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +47 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/find.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/find.js +1 -0
- package/packages/pi-coding-agent/dist/core/tools/find.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +6 -15
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts +18 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +46 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +25 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +1 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +167 -22
- package/packages/pi-coding-agent/src/core/auth-storage.ts +45 -0
- package/packages/pi-coding-agent/src/core/fallback-resolver.test.ts +229 -0
- package/packages/pi-coding-agent/src/core/fallback-resolver.ts +165 -0
- package/packages/pi-coding-agent/src/core/index.ts +1 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +65 -0
- package/packages/pi-coding-agent/src/core/session-manager.ts +51 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +66 -0
- package/packages/pi-coding-agent/src/core/tools/find.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +6 -16
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +49 -4
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +28 -2
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +3 -4
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/dist/components/editor.d.ts +7 -0
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +57 -50
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/autocomplete.ts +3 -5
- package/packages/pi-tui/src/components/editor.ts +70 -49
- package/pkg/dist/core/export-html/ansi-to-html.d.ts +22 -0
- package/pkg/dist/core/export-html/ansi-to-html.d.ts.map +1 -0
- package/pkg/dist/core/export-html/ansi-to-html.js +249 -0
- package/pkg/dist/core/export-html/ansi-to-html.js.map +1 -0
- package/pkg/dist/core/export-html/index.d.ts +37 -0
- package/pkg/dist/core/export-html/index.d.ts.map +1 -0
- package/pkg/dist/core/export-html/index.js +223 -0
- package/pkg/dist/core/export-html/index.js.map +1 -0
- package/pkg/dist/core/export-html/template.css +971 -0
- package/pkg/dist/core/export-html/template.html +54 -0
- package/pkg/dist/core/export-html/template.js +1583 -0
- package/pkg/dist/core/export-html/tool-renderer.d.ts +38 -0
- package/pkg/dist/core/export-html/tool-renderer.d.ts.map +1 -0
- package/pkg/dist/core/export-html/tool-renderer.js +70 -0
- package/pkg/dist/core/export-html/tool-renderer.js.map +1 -0
- package/pkg/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/pkg/dist/core/export-html/vendor/marked.min.js +6 -0
- package/scripts/postinstall.js +30 -17
- package/src/resources/extensions/bg-shell/index.ts +51 -7
- package/src/resources/extensions/gsd/activity-log.ts +61 -14
- package/src/resources/extensions/gsd/auto.ts +288 -19
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +55 -12
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/src/resources/extensions/gsd/doctor.ts +60 -3
- package/src/resources/extensions/gsd/files.ts +45 -1
- package/src/resources/extensions/gsd/git-service.ts +46 -41
- package/src/resources/extensions/gsd/guided-flow.ts +81 -9
- package/src/resources/extensions/gsd/index.ts +62 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +181 -0
- package/src/resources/extensions/gsd/paths.ts +35 -4
- package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/src/resources/extensions/gsd/preferences.ts +232 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -3
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/queue.md +3 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/state.ts +130 -18
- package/src/resources/extensions/gsd/templates/context.md +1 -1
- package/src/resources/extensions/gsd/templates/state.md +3 -3
- package/src/resources/extensions/gsd/tests/activity-log-save.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +6 -2
- package/src/resources/extensions/gsd/tests/git-service.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +8 -8
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree.test.ts +19 -19
- package/src/resources/extensions/gsd/types.ts +109 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +72 -13
- package/src/resources/extensions/gsd/worktree.ts +5 -4
- package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/src/resources/extensions/search-the-web/native-search.ts +32 -23
- package/src/resources/extensions/search-the-web/provider.ts +19 -2
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
- package/src/resources/extensions/shared/bundled-extension-paths.ts +11 -0
- package/src/resources/extensions/subagent/index.ts +2 -1
- package/src/resources/skills/github-workflows/SKILL.md +5 -0
- package/dist/resources/extensions/gsd/git-service.d.ts +0 -225
- package/dist/resources/extensions/gsd/git-service.js +0 -756
- package/dist/resources/extensions/gsd/preferences.d.ts +0 -122
- package/dist/resources/extensions/gsd/preferences.js +0 -616
- package/dist/resources/extensions/gsd/worktree.d.ts +0 -106
- package/dist/resources/extensions/remote-questions/config.d.ts +0 -14
- package/dist/resources/extensions/remote-questions/config.js +0 -68
- package/dist/resources/extensions/remote-questions/discord-adapter.d.ts +0 -17
- package/dist/resources/extensions/remote-questions/discord-adapter.js +0 -118
- package/dist/resources/extensions/remote-questions/format.d.ts +0 -38
- package/dist/resources/extensions/remote-questions/format.js +0 -119
- package/dist/resources/extensions/remote-questions/manager.d.ts +0 -23
- package/dist/resources/extensions/remote-questions/manager.js +0 -161
- package/dist/resources/extensions/remote-questions/remote-command.d.ts +0 -6
- package/dist/resources/extensions/remote-questions/remote-command.js +0 -358
- package/dist/resources/extensions/remote-questions/slack-adapter.d.ts +0 -15
- package/dist/resources/extensions/remote-questions/slack-adapter.js +0 -82
- package/dist/resources/extensions/remote-questions/status.d.ts +0 -9
- package/dist/resources/extensions/remote-questions/status.js +0 -25
- package/dist/resources/extensions/remote-questions/store.d.ts +0 -11
- package/dist/resources/extensions/remote-questions/store.js +0 -66
- package/dist/resources/extensions/remote-questions/types.d.ts +0 -68
- package/dist/resources/extensions/remote-questions/types.js +0 -4
|
@@ -0,0 +1,1788 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
inferCommitType,
|
|
8
|
+
GitServiceImpl,
|
|
9
|
+
RUNTIME_EXCLUSION_PATHS,
|
|
10
|
+
VALID_BRANCH_NAME,
|
|
11
|
+
runGit,
|
|
12
|
+
readIntegrationBranch,
|
|
13
|
+
writeIntegrationBranch,
|
|
14
|
+
type GitPreferences,
|
|
15
|
+
type CommitOptions,
|
|
16
|
+
type MergeSliceResult,
|
|
17
|
+
type PreMergeCheckResult,
|
|
18
|
+
} from "../git-service.ts";
|
|
19
|
+
import { createTestContext } from './test-helpers.ts';
|
|
20
|
+
|
|
21
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
22
|
+
function run(command: string, cwd: string): string {
|
|
23
|
+
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main(): Promise<void> {
|
|
27
|
+
// ─── inferCommitType ───────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
console.log("\n=== inferCommitType ===");
|
|
30
|
+
|
|
31
|
+
assertEq(
|
|
32
|
+
inferCommitType("Implement user authentication"),
|
|
33
|
+
"feat",
|
|
34
|
+
"generic feature title → feat"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
assertEq(
|
|
38
|
+
inferCommitType("Add dashboard page"),
|
|
39
|
+
"feat",
|
|
40
|
+
"add-style title → feat"
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
assertEq(
|
|
44
|
+
inferCommitType("Fix login redirect bug"),
|
|
45
|
+
"fix",
|
|
46
|
+
"title with 'fix' → fix"
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
assertEq(
|
|
50
|
+
inferCommitType("Bug in session handling"),
|
|
51
|
+
"fix",
|
|
52
|
+
"title with 'bug' → fix"
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
assertEq(
|
|
56
|
+
inferCommitType("Hotfix for production crash"),
|
|
57
|
+
"fix",
|
|
58
|
+
"title with 'hotfix' → fix"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
assertEq(
|
|
62
|
+
inferCommitType("Patch memory leak"),
|
|
63
|
+
"fix",
|
|
64
|
+
"title with 'patch' → fix"
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
assertEq(
|
|
68
|
+
inferCommitType("Refactor state management"),
|
|
69
|
+
"refactor",
|
|
70
|
+
"title with 'refactor' → refactor"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
assertEq(
|
|
74
|
+
inferCommitType("Restructure project layout"),
|
|
75
|
+
"refactor",
|
|
76
|
+
"title with 'restructure' → refactor"
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
assertEq(
|
|
80
|
+
inferCommitType("Reorganize module imports"),
|
|
81
|
+
"refactor",
|
|
82
|
+
"title with 'reorganize' → refactor"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
assertEq(
|
|
86
|
+
inferCommitType("Update API documentation"),
|
|
87
|
+
"docs",
|
|
88
|
+
"title with 'documentation' → docs"
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
assertEq(
|
|
92
|
+
inferCommitType("Add doc for setup guide"),
|
|
93
|
+
"docs",
|
|
94
|
+
"title with 'doc' → docs"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
assertEq(
|
|
98
|
+
inferCommitType("Add unit tests for auth"),
|
|
99
|
+
"test",
|
|
100
|
+
"title with 'tests' → test"
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
assertEq(
|
|
104
|
+
inferCommitType("Testing infrastructure setup"),
|
|
105
|
+
"test",
|
|
106
|
+
"title with 'testing' → test"
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
assertEq(
|
|
110
|
+
inferCommitType("Chore: update dependencies"),
|
|
111
|
+
"chore",
|
|
112
|
+
"title with 'chore' → chore"
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
assertEq(
|
|
116
|
+
inferCommitType("Cleanup unused imports"),
|
|
117
|
+
"chore",
|
|
118
|
+
"title with 'cleanup' → chore"
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
assertEq(
|
|
122
|
+
inferCommitType("Clean up stale branches"),
|
|
123
|
+
"chore",
|
|
124
|
+
"title with 'clean up' → chore"
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
assertEq(
|
|
128
|
+
inferCommitType("Archive old milestones"),
|
|
129
|
+
"chore",
|
|
130
|
+
"title with 'archive' → chore"
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
assertEq(
|
|
134
|
+
inferCommitType("Remove deprecated endpoints"),
|
|
135
|
+
"chore",
|
|
136
|
+
"title with 'remove' → chore"
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
assertEq(
|
|
140
|
+
inferCommitType("Delete temp files"),
|
|
141
|
+
"chore",
|
|
142
|
+
"title with 'delete' → chore"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Mixed keywords — first match wins
|
|
146
|
+
assertEq(
|
|
147
|
+
inferCommitType("Fix and refactor the login module"),
|
|
148
|
+
"fix",
|
|
149
|
+
"mixed keywords → first match wins (fix before refactor)"
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
assertEq(
|
|
153
|
+
inferCommitType("Refactor test utilities"),
|
|
154
|
+
"refactor",
|
|
155
|
+
"mixed keywords → first match wins (refactor before test)"
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Unknown / unrecognized title → feat
|
|
159
|
+
assertEq(
|
|
160
|
+
inferCommitType("Build the new pipeline"),
|
|
161
|
+
"feat",
|
|
162
|
+
"unrecognized title → feat"
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
assertEq(
|
|
166
|
+
inferCommitType(""),
|
|
167
|
+
"feat",
|
|
168
|
+
"empty title → feat"
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Word boundary: "testify" should NOT match "test"
|
|
172
|
+
assertEq(
|
|
173
|
+
inferCommitType("Testify integration"),
|
|
174
|
+
"feat",
|
|
175
|
+
"'testify' does not match 'test' — word boundary prevents partial match"
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// "documentary" should NOT match "doc" (word boundary)
|
|
179
|
+
assertEq(
|
|
180
|
+
inferCommitType("Documentary style UI"),
|
|
181
|
+
"feat",
|
|
182
|
+
"'documentary' does not match 'doc' — word boundary prevents partial match"
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// "prefix" should NOT match "fix" (word boundary)
|
|
186
|
+
assertEq(
|
|
187
|
+
inferCommitType("Add prefix to all IDs"),
|
|
188
|
+
"feat",
|
|
189
|
+
"'prefix' does not match 'fix' — word boundary prevents partial match"
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// ─── RUNTIME_EXCLUSION_PATHS ───────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
console.log("\n=== RUNTIME_EXCLUSION_PATHS ===");
|
|
195
|
+
|
|
196
|
+
assertEq(
|
|
197
|
+
RUNTIME_EXCLUSION_PATHS.length,
|
|
198
|
+
7,
|
|
199
|
+
"exactly 7 runtime exclusion paths"
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const expectedPaths = [
|
|
203
|
+
".gsd/activity/",
|
|
204
|
+
".gsd/runtime/",
|
|
205
|
+
".gsd/worktrees/",
|
|
206
|
+
".gsd/auto.lock",
|
|
207
|
+
".gsd/metrics.json",
|
|
208
|
+
".gsd/completed-units.json",
|
|
209
|
+
".gsd/STATE.md",
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
assertEq(
|
|
213
|
+
[...RUNTIME_EXCLUSION_PATHS],
|
|
214
|
+
expectedPaths,
|
|
215
|
+
"paths match expected set in order"
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
assertTrue(
|
|
219
|
+
RUNTIME_EXCLUSION_PATHS.includes(".gsd/activity/"),
|
|
220
|
+
"includes .gsd/activity/"
|
|
221
|
+
);
|
|
222
|
+
assertTrue(
|
|
223
|
+
RUNTIME_EXCLUSION_PATHS.includes(".gsd/STATE.md"),
|
|
224
|
+
"includes .gsd/STATE.md"
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// ─── runGit ────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
console.log("\n=== runGit ===");
|
|
230
|
+
|
|
231
|
+
const tempDir = mkdtempSync(join(tmpdir(), "gsd-git-service-test-"));
|
|
232
|
+
run("git init -b main", tempDir);
|
|
233
|
+
run('git config user.name "Pi Test"', tempDir);
|
|
234
|
+
run('git config user.email "pi@example.com"', tempDir);
|
|
235
|
+
|
|
236
|
+
// runGit should work on a valid repo
|
|
237
|
+
const branch = runGit(tempDir, ["branch", "--show-current"]);
|
|
238
|
+
assertEq(branch, "main", "runGit returns current branch");
|
|
239
|
+
|
|
240
|
+
// runGit allowFailure returns empty string on failure
|
|
241
|
+
const result = runGit(tempDir, ["log", "--oneline"], { allowFailure: true });
|
|
242
|
+
assertEq(result, "", "runGit allowFailure returns empty on error (no commits yet)");
|
|
243
|
+
|
|
244
|
+
// runGit throws on failure without allowFailure
|
|
245
|
+
let threw = false;
|
|
246
|
+
try {
|
|
247
|
+
runGit(tempDir, ["log", "--oneline"]);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
threw = true;
|
|
250
|
+
assertTrue(
|
|
251
|
+
(e as Error).message.includes("git log --oneline failed"),
|
|
252
|
+
"error message includes command and path"
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
assertTrue(threw, "runGit throws without allowFailure on error");
|
|
256
|
+
|
|
257
|
+
// ─── Type exports compile check ────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
console.log("\n=== Type exports ===");
|
|
260
|
+
|
|
261
|
+
// These are compile-time checks — if we got here, the types import fine
|
|
262
|
+
const _prefs: GitPreferences = { auto_push: true, remote: "origin" };
|
|
263
|
+
const _opts: CommitOptions = { message: "test" };
|
|
264
|
+
const _result: MergeSliceResult = { branch: "main", mergedCommitMessage: "msg", deletedBranch: false };
|
|
265
|
+
assertTrue(true, "GitPreferences type exported and usable");
|
|
266
|
+
assertTrue(true, "CommitOptions type exported and usable");
|
|
267
|
+
assertTrue(true, "MergeSliceResult type exported and usable");
|
|
268
|
+
|
|
269
|
+
// Cleanup T01 temp dir
|
|
270
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
271
|
+
|
|
272
|
+
// ─── Helper: create file with intermediate dirs ────────────────────────
|
|
273
|
+
|
|
274
|
+
function createFile(base: string, relativePath: string, content: string = "x"): void {
|
|
275
|
+
const full = join(base, relativePath);
|
|
276
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
277
|
+
writeFileSync(full, content, "utf-8");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function initTempRepo(): string {
|
|
281
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-git-t02-"));
|
|
282
|
+
run("git init -b main", dir);
|
|
283
|
+
run('git config user.name "Pi Test"', dir);
|
|
284
|
+
run('git config user.email "pi@example.com"', dir);
|
|
285
|
+
// Need an initial commit so HEAD exists
|
|
286
|
+
createFile(dir, ".gitkeep", "");
|
|
287
|
+
run("git add -A", dir);
|
|
288
|
+
run('git commit -m "init"', dir);
|
|
289
|
+
return dir;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── GitServiceImpl: smart staging ─────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
console.log("\n=== GitServiceImpl: smart staging ===");
|
|
295
|
+
|
|
296
|
+
{
|
|
297
|
+
const repo = initTempRepo();
|
|
298
|
+
const svc = new GitServiceImpl(repo);
|
|
299
|
+
|
|
300
|
+
// Create runtime files (should be excluded from staging)
|
|
301
|
+
createFile(repo, ".gsd/activity/log.jsonl", "log data");
|
|
302
|
+
createFile(repo, ".gsd/runtime/state.json", '{"state":true}');
|
|
303
|
+
createFile(repo, ".gsd/STATE.md", "# State");
|
|
304
|
+
createFile(repo, ".gsd/auto.lock", "lock");
|
|
305
|
+
createFile(repo, ".gsd/metrics.json", "{}");
|
|
306
|
+
createFile(repo, ".gsd/worktrees/wt/file.txt", "wt data");
|
|
307
|
+
|
|
308
|
+
// Create a real file (should be staged)
|
|
309
|
+
createFile(repo, "src/code.ts", 'console.log("hello");');
|
|
310
|
+
|
|
311
|
+
const result = svc.commit({ message: "test: smart staging" });
|
|
312
|
+
|
|
313
|
+
assertEq(result, "test: smart staging", "commit returns the commit message");
|
|
314
|
+
|
|
315
|
+
// Verify only src/code.ts is in the commit
|
|
316
|
+
const showStat = run("git show --stat --format= HEAD", repo);
|
|
317
|
+
assertTrue(showStat.includes("src/code.ts"), "src/code.ts is in the commit");
|
|
318
|
+
assertTrue(!showStat.includes(".gsd/activity"), ".gsd/activity/ excluded from commit");
|
|
319
|
+
assertTrue(!showStat.includes(".gsd/runtime"), ".gsd/runtime/ excluded from commit");
|
|
320
|
+
assertTrue(!showStat.includes("STATE.md"), ".gsd/STATE.md excluded from commit");
|
|
321
|
+
assertTrue(!showStat.includes("auto.lock"), ".gsd/auto.lock excluded from commit");
|
|
322
|
+
assertTrue(!showStat.includes("metrics.json"), ".gsd/metrics.json excluded from commit");
|
|
323
|
+
assertTrue(!showStat.includes(".gsd/worktrees"), ".gsd/worktrees/ excluded from commit");
|
|
324
|
+
|
|
325
|
+
// Verify runtime files are still untracked
|
|
326
|
+
// git status --short may collapse to "?? .gsd/" or show individual files
|
|
327
|
+
// Use --untracked-files=all to force individual listing
|
|
328
|
+
const statusOut = run("git status --short --untracked-files=all", repo);
|
|
329
|
+
assertTrue(statusOut.includes(".gsd/activity/"), "activity still untracked after commit");
|
|
330
|
+
assertTrue(statusOut.includes(".gsd/runtime/"), "runtime still untracked after commit");
|
|
331
|
+
assertTrue(statusOut.includes(".gsd/STATE.md"), "STATE.md still untracked after commit");
|
|
332
|
+
|
|
333
|
+
rmSync(repo, { recursive: true, force: true });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ─── GitServiceImpl: smart staging excludes tracked runtime files ──────
|
|
337
|
+
|
|
338
|
+
console.log("\n=== GitServiceImpl: smart staging excludes tracked runtime files ===");
|
|
339
|
+
|
|
340
|
+
{
|
|
341
|
+
// Reproduces the real bug: .gsd/ runtime files that are already tracked
|
|
342
|
+
// (in the git index) must be excluded from staging even when .gsd/ is
|
|
343
|
+
// in .gitignore. The old pathspec-exclude approach failed silently in
|
|
344
|
+
// this case and fell back to `git add -A`, staging everything.
|
|
345
|
+
//
|
|
346
|
+
// The fix has three layers:
|
|
347
|
+
// 1. Auto-cleanup: git rm --cached removes tracked runtime files from index
|
|
348
|
+
// 2. Stage-then-unstage: git add -A + git reset HEAD replaces pathspec excludes
|
|
349
|
+
// 3. Pre-checkout discard: git checkout -- .gsd/ clears dirty runtime files
|
|
350
|
+
|
|
351
|
+
const repo = initTempRepo();
|
|
352
|
+
const svc = new GitServiceImpl(repo);
|
|
353
|
+
|
|
354
|
+
// Simulate a repo where .gsd/ files were previously force-added
|
|
355
|
+
createFile(repo, ".gsd/metrics.json", '{"version":1}');
|
|
356
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1"]');
|
|
357
|
+
createFile(repo, ".gsd/activity/log.jsonl", '{"ts":1}');
|
|
358
|
+
createFile(repo, "src/real.ts", "real code");
|
|
359
|
+
// Force-add .gsd/ files to simulate historical tracking
|
|
360
|
+
runGit(repo, ["add", "-f", ".gsd/metrics.json", ".gsd/completed-units.json", ".gsd/activity/log.jsonl", "src/real.ts"]);
|
|
361
|
+
runGit(repo, ["commit", "-F", "-"], { input: "init with tracked runtime files" });
|
|
362
|
+
|
|
363
|
+
// Add .gitignore with .gsd/ (matches real-world setup from ensureGitignore)
|
|
364
|
+
createFile(repo, ".gitignore", ".gsd/\n");
|
|
365
|
+
runGit(repo, ["add", ".gitignore"]);
|
|
366
|
+
runGit(repo, ["commit", "-F", "-"], { input: "add gitignore" });
|
|
367
|
+
|
|
368
|
+
// Verify runtime files are tracked (precondition)
|
|
369
|
+
const tracked = run("git ls-files .gsd/", repo);
|
|
370
|
+
assertTrue(tracked.includes("metrics.json"), "precondition: metrics.json tracked");
|
|
371
|
+
assertTrue(tracked.includes("completed-units.json"), "precondition: completed-units.json tracked");
|
|
372
|
+
assertTrue(tracked.includes("activity/log.jsonl"), "precondition: activity log tracked");
|
|
373
|
+
|
|
374
|
+
// Now modify both runtime and real files
|
|
375
|
+
createFile(repo, ".gsd/metrics.json", '{"version":2}');
|
|
376
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1","unit2"]');
|
|
377
|
+
createFile(repo, ".gsd/activity/log.jsonl", '{"ts":2}');
|
|
378
|
+
createFile(repo, "src/real.ts", "updated code");
|
|
379
|
+
|
|
380
|
+
// autoCommit should commit real.ts. The first call also runs auto-cleanup
|
|
381
|
+
// which removes runtime files from the index via a dedicated commit.
|
|
382
|
+
const msg = svc.autoCommit("execute-task", "M001/S01/T01");
|
|
383
|
+
assertTrue(msg !== null, "autoCommit produces a commit");
|
|
384
|
+
|
|
385
|
+
const show = run("git show --stat HEAD", repo);
|
|
386
|
+
assertTrue(show.includes("src/real.ts"), "real files are committed");
|
|
387
|
+
|
|
388
|
+
// After the commit, runtime files must no longer be in the git index.
|
|
389
|
+
// They remain on disk but are untracked (protected by .gitignore).
|
|
390
|
+
const trackedAfter = run("git ls-files .gsd/", repo);
|
|
391
|
+
assertEq(trackedAfter, "", "no .gsd/ runtime files remain in the index");
|
|
392
|
+
|
|
393
|
+
// Verify a second autoCommit with changed runtime files does NOT stage them
|
|
394
|
+
createFile(repo, ".gsd/metrics.json", '{"version":3}');
|
|
395
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1","unit2","unit3"]');
|
|
396
|
+
createFile(repo, "src/real.ts", "third version");
|
|
397
|
+
|
|
398
|
+
const msg2 = svc.autoCommit("execute-task", "M001/S01/T02");
|
|
399
|
+
assertTrue(msg2 !== null, "second autoCommit produces a commit");
|
|
400
|
+
|
|
401
|
+
const show2 = run("git show --stat HEAD", repo);
|
|
402
|
+
assertTrue(show2.includes("src/real.ts"), "real files committed in second commit");
|
|
403
|
+
assertTrue(!show2.includes("metrics"), "metrics.json not in second commit");
|
|
404
|
+
assertTrue(!show2.includes("completed-units"), "completed-units.json not in second commit");
|
|
405
|
+
assertTrue(!show2.includes("activity"), "activity not in second commit");
|
|
406
|
+
|
|
407
|
+
rmSync(repo, { recursive: true, force: true });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── GitServiceImpl: autoCommit on clean repo ──────────────────────────
|
|
411
|
+
|
|
412
|
+
console.log("\n=== GitServiceImpl: autoCommit ===");
|
|
413
|
+
|
|
414
|
+
{
|
|
415
|
+
const repo = initTempRepo();
|
|
416
|
+
const svc = new GitServiceImpl(repo);
|
|
417
|
+
|
|
418
|
+
// Clean repo — autoCommit should return null
|
|
419
|
+
const cleanResult = svc.autoCommit("task", "T01");
|
|
420
|
+
assertEq(cleanResult, null, "autoCommit on clean repo returns null");
|
|
421
|
+
|
|
422
|
+
rmSync(repo, { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── GitServiceImpl: autoCommit on dirty repo ──────────────────────────
|
|
426
|
+
|
|
427
|
+
console.log("\n=== GitServiceImpl: autoCommit on dirty repo ===");
|
|
428
|
+
|
|
429
|
+
{
|
|
430
|
+
const repo = initTempRepo();
|
|
431
|
+
const svc = new GitServiceImpl(repo);
|
|
432
|
+
|
|
433
|
+
createFile(repo, "src/new-feature.ts", "export const x = 1;");
|
|
434
|
+
const msg = svc.autoCommit("task", "T01");
|
|
435
|
+
|
|
436
|
+
assertEq(msg, "chore(T01): auto-commit after task", "autoCommit returns correct message format");
|
|
437
|
+
|
|
438
|
+
// Verify the commit exists
|
|
439
|
+
const log = run("git log --oneline -1", repo);
|
|
440
|
+
assertTrue(log.includes("chore(T01): auto-commit after task"), "commit message is in git log");
|
|
441
|
+
|
|
442
|
+
rmSync(repo, { recursive: true, force: true });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ─── GitServiceImpl: empty-after-staging guard ─────────────────────────
|
|
446
|
+
|
|
447
|
+
console.log("\n=== GitServiceImpl: empty-after-staging guard ===");
|
|
448
|
+
|
|
449
|
+
{
|
|
450
|
+
const repo = initTempRepo();
|
|
451
|
+
const svc = new GitServiceImpl(repo);
|
|
452
|
+
|
|
453
|
+
// Create only runtime files
|
|
454
|
+
createFile(repo, ".gsd/activity/x.jsonl", "data");
|
|
455
|
+
|
|
456
|
+
const result = svc.autoCommit("task", "T02");
|
|
457
|
+
assertEq(result, null, "autoCommit returns null when only runtime files are dirty");
|
|
458
|
+
|
|
459
|
+
// Verify no new commit was created (should still be at init commit)
|
|
460
|
+
const logCount = run("git rev-list --count HEAD", repo);
|
|
461
|
+
assertEq(logCount, "1", "no new commit created when only runtime files changed");
|
|
462
|
+
|
|
463
|
+
rmSync(repo, { recursive: true, force: true });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ─── GitServiceImpl: autoCommit with extraExclusions ───────────────────
|
|
467
|
+
|
|
468
|
+
console.log("\n=== GitServiceImpl: autoCommit with extraExclusions ===");
|
|
469
|
+
|
|
470
|
+
{
|
|
471
|
+
const repo = initTempRepo();
|
|
472
|
+
const svc = new GitServiceImpl(repo);
|
|
473
|
+
|
|
474
|
+
// Create both a .gsd/ planning file and a regular source file
|
|
475
|
+
createFile(repo, ".gsd/milestones/M001/M001-ROADMAP.md", "- [x] S01");
|
|
476
|
+
createFile(repo, "src/feature.ts", "export const y = 2;");
|
|
477
|
+
|
|
478
|
+
// Auto-commit with .gsd/ excluded (simulates pre-switch)
|
|
479
|
+
const msg = svc.autoCommit("pre-switch", "main", [".gsd/"]);
|
|
480
|
+
assertEq(msg, "chore(main): auto-commit after pre-switch", "pre-switch autoCommit with .gsd/ exclusion commits");
|
|
481
|
+
|
|
482
|
+
// Verify .gsd/ file was NOT committed
|
|
483
|
+
const show = run("git show --stat HEAD", repo);
|
|
484
|
+
assertTrue(!show.includes("ROADMAP"), ".gsd/ files excluded from pre-switch auto-commit");
|
|
485
|
+
assertTrue(show.includes("feature.ts"), "non-.gsd/ files included in pre-switch auto-commit");
|
|
486
|
+
|
|
487
|
+
rmSync(repo, { recursive: true, force: true });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ─── GitServiceImpl: autoCommit extraExclusions — only .gsd/ dirty ────
|
|
491
|
+
|
|
492
|
+
console.log("\n=== GitServiceImpl: autoCommit extraExclusions — only .gsd/ dirty ===");
|
|
493
|
+
|
|
494
|
+
{
|
|
495
|
+
const repo = initTempRepo();
|
|
496
|
+
const svc = new GitServiceImpl(repo);
|
|
497
|
+
|
|
498
|
+
// Create only .gsd/ planning files
|
|
499
|
+
createFile(repo, ".gsd/milestones/M001/M001-ROADMAP.md", "- [x] S01");
|
|
500
|
+
createFile(repo, ".gsd/STATE.md", "state content");
|
|
501
|
+
|
|
502
|
+
// Auto-commit with .gsd/ excluded — nothing else to commit
|
|
503
|
+
const result = svc.autoCommit("pre-switch", "main", [".gsd/"]);
|
|
504
|
+
assertEq(result, null, "autoCommit returns null when only .gsd/ files are dirty and excluded");
|
|
505
|
+
|
|
506
|
+
rmSync(repo, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ─── GitServiceImpl: commit returns null when nothing staged ───────────
|
|
510
|
+
|
|
511
|
+
console.log("\n=== GitServiceImpl: commit empty ===");
|
|
512
|
+
|
|
513
|
+
{
|
|
514
|
+
const repo = initTempRepo();
|
|
515
|
+
const svc = new GitServiceImpl(repo);
|
|
516
|
+
|
|
517
|
+
// Nothing dirty, commit should return null
|
|
518
|
+
const result = svc.commit({ message: "should not commit" });
|
|
519
|
+
assertEq(result, null, "commit returns null when nothing to stage");
|
|
520
|
+
|
|
521
|
+
rmSync(repo, { recursive: true, force: true });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ─── Helper: create repo for branch tests ────────────────────────────
|
|
525
|
+
|
|
526
|
+
function initBranchTestRepo(): string {
|
|
527
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-git-t03-"));
|
|
528
|
+
run("git init -b main", dir);
|
|
529
|
+
run('git config user.name "Pi Test"', dir);
|
|
530
|
+
run('git config user.email "pi@example.com"', dir);
|
|
531
|
+
createFile(dir, ".gitkeep", "");
|
|
532
|
+
run("git add -A", dir);
|
|
533
|
+
run('git commit -m "init"', dir);
|
|
534
|
+
return dir;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ─── getCurrentBranch / isOnSliceBranch / getActiveSliceBranch ─────────
|
|
538
|
+
|
|
539
|
+
console.log("\n=== Branch queries ===");
|
|
540
|
+
|
|
541
|
+
{
|
|
542
|
+
const repo = initBranchTestRepo();
|
|
543
|
+
const svc = new GitServiceImpl(repo);
|
|
544
|
+
|
|
545
|
+
// On main
|
|
546
|
+
assertEq(svc.getCurrentBranch(), "main", "getCurrentBranch returns main on main branch");
|
|
547
|
+
assertEq(svc.isOnSliceBranch(), false, "isOnSliceBranch returns false on main");
|
|
548
|
+
assertEq(svc.getActiveSliceBranch(), null, "getActiveSliceBranch returns null on main");
|
|
549
|
+
|
|
550
|
+
// Create and checkout a slice branch manually
|
|
551
|
+
run("git checkout -b gsd/M001/S01", repo);
|
|
552
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "getCurrentBranch returns slice branch name");
|
|
553
|
+
assertEq(svc.isOnSliceBranch(), true, "isOnSliceBranch returns true on slice branch");
|
|
554
|
+
assertEq(svc.getActiveSliceBranch(), "gsd/M001/S01", "getActiveSliceBranch returns branch name on slice branch");
|
|
555
|
+
|
|
556
|
+
// Non-slice feature branch
|
|
557
|
+
run("git checkout -b feature/foo", repo);
|
|
558
|
+
assertEq(svc.isOnSliceBranch(), false, "isOnSliceBranch returns false on non-slice branch");
|
|
559
|
+
assertEq(svc.getActiveSliceBranch(), null, "getActiveSliceBranch returns null on non-slice branch");
|
|
560
|
+
|
|
561
|
+
rmSync(repo, { recursive: true, force: true });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ─── getMainBranch ────────────────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
console.log("\n=== getMainBranch ===");
|
|
567
|
+
|
|
568
|
+
{
|
|
569
|
+
const repo = initBranchTestRepo();
|
|
570
|
+
const svc = new GitServiceImpl(repo);
|
|
571
|
+
|
|
572
|
+
// Basic case: repo has "main" branch
|
|
573
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch returns main when main exists");
|
|
574
|
+
|
|
575
|
+
rmSync(repo, { recursive: true, force: true });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
{
|
|
579
|
+
// master-only repo
|
|
580
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-git-t03-master-"));
|
|
581
|
+
run("git init -b master", repo);
|
|
582
|
+
run('git config user.name "Pi Test"', repo);
|
|
583
|
+
run('git config user.email "pi@example.com"', repo);
|
|
584
|
+
createFile(repo, ".gitkeep", "");
|
|
585
|
+
run("git add -A", repo);
|
|
586
|
+
run('git commit -m "init"', repo);
|
|
587
|
+
|
|
588
|
+
const svc = new GitServiceImpl(repo);
|
|
589
|
+
assertEq(svc.getMainBranch(), "master", "getMainBranch returns master when only master exists");
|
|
590
|
+
|
|
591
|
+
rmSync(repo, { recursive: true, force: true });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ─── ensureSliceBranch: creates and checks out ────────────────────────
|
|
595
|
+
|
|
596
|
+
console.log("\n=== ensureSliceBranch ===");
|
|
597
|
+
|
|
598
|
+
{
|
|
599
|
+
const repo = initBranchTestRepo();
|
|
600
|
+
const svc = new GitServiceImpl(repo);
|
|
601
|
+
|
|
602
|
+
const created = svc.ensureSliceBranch("M001", "S01");
|
|
603
|
+
assertEq(created, true, "ensureSliceBranch returns true on first call (branch created)");
|
|
604
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "ensureSliceBranch checks out the slice branch");
|
|
605
|
+
|
|
606
|
+
rmSync(repo, { recursive: true, force: true });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ─── ensureSliceBranch: idempotent ────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
console.log("\n=== ensureSliceBranch: idempotent ===");
|
|
612
|
+
|
|
613
|
+
{
|
|
614
|
+
const repo = initBranchTestRepo();
|
|
615
|
+
const svc = new GitServiceImpl(repo);
|
|
616
|
+
|
|
617
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
618
|
+
const secondCall = svc.ensureSliceBranch("M001", "S01");
|
|
619
|
+
assertEq(secondCall, false, "ensureSliceBranch returns false when already on the branch");
|
|
620
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "still on slice branch after idempotent call");
|
|
621
|
+
|
|
622
|
+
rmSync(repo, { recursive: true, force: true });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ─── ensureSliceBranch: from non-main working branch inherits artifacts ──
|
|
626
|
+
|
|
627
|
+
console.log("\n=== ensureSliceBranch: from non-main inherits artifacts ===");
|
|
628
|
+
|
|
629
|
+
{
|
|
630
|
+
const repo = initBranchTestRepo();
|
|
631
|
+
const svc = new GitServiceImpl(repo);
|
|
632
|
+
|
|
633
|
+
// Create a feature branch with planning artifacts
|
|
634
|
+
run("git checkout -b developer", repo);
|
|
635
|
+
createFile(repo, ".gsd/milestones/M001/M001-ROADMAP.md", "# Roadmap");
|
|
636
|
+
run("git add -A", repo);
|
|
637
|
+
run('git commit -m "add roadmap"', repo);
|
|
638
|
+
|
|
639
|
+
// ensureSliceBranch from this non-main, non-slice branch
|
|
640
|
+
const created = svc.ensureSliceBranch("M001", "S01");
|
|
641
|
+
assertEq(created, true, "branch created from non-main working branch");
|
|
642
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "checked out to slice branch");
|
|
643
|
+
|
|
644
|
+
// The roadmap from developer branch should be present
|
|
645
|
+
const logOutput = run("git log --oneline", repo);
|
|
646
|
+
assertTrue(logOutput.includes("add roadmap"), "slice branch inherits artifacts from working branch");
|
|
647
|
+
|
|
648
|
+
rmSync(repo, { recursive: true, force: true });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ─── ensureSliceBranch: from another slice branch falls back to main ──
|
|
652
|
+
|
|
653
|
+
console.log("\n=== ensureSliceBranch: from slice branch falls back to main ===");
|
|
654
|
+
|
|
655
|
+
{
|
|
656
|
+
const repo = initBranchTestRepo();
|
|
657
|
+
const svc = new GitServiceImpl(repo);
|
|
658
|
+
|
|
659
|
+
// Create file only on main
|
|
660
|
+
createFile(repo, "main-only.txt", "from main");
|
|
661
|
+
run("git add -A", repo);
|
|
662
|
+
run('git commit -m "main-only file"', repo);
|
|
663
|
+
|
|
664
|
+
// Create and check out S01
|
|
665
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
666
|
+
// Add a file only on S01
|
|
667
|
+
createFile(repo, "s01-only.txt", "from s01");
|
|
668
|
+
run("git add -A", repo);
|
|
669
|
+
run('git commit -m "S01 work"', repo);
|
|
670
|
+
|
|
671
|
+
// Now create S02 from S01 — should fall back to main
|
|
672
|
+
const created = svc.ensureSliceBranch("M001", "S02");
|
|
673
|
+
assertEq(created, true, "S02 branch created from S01 (fell back to main)");
|
|
674
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S02", "on S02 branch");
|
|
675
|
+
|
|
676
|
+
// S02 should NOT have the S01-only file (it branched from main)
|
|
677
|
+
const showFiles = run("git ls-files", repo);
|
|
678
|
+
assertTrue(!showFiles.includes("s01-only.txt"), "S02 does not have S01-only files (branched from main)");
|
|
679
|
+
assertTrue(showFiles.includes("main-only.txt"), "S02 has main files");
|
|
680
|
+
|
|
681
|
+
rmSync(repo, { recursive: true, force: true });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ─── ensureSliceBranch: auto-commits dirty files via smart staging ────
|
|
685
|
+
|
|
686
|
+
console.log("\n=== ensureSliceBranch: auto-commits with smart staging ===");
|
|
687
|
+
|
|
688
|
+
{
|
|
689
|
+
const repo = initBranchTestRepo();
|
|
690
|
+
const svc = new GitServiceImpl(repo);
|
|
691
|
+
|
|
692
|
+
// Create dirty files: both real and runtime
|
|
693
|
+
createFile(repo, "src/feature.ts", "export const y = 2;");
|
|
694
|
+
createFile(repo, ".gsd/activity/session.jsonl", "session data");
|
|
695
|
+
createFile(repo, ".gsd/STATE.md", "# Current State");
|
|
696
|
+
createFile(repo, ".gsd/metrics.json", '{"tasks":1}');
|
|
697
|
+
|
|
698
|
+
// ensureSliceBranch should auto-commit before checkout
|
|
699
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
700
|
+
|
|
701
|
+
// The auto-commit on main should have src/feature.ts but NOT runtime files
|
|
702
|
+
run("git checkout main", repo);
|
|
703
|
+
const showStat = run("git show --stat --format= HEAD", repo);
|
|
704
|
+
assertTrue(showStat.includes("src/feature.ts"), "auto-commit includes real files");
|
|
705
|
+
assertTrue(!showStat.includes(".gsd/activity"), "auto-commit excludes .gsd/activity/ (smart staging)");
|
|
706
|
+
assertTrue(!showStat.includes("STATE.md"), "auto-commit excludes .gsd/STATE.md (smart staging)");
|
|
707
|
+
assertTrue(!showStat.includes("metrics.json"), "auto-commit excludes .gsd/metrics.json (smart staging)");
|
|
708
|
+
|
|
709
|
+
rmSync(repo, { recursive: true, force: true });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ─── ensureSliceBranch: tracked STATE.md + dirty (regression: "local changes overwritten") ─
|
|
713
|
+
//
|
|
714
|
+
// Reproduces: "error: Your local changes to the following files would be overwritten
|
|
715
|
+
// by checkout: .gsd/STATE.md" that occurred in gsd auto when STATE.md was historically
|
|
716
|
+
// committed to the repo (before it was added to .gitignore).
|
|
717
|
+
|
|
718
|
+
console.log("\n=== ensureSliceBranch: tracked STATE.md + dirty (checkout conflict regression) ===");
|
|
719
|
+
|
|
720
|
+
{
|
|
721
|
+
const repo = initBranchTestRepo();
|
|
722
|
+
const svc = new GitServiceImpl(repo);
|
|
723
|
+
|
|
724
|
+
// Simulate historical state: STATE.md was committed before gitignore was configured
|
|
725
|
+
createFile(repo, ".gsd/STATE.md", "# State v1");
|
|
726
|
+
run("git add -f .gsd/STATE.md", repo);
|
|
727
|
+
run('git commit -m "add state (pre-gitignore)"', repo);
|
|
728
|
+
|
|
729
|
+
// STATE.md gets modified during runtime (dirty)
|
|
730
|
+
createFile(repo, ".gsd/STATE.md", "# State v2 (modified at runtime)");
|
|
731
|
+
|
|
732
|
+
// ensureSliceBranch must not fail with "local changes would be overwritten"
|
|
733
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
734
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "checked out slice branch despite tracked+dirty STATE.md");
|
|
735
|
+
|
|
736
|
+
rmSync(repo, { recursive: true, force: true });
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ─── ensureSliceBranch: untracked STATE.md blocks checkout (regression: cleanup-commit edge case) ─
|
|
740
|
+
//
|
|
741
|
+
// Reproduces: "The following untracked working tree files would be overwritten by checkout:
|
|
742
|
+
// .gsd/STATE.md" when the smartStage cleanup commit removes STATE.md from the current
|
|
743
|
+
// branch's HEAD but the target branch was already created from the old HEAD (so it still
|
|
744
|
+
// has STATE.md tracked). Without discardUntrackedRuntimeFiles(), the untracked STATE.md
|
|
745
|
+
// on disk would block the checkout.
|
|
746
|
+
|
|
747
|
+
console.log("\n=== ensureSliceBranch: untracked runtime files blocked by target branch (cleanup-commit edge case) ===");
|
|
748
|
+
|
|
749
|
+
{
|
|
750
|
+
const repo = initBranchTestRepo();
|
|
751
|
+
|
|
752
|
+
// Simulate: STATE.md is tracked in main's HEAD (historical state)
|
|
753
|
+
createFile(repo, ".gsd/STATE.md", "# State original");
|
|
754
|
+
run("git add -f .gsd/STATE.md", repo);
|
|
755
|
+
run('git commit -m "initial with tracked STATE.md"', repo);
|
|
756
|
+
|
|
757
|
+
// Simulate what smartStage one-time cleanup does: remove STATE.md from index and commit.
|
|
758
|
+
// This leaves STATE.md on disk but removes it from main's HEAD.
|
|
759
|
+
run("git rm --cached .gsd/STATE.md", repo);
|
|
760
|
+
run('git commit -m "chore: untrack runtime files"', repo);
|
|
761
|
+
|
|
762
|
+
// STATE.md exists on disk (modified) but is now untracked in main's HEAD
|
|
763
|
+
createFile(repo, ".gsd/STATE.md", "# State modified after cleanup");
|
|
764
|
+
|
|
765
|
+
// Create slice branch — this is what ensureSliceBranch does internally but we
|
|
766
|
+
// simulate a GitServiceImpl that has already done the cleanup commit.
|
|
767
|
+
// The slice branch is created from the OLD HEAD (before cleanup commit) so it HAS
|
|
768
|
+
// STATE.md tracked. Without discardUntrackedRuntimeFiles(), the checkout would fail.
|
|
769
|
+
run("git branch gsd/M001/S01 HEAD~1", repo); // branch from HEAD~1 = the commit that had STATE.md
|
|
770
|
+
|
|
771
|
+
// Now use GitServiceImpl to switch to the already-existing slice branch
|
|
772
|
+
const svc = new GitServiceImpl(repo);
|
|
773
|
+
|
|
774
|
+
// ensureSliceBranch must succeed despite the untracked STATE.md on disk
|
|
775
|
+
// conflicting with the tracked STATE.md in the target branch
|
|
776
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
777
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "checked out slice branch (untracked runtime file removed before checkout)");
|
|
778
|
+
|
|
779
|
+
rmSync(repo, { recursive: true, force: true });
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ─── switchToMain: tracked STATE.md + dirty (regression) ─────────────
|
|
783
|
+
|
|
784
|
+
console.log("\n=== switchToMain: tracked STATE.md + dirty (checkout conflict regression) ===");
|
|
785
|
+
|
|
786
|
+
{
|
|
787
|
+
const repo = initBranchTestRepo();
|
|
788
|
+
const svc = new GitServiceImpl(repo);
|
|
789
|
+
|
|
790
|
+
// Track STATE.md on main (historical pre-gitignore state)
|
|
791
|
+
createFile(repo, ".gsd/STATE.md", "# State on main");
|
|
792
|
+
run("git add -f .gsd/STATE.md", repo);
|
|
793
|
+
run('git commit -m "add state (pre-gitignore)"', repo);
|
|
794
|
+
|
|
795
|
+
// Create slice branch (inherits STATE.md from main)
|
|
796
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
797
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "on slice branch before switchToMain");
|
|
798
|
+
|
|
799
|
+
// Modify STATE.md on slice branch (runtime update)
|
|
800
|
+
createFile(repo, ".gsd/STATE.md", "# State updated on slice branch");
|
|
801
|
+
|
|
802
|
+
// switchToMain must not fail with "local changes would be overwritten"
|
|
803
|
+
svc.switchToMain();
|
|
804
|
+
assertEq(svc.getCurrentBranch(), svc.getMainBranch(), "back on main after switchToMain despite tracked+dirty STATE.md");
|
|
805
|
+
|
|
806
|
+
rmSync(repo, { recursive: true, force: true });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ─── switchToMain ─────────────────────────────────────────────────────
|
|
810
|
+
|
|
811
|
+
console.log("\n=== switchToMain ===");
|
|
812
|
+
|
|
813
|
+
{
|
|
814
|
+
const repo = initBranchTestRepo();
|
|
815
|
+
const svc = new GitServiceImpl(repo);
|
|
816
|
+
|
|
817
|
+
// Switch to a slice branch first
|
|
818
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
819
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "on slice branch before switchToMain");
|
|
820
|
+
|
|
821
|
+
// Create dirty files
|
|
822
|
+
createFile(repo, "src/work.ts", "work in progress");
|
|
823
|
+
createFile(repo, ".gsd/activity/log.jsonl", "activity log");
|
|
824
|
+
createFile(repo, ".gsd/runtime/state.json", '{"running":true}');
|
|
825
|
+
|
|
826
|
+
svc.switchToMain();
|
|
827
|
+
assertEq(svc.getCurrentBranch(), "main", "switchToMain switches to main");
|
|
828
|
+
|
|
829
|
+
// Verify the auto-commit on the slice branch used smart staging
|
|
830
|
+
const sliceLog = run("git log gsd/M001/S01 --oneline -1", repo);
|
|
831
|
+
assertTrue(sliceLog.includes("pre-switch"), "auto-commit message includes pre-switch");
|
|
832
|
+
|
|
833
|
+
// Check that the auto-commit on the slice branch excluded runtime files
|
|
834
|
+
const showStat = run("git log gsd/M001/S01 -1 --format= --stat", repo);
|
|
835
|
+
assertTrue(showStat.includes("src/work.ts"), "switchToMain auto-commit includes real files");
|
|
836
|
+
assertTrue(!showStat.includes(".gsd/activity"), "switchToMain auto-commit excludes .gsd/activity/");
|
|
837
|
+
assertTrue(!showStat.includes(".gsd/runtime"), "switchToMain auto-commit excludes .gsd/runtime/");
|
|
838
|
+
|
|
839
|
+
rmSync(repo, { recursive: true, force: true });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ─── switchToMain: idempotent when already on main ─────────────────────
|
|
843
|
+
|
|
844
|
+
console.log("\n=== switchToMain: idempotent ===");
|
|
845
|
+
|
|
846
|
+
{
|
|
847
|
+
const repo = initBranchTestRepo();
|
|
848
|
+
const svc = new GitServiceImpl(repo);
|
|
849
|
+
|
|
850
|
+
assertEq(svc.getCurrentBranch(), "main", "already on main");
|
|
851
|
+
svc.switchToMain(); // Should not throw
|
|
852
|
+
assertEq(svc.getCurrentBranch(), "main", "still on main after idempotent switchToMain");
|
|
853
|
+
|
|
854
|
+
// Verify no extra commits were created
|
|
855
|
+
const logCount = run("git rev-list --count HEAD", repo);
|
|
856
|
+
assertEq(logCount, "1", "no extra commits from idempotent switchToMain");
|
|
857
|
+
|
|
858
|
+
rmSync(repo, { recursive: true, force: true });
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ─── mergeSliceToMain: full lifecycle with feat ─────────────────────────
|
|
862
|
+
|
|
863
|
+
console.log("\n=== mergeSliceToMain: full lifecycle ===");
|
|
864
|
+
|
|
865
|
+
{
|
|
866
|
+
const repo = initBranchTestRepo();
|
|
867
|
+
const svc = new GitServiceImpl(repo);
|
|
868
|
+
|
|
869
|
+
// Create and switch to slice branch
|
|
870
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
871
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "on slice branch for merge test");
|
|
872
|
+
|
|
873
|
+
// Do work on the slice branch
|
|
874
|
+
createFile(repo, "src/feature.ts", "export const feature = true;");
|
|
875
|
+
svc.commit({ message: "add feature module" });
|
|
876
|
+
|
|
877
|
+
// Switch to main and merge
|
|
878
|
+
svc.switchToMain();
|
|
879
|
+
const result = svc.mergeSliceToMain("M001", "S01", "Implement user authentication");
|
|
880
|
+
|
|
881
|
+
assertEq(result.mergedCommitMessage, "feat(M001/S01): Implement user authentication", "merge commit message uses feat type");
|
|
882
|
+
assertEq(result.deletedBranch, true, "branch was deleted");
|
|
883
|
+
assertEq(result.branch, "gsd/M001/S01", "result includes branch name");
|
|
884
|
+
|
|
885
|
+
// Verify commit is on main
|
|
886
|
+
const log = run("git log --oneline -1", repo);
|
|
887
|
+
assertTrue(log.includes("feat(M001/S01): Implement user authentication"), "merge commit visible in git log");
|
|
888
|
+
|
|
889
|
+
// Verify the file is on main
|
|
890
|
+
const files = run("git ls-files", repo);
|
|
891
|
+
assertTrue(files.includes("src/feature.ts"), "merged file exists on main");
|
|
892
|
+
|
|
893
|
+
// Verify slice branch is deleted
|
|
894
|
+
const branches = run("git branch", repo);
|
|
895
|
+
assertTrue(!branches.includes("gsd/M001/S01"), "slice branch deleted after merge");
|
|
896
|
+
|
|
897
|
+
rmSync(repo, { recursive: true, force: true });
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// ─── mergeSliceToMain: fix type ───────────────────────────────────────
|
|
901
|
+
|
|
902
|
+
console.log("\n=== mergeSliceToMain: fix type ===");
|
|
903
|
+
|
|
904
|
+
{
|
|
905
|
+
const repo = initBranchTestRepo();
|
|
906
|
+
const svc = new GitServiceImpl(repo);
|
|
907
|
+
|
|
908
|
+
svc.ensureSliceBranch("M001", "S02");
|
|
909
|
+
createFile(repo, "src/bugfix.ts", "// fixed");
|
|
910
|
+
svc.commit({ message: "fix the bug" });
|
|
911
|
+
|
|
912
|
+
svc.switchToMain();
|
|
913
|
+
const result = svc.mergeSliceToMain("M001", "S02", "Fix broken config");
|
|
914
|
+
|
|
915
|
+
assertTrue(result.mergedCommitMessage.startsWith("fix("), "merge commit starts with fix(");
|
|
916
|
+
assertEq(result.mergedCommitMessage, "fix(M001/S02): Fix broken config", "fix merge commit message correct");
|
|
917
|
+
|
|
918
|
+
rmSync(repo, { recursive: true, force: true });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// ─── mergeSliceToMain: docs type ──────────────────────────────────────
|
|
922
|
+
|
|
923
|
+
console.log("\n=== mergeSliceToMain: docs type ===");
|
|
924
|
+
|
|
925
|
+
{
|
|
926
|
+
const repo = initBranchTestRepo();
|
|
927
|
+
const svc = new GitServiceImpl(repo);
|
|
928
|
+
|
|
929
|
+
svc.ensureSliceBranch("M001", "S03");
|
|
930
|
+
createFile(repo, "docs/guide.md", "# Guide");
|
|
931
|
+
svc.commit({ message: "write docs" });
|
|
932
|
+
|
|
933
|
+
svc.switchToMain();
|
|
934
|
+
const result = svc.mergeSliceToMain("M001", "S03", "Docs update");
|
|
935
|
+
|
|
936
|
+
assertTrue(result.mergedCommitMessage.startsWith("docs("), "merge commit starts with docs(");
|
|
937
|
+
assertEq(result.mergedCommitMessage, "docs(M001/S03): Docs update", "docs merge commit message correct");
|
|
938
|
+
|
|
939
|
+
rmSync(repo, { recursive: true, force: true });
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ─── mergeSliceToMain: refactor type ──────────────────────────────────
|
|
943
|
+
|
|
944
|
+
console.log("\n=== mergeSliceToMain: refactor type ===");
|
|
945
|
+
|
|
946
|
+
{
|
|
947
|
+
const repo = initBranchTestRepo();
|
|
948
|
+
const svc = new GitServiceImpl(repo);
|
|
949
|
+
|
|
950
|
+
svc.ensureSliceBranch("M001", "S04");
|
|
951
|
+
createFile(repo, "src/refactored.ts", "// cleaner");
|
|
952
|
+
svc.commit({ message: "restructure modules" });
|
|
953
|
+
|
|
954
|
+
svc.switchToMain();
|
|
955
|
+
const result = svc.mergeSliceToMain("M001", "S04", "Refactor state management");
|
|
956
|
+
|
|
957
|
+
assertTrue(result.mergedCommitMessage.startsWith("refactor("), "merge commit starts with refactor(");
|
|
958
|
+
assertEq(result.mergedCommitMessage, "refactor(M001/S04): Refactor state management", "refactor merge commit message correct");
|
|
959
|
+
|
|
960
|
+
rmSync(repo, { recursive: true, force: true });
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// ─── mergeSliceToMain: error — not on main ────────────────────────────
|
|
964
|
+
|
|
965
|
+
console.log("\n=== mergeSliceToMain: error cases ===");
|
|
966
|
+
|
|
967
|
+
{
|
|
968
|
+
const repo = initBranchTestRepo();
|
|
969
|
+
const svc = new GitServiceImpl(repo);
|
|
970
|
+
|
|
971
|
+
// Create a slice branch with a commit
|
|
972
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
973
|
+
createFile(repo, "src/work.ts", "work");
|
|
974
|
+
svc.commit({ message: "slice work" });
|
|
975
|
+
|
|
976
|
+
// Try to merge while still on the slice branch
|
|
977
|
+
let threw = false;
|
|
978
|
+
try {
|
|
979
|
+
svc.mergeSliceToMain("M001", "S01", "Some feature");
|
|
980
|
+
} catch (e) {
|
|
981
|
+
threw = true;
|
|
982
|
+
const msg = (e as Error).message;
|
|
983
|
+
assertTrue(msg.includes("must be called from the main branch"), "error mentions main branch requirement");
|
|
984
|
+
assertTrue(msg.includes("gsd/M001/S01"), "error includes current branch name");
|
|
985
|
+
}
|
|
986
|
+
assertTrue(threw, "mergeSliceToMain throws when not on main");
|
|
987
|
+
|
|
988
|
+
rmSync(repo, { recursive: true, force: true });
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ─── mergeSliceToMain: error — branch doesn't exist ───────────────────
|
|
992
|
+
|
|
993
|
+
{
|
|
994
|
+
const repo = initBranchTestRepo();
|
|
995
|
+
const svc = new GitServiceImpl(repo);
|
|
996
|
+
|
|
997
|
+
let threw = false;
|
|
998
|
+
try {
|
|
999
|
+
svc.mergeSliceToMain("M001", "S99", "Nonexistent");
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
threw = true;
|
|
1002
|
+
const msg = (e as Error).message;
|
|
1003
|
+
assertTrue(msg.includes("does not exist"), "error mentions branch does not exist");
|
|
1004
|
+
assertTrue(msg.includes("gsd/M001/S99"), "error includes missing branch name");
|
|
1005
|
+
}
|
|
1006
|
+
assertTrue(threw, "mergeSliceToMain throws when branch doesn't exist");
|
|
1007
|
+
|
|
1008
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ─── mergeSliceToMain: error — no commits ahead ───────────────────────
|
|
1012
|
+
|
|
1013
|
+
{
|
|
1014
|
+
const repo = initBranchTestRepo();
|
|
1015
|
+
const svc = new GitServiceImpl(repo);
|
|
1016
|
+
|
|
1017
|
+
// Create slice branch but don't add any commits
|
|
1018
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1019
|
+
// Switch back to main without committing anything on the slice branch
|
|
1020
|
+
svc.switchToMain();
|
|
1021
|
+
|
|
1022
|
+
let threw = false;
|
|
1023
|
+
try {
|
|
1024
|
+
svc.mergeSliceToMain("M001", "S01", "Empty slice");
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
threw = true;
|
|
1027
|
+
const msg = (e as Error).message;
|
|
1028
|
+
assertTrue(msg.includes("no commits ahead"), "error mentions no commits ahead");
|
|
1029
|
+
assertTrue(msg.includes("gsd/M001/S01"), "error includes branch name");
|
|
1030
|
+
}
|
|
1031
|
+
assertTrue(threw, "mergeSliceToMain throws when no commits ahead");
|
|
1032
|
+
|
|
1033
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ─── mergeSliceToMain: auto-resolve .gsd/ planning artifact conflicts ──
|
|
1037
|
+
|
|
1038
|
+
console.log("\n=== mergeSliceToMain: auto-resolve .gsd/ planning conflicts ===");
|
|
1039
|
+
|
|
1040
|
+
{
|
|
1041
|
+
const repo = initBranchTestRepo();
|
|
1042
|
+
const svc = new GitServiceImpl(repo);
|
|
1043
|
+
|
|
1044
|
+
// Create a .gsd/ planning artifact on main (simulates reassess-roadmap)
|
|
1045
|
+
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Original decision\n");
|
|
1046
|
+
run("git add -A", repo);
|
|
1047
|
+
run('git commit -m "add decisions on main"', repo);
|
|
1048
|
+
|
|
1049
|
+
// Create slice branch and modify the same .gsd/ file differently
|
|
1050
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1051
|
+
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Original decision\n- D002: New decision from slice\n");
|
|
1052
|
+
createFile(repo, "src/feature.ts", "export const x = 1;");
|
|
1053
|
+
run("git add -A", repo);
|
|
1054
|
+
run('git commit -m "slice work with .gsd/ changes"', repo);
|
|
1055
|
+
|
|
1056
|
+
// Back on main, modify the same .gsd/ file to create a conflict
|
|
1057
|
+
svc.switchToMain();
|
|
1058
|
+
createFile(repo, ".gsd/DECISIONS.md", "# Decisions\n\n- D001: Updated decision on main\n");
|
|
1059
|
+
run("git add -A", repo);
|
|
1060
|
+
run('git commit -m "update decisions on main"', repo);
|
|
1061
|
+
|
|
1062
|
+
// Merge should auto-resolve .gsd/ conflicts by taking theirs (slice branch)
|
|
1063
|
+
const result = svc.mergeSliceToMain("M001", "S01", "Feature with .gsd/ conflicts");
|
|
1064
|
+
assertEq(result.deletedBranch, true, ".gsd/ conflict auto-resolved: branch deleted");
|
|
1065
|
+
|
|
1066
|
+
// Verify the merge succeeded and src file is present
|
|
1067
|
+
assertTrue(existsSync(join(repo, "src/feature.ts")), ".gsd/ conflict auto-resolved: src file merged");
|
|
1068
|
+
|
|
1069
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1073
|
+
// S05: Enhanced features — merge guards, snapshots, auto-push, rich commits
|
|
1074
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1075
|
+
|
|
1076
|
+
// ─── createSnapshot: prefs enabled ─────────────────────────────────────
|
|
1077
|
+
|
|
1078
|
+
console.log("\n=== createSnapshot: enabled ===");
|
|
1079
|
+
|
|
1080
|
+
{
|
|
1081
|
+
const repo = initBranchTestRepo();
|
|
1082
|
+
const svc = new GitServiceImpl(repo, { snapshots: true });
|
|
1083
|
+
|
|
1084
|
+
// Create a slice branch with a commit
|
|
1085
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1086
|
+
createFile(repo, "src/snap.ts", "snapshot me");
|
|
1087
|
+
svc.commit({ message: "snapshot test commit" });
|
|
1088
|
+
|
|
1089
|
+
// Create snapshot ref for this slice branch
|
|
1090
|
+
svc.createSnapshot("gsd/M001/S01");
|
|
1091
|
+
|
|
1092
|
+
// Verify ref exists under refs/gsd/snapshots/
|
|
1093
|
+
const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
|
|
1094
|
+
assertTrue(refs.includes("refs/gsd/snapshots/gsd/M001/S01/"), "snapshot ref created under refs/gsd/snapshots/");
|
|
1095
|
+
|
|
1096
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// ─── createSnapshot: prefs disabled ────────────────────────────────────
|
|
1100
|
+
|
|
1101
|
+
console.log("\n=== createSnapshot: disabled ===");
|
|
1102
|
+
|
|
1103
|
+
{
|
|
1104
|
+
const repo = initBranchTestRepo();
|
|
1105
|
+
const svc = new GitServiceImpl(repo, { snapshots: false });
|
|
1106
|
+
|
|
1107
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1108
|
+
createFile(repo, "src/no-snap.ts", "no snapshot");
|
|
1109
|
+
svc.commit({ message: "no snapshot commit" });
|
|
1110
|
+
|
|
1111
|
+
// createSnapshot should be a no-op when disabled
|
|
1112
|
+
svc.createSnapshot("gsd/M001/S01");
|
|
1113
|
+
|
|
1114
|
+
const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
|
|
1115
|
+
assertEq(refs, "", "no snapshot ref created when prefs.snapshots is false");
|
|
1116
|
+
|
|
1117
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ─── runPreMergeCheck: pass ────────────────────────────────────────────
|
|
1121
|
+
|
|
1122
|
+
console.log("\n=== runPreMergeCheck: pass ===");
|
|
1123
|
+
|
|
1124
|
+
{
|
|
1125
|
+
const repo = initBranchTestRepo();
|
|
1126
|
+
// Create package.json with passing test script
|
|
1127
|
+
createFile(repo, "package.json", JSON.stringify({
|
|
1128
|
+
name: "test-pass",
|
|
1129
|
+
scripts: { test: 'node -e "process.exit(0)"' },
|
|
1130
|
+
}));
|
|
1131
|
+
run("git add -A", repo);
|
|
1132
|
+
run('git commit -m "add package.json"', repo);
|
|
1133
|
+
|
|
1134
|
+
const svc = new GitServiceImpl(repo, { pre_merge_check: true });
|
|
1135
|
+
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
1136
|
+
|
|
1137
|
+
assertEq(result.passed, true, "runPreMergeCheck returns passed:true when tests pass");
|
|
1138
|
+
assertTrue(!result.skipped, "runPreMergeCheck is not skipped when enabled");
|
|
1139
|
+
|
|
1140
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// ─── runPreMergeCheck: fail ────────────────────────────────────────────
|
|
1144
|
+
|
|
1145
|
+
console.log("\n=== runPreMergeCheck: fail ===");
|
|
1146
|
+
|
|
1147
|
+
{
|
|
1148
|
+
const repo = initBranchTestRepo();
|
|
1149
|
+
// Create package.json with failing test script
|
|
1150
|
+
createFile(repo, "package.json", JSON.stringify({
|
|
1151
|
+
name: "test-fail",
|
|
1152
|
+
scripts: { test: 'node -e "process.exit(1)"' },
|
|
1153
|
+
}));
|
|
1154
|
+
run("git add -A", repo);
|
|
1155
|
+
run('git commit -m "add failing package.json"', repo);
|
|
1156
|
+
|
|
1157
|
+
const svc = new GitServiceImpl(repo, { pre_merge_check: true });
|
|
1158
|
+
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
1159
|
+
|
|
1160
|
+
assertEq(result.passed, false, "runPreMergeCheck returns passed:false when tests fail");
|
|
1161
|
+
assertTrue(!result.skipped, "runPreMergeCheck is not skipped when enabled");
|
|
1162
|
+
|
|
1163
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// ─── runPreMergeCheck: disabled ────────────────────────────────────────
|
|
1167
|
+
|
|
1168
|
+
console.log("\n=== runPreMergeCheck: disabled ===");
|
|
1169
|
+
|
|
1170
|
+
{
|
|
1171
|
+
const repo = initBranchTestRepo();
|
|
1172
|
+
createFile(repo, "package.json", JSON.stringify({
|
|
1173
|
+
name: "test-disabled",
|
|
1174
|
+
scripts: { test: 'node -e "process.exit(1)"' },
|
|
1175
|
+
}));
|
|
1176
|
+
run("git add -A", repo);
|
|
1177
|
+
run('git commit -m "add package.json"', repo);
|
|
1178
|
+
|
|
1179
|
+
const svc = new GitServiceImpl(repo, { pre_merge_check: false });
|
|
1180
|
+
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
1181
|
+
|
|
1182
|
+
assertEq(result.skipped, true, "runPreMergeCheck skipped when pre_merge_check is false");
|
|
1183
|
+
assertEq(result.passed, true, "runPreMergeCheck returns passed:true when skipped (no block)");
|
|
1184
|
+
|
|
1185
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// ─── runPreMergeCheck: custom command ──────────────────────────────────
|
|
1189
|
+
|
|
1190
|
+
console.log("\n=== runPreMergeCheck: custom command ===");
|
|
1191
|
+
|
|
1192
|
+
{
|
|
1193
|
+
const repo = initBranchTestRepo();
|
|
1194
|
+
// Custom command string overrides auto-detection
|
|
1195
|
+
const svc = new GitServiceImpl(repo, { pre_merge_check: 'node -e "process.exit(0)"' });
|
|
1196
|
+
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
1197
|
+
|
|
1198
|
+
assertEq(result.passed, true, "runPreMergeCheck passes with custom command that exits 0");
|
|
1199
|
+
assertTrue(!result.skipped, "custom command is not skipped");
|
|
1200
|
+
|
|
1201
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// ─── Rich commit message ──────────────────────────────────────────────
|
|
1205
|
+
|
|
1206
|
+
console.log("\n=== mergeSliceToMain: rich commit message ===");
|
|
1207
|
+
|
|
1208
|
+
{
|
|
1209
|
+
const repo = initBranchTestRepo();
|
|
1210
|
+
const svc = new GitServiceImpl(repo, { pre_merge_check: false });
|
|
1211
|
+
|
|
1212
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1213
|
+
|
|
1214
|
+
// Make 3 distinct commits on the slice branch
|
|
1215
|
+
createFile(repo, "src/auth.ts", "export const auth = true;");
|
|
1216
|
+
svc.commit({ message: "add auth module" });
|
|
1217
|
+
|
|
1218
|
+
createFile(repo, "src/login.ts", "export const login = true;");
|
|
1219
|
+
svc.commit({ message: "add login page" });
|
|
1220
|
+
|
|
1221
|
+
createFile(repo, "src/session.ts", "export const session = true;");
|
|
1222
|
+
svc.commit({ message: "add session handling" });
|
|
1223
|
+
|
|
1224
|
+
svc.switchToMain();
|
|
1225
|
+
const result = svc.mergeSliceToMain("M001", "S01", "Implement user authentication");
|
|
1226
|
+
|
|
1227
|
+
// Inspect the full commit body on main
|
|
1228
|
+
const commitBody = run("git log -1 --format=%B", repo);
|
|
1229
|
+
|
|
1230
|
+
// Rich commit should have the subject line
|
|
1231
|
+
assertTrue(commitBody.includes("feat(M001/S01): Implement user authentication"),
|
|
1232
|
+
"rich commit has conventional subject line");
|
|
1233
|
+
|
|
1234
|
+
// Rich commit body should include task list with commit subjects
|
|
1235
|
+
assertTrue(commitBody.includes("add auth module"),
|
|
1236
|
+
"rich commit body includes first commit subject");
|
|
1237
|
+
assertTrue(commitBody.includes("add login page"),
|
|
1238
|
+
"rich commit body includes second commit subject");
|
|
1239
|
+
assertTrue(commitBody.includes("add session handling"),
|
|
1240
|
+
"rich commit body includes third commit subject");
|
|
1241
|
+
|
|
1242
|
+
// Rich commit body should include Branch: line for forensics
|
|
1243
|
+
assertTrue(commitBody.includes("Branch:"),
|
|
1244
|
+
"rich commit body includes Branch: line");
|
|
1245
|
+
assertTrue(commitBody.includes("gsd/M001/S01"),
|
|
1246
|
+
"rich commit body Branch: line includes slice branch name");
|
|
1247
|
+
|
|
1248
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// ─── Auto-push: enabled ───────────────────────────────────────────────
|
|
1252
|
+
|
|
1253
|
+
console.log("\n=== Auto-push: enabled ===");
|
|
1254
|
+
|
|
1255
|
+
{
|
|
1256
|
+
// Create a bare remote repo
|
|
1257
|
+
const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
|
|
1258
|
+
run("git init --bare -b main", bareDir);
|
|
1259
|
+
|
|
1260
|
+
// Create local repo and add the bare as remote
|
|
1261
|
+
const repo = initBranchTestRepo();
|
|
1262
|
+
run(`git remote add origin ${bareDir}`, repo);
|
|
1263
|
+
run("git push -u origin main", repo);
|
|
1264
|
+
|
|
1265
|
+
const svc = new GitServiceImpl(repo, { auto_push: true, pre_merge_check: false });
|
|
1266
|
+
|
|
1267
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1268
|
+
createFile(repo, "src/pushed.ts", "export const pushed = true;");
|
|
1269
|
+
svc.commit({ message: "work to push" });
|
|
1270
|
+
|
|
1271
|
+
svc.switchToMain();
|
|
1272
|
+
svc.mergeSliceToMain("M001", "S01", "Add pushed feature");
|
|
1273
|
+
|
|
1274
|
+
// Verify the remote has the merge commit
|
|
1275
|
+
const remoteLog = run(`git --git-dir=${bareDir} log --oneline -1`, bareDir);
|
|
1276
|
+
assertTrue(remoteLog.includes("Add pushed feature"),
|
|
1277
|
+
"auto-push: remote has the merge commit when auto_push is true");
|
|
1278
|
+
|
|
1279
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1280
|
+
rmSync(bareDir, { recursive: true, force: true });
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// ─── Auto-push: disabled ──────────────────────────────────────────────
|
|
1284
|
+
|
|
1285
|
+
console.log("\n=== Auto-push: disabled ===");
|
|
1286
|
+
|
|
1287
|
+
{
|
|
1288
|
+
const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
|
|
1289
|
+
run("git init --bare -b main", bareDir);
|
|
1290
|
+
|
|
1291
|
+
const repo = initBranchTestRepo();
|
|
1292
|
+
run(`git remote add origin ${bareDir}`, repo);
|
|
1293
|
+
run("git push -u origin main", repo);
|
|
1294
|
+
|
|
1295
|
+
// auto_push explicitly false (or omitted — same behavior)
|
|
1296
|
+
const svc = new GitServiceImpl(repo, { auto_push: false, pre_merge_check: false });
|
|
1297
|
+
|
|
1298
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1299
|
+
createFile(repo, "src/not-pushed.ts", "export const notPushed = true;");
|
|
1300
|
+
svc.commit({ message: "work not pushed" });
|
|
1301
|
+
|
|
1302
|
+
svc.switchToMain();
|
|
1303
|
+
svc.mergeSliceToMain("M001", "S01", "Add unpushed feature");
|
|
1304
|
+
|
|
1305
|
+
// Remote should NOT have the new merge commit — still at the initial push
|
|
1306
|
+
const remoteLog = run(`git --git-dir=${bareDir} log --oneline`, bareDir);
|
|
1307
|
+
assertTrue(!remoteLog.includes("Add unpushed feature"),
|
|
1308
|
+
"auto-push: remote does NOT have merge commit when auto_push is false");
|
|
1309
|
+
|
|
1310
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1311
|
+
rmSync(bareDir, { recursive: true, force: true });
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// ─── Remote fetch before branching: with remote ────────────────────────
|
|
1315
|
+
|
|
1316
|
+
console.log("\n=== Remote fetch: with remote ===");
|
|
1317
|
+
|
|
1318
|
+
{
|
|
1319
|
+
const bareDir = mkdtempSync(join(tmpdir(), "gsd-git-bare-"));
|
|
1320
|
+
run("git init --bare -b main", bareDir);
|
|
1321
|
+
|
|
1322
|
+
const repo = initBranchTestRepo();
|
|
1323
|
+
run(`git remote add origin ${bareDir}`, repo);
|
|
1324
|
+
run("git push -u origin main", repo);
|
|
1325
|
+
|
|
1326
|
+
// Add a commit to the remote via a temporary clone
|
|
1327
|
+
const cloneDir = mkdtempSync(join(tmpdir(), "gsd-git-clone-"));
|
|
1328
|
+
run(`git clone ${bareDir} ${cloneDir}`, cloneDir);
|
|
1329
|
+
run('git config user.name "Remote Dev"', cloneDir);
|
|
1330
|
+
run('git config user.email "remote@example.com"', cloneDir);
|
|
1331
|
+
createFile(cloneDir, "remote-file.txt", "from remote");
|
|
1332
|
+
run("git add -A", cloneDir);
|
|
1333
|
+
run('git commit -m "remote commit"', cloneDir);
|
|
1334
|
+
run("git push origin main", cloneDir);
|
|
1335
|
+
|
|
1336
|
+
// ensureSliceBranch should fetch before creating the branch — no crash
|
|
1337
|
+
const svc = new GitServiceImpl(repo);
|
|
1338
|
+
let noError = true;
|
|
1339
|
+
try {
|
|
1340
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1341
|
+
} catch {
|
|
1342
|
+
noError = false;
|
|
1343
|
+
}
|
|
1344
|
+
assertTrue(noError, "ensureSliceBranch succeeds when remote has new commits (fetch runs)");
|
|
1345
|
+
|
|
1346
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1347
|
+
rmSync(bareDir, { recursive: true, force: true });
|
|
1348
|
+
rmSync(cloneDir, { recursive: true, force: true });
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// ─── Remote fetch before branching: without remote ─────────────────────
|
|
1352
|
+
|
|
1353
|
+
console.log("\n=== Remote fetch: without remote ===");
|
|
1354
|
+
|
|
1355
|
+
{
|
|
1356
|
+
const repo = initBranchTestRepo();
|
|
1357
|
+
// No remote configured — ensureSliceBranch should not crash
|
|
1358
|
+
const svc = new GitServiceImpl(repo);
|
|
1359
|
+
|
|
1360
|
+
let noError = true;
|
|
1361
|
+
try {
|
|
1362
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1363
|
+
} catch {
|
|
1364
|
+
noError = false;
|
|
1365
|
+
}
|
|
1366
|
+
assertTrue(noError, "ensureSliceBranch succeeds when no remote is configured");
|
|
1367
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "branch created even without remote");
|
|
1368
|
+
|
|
1369
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// ─── Facade prefs: mergeSliceToMain creates snapshot when prefs set ────
|
|
1373
|
+
|
|
1374
|
+
console.log("\n=== Facade prefs: snapshot via merge with prefs ===");
|
|
1375
|
+
|
|
1376
|
+
{
|
|
1377
|
+
const repo = initBranchTestRepo();
|
|
1378
|
+
// Simulate facade behavior: GitServiceImpl with snapshots:true should
|
|
1379
|
+
// create a snapshot ref during mergeSliceToMain
|
|
1380
|
+
const svc = new GitServiceImpl(repo, { snapshots: true, pre_merge_check: false });
|
|
1381
|
+
|
|
1382
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1383
|
+
createFile(repo, "src/facade-test.ts", "facade");
|
|
1384
|
+
svc.commit({ message: "facade test commit" });
|
|
1385
|
+
|
|
1386
|
+
svc.switchToMain();
|
|
1387
|
+
svc.mergeSliceToMain("M001", "S01", "Facade snapshot test");
|
|
1388
|
+
|
|
1389
|
+
// After merge, a snapshot ref should exist (created before merge)
|
|
1390
|
+
const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
|
|
1391
|
+
assertTrue(refs.includes("refs/gsd/snapshots/"), "mergeSliceToMain creates snapshot when prefs.snapshots is true");
|
|
1392
|
+
assertTrue(refs.includes("gsd/M001/S01"), "snapshot ref references the slice branch name");
|
|
1393
|
+
|
|
1394
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// ─── Facade prefs: no snapshot when prefs omit snapshots ───────────────
|
|
1398
|
+
|
|
1399
|
+
console.log("\n=== Facade prefs: no snapshot when prefs omit snapshots ===");
|
|
1400
|
+
|
|
1401
|
+
{
|
|
1402
|
+
const repo = initBranchTestRepo();
|
|
1403
|
+
// Default prefs — snapshots not enabled
|
|
1404
|
+
const svc = new GitServiceImpl(repo, { pre_merge_check: false });
|
|
1405
|
+
|
|
1406
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1407
|
+
createFile(repo, "src/no-facade-snap.ts", "no facade snap");
|
|
1408
|
+
svc.commit({ message: "no facade snapshot" });
|
|
1409
|
+
|
|
1410
|
+
svc.switchToMain();
|
|
1411
|
+
svc.mergeSliceToMain("M001", "S01", "No snapshot test");
|
|
1412
|
+
|
|
1413
|
+
// No snapshot ref should exist
|
|
1414
|
+
const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
|
|
1415
|
+
assertEq(refs, "", "no snapshot ref when snapshots pref is not set");
|
|
1416
|
+
|
|
1417
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// ─── VALID_BRANCH_NAME regex ──────────────────────────────────────────
|
|
1421
|
+
|
|
1422
|
+
console.log("\n=== VALID_BRANCH_NAME regex ===");
|
|
1423
|
+
|
|
1424
|
+
{
|
|
1425
|
+
// Valid branch names
|
|
1426
|
+
assertTrue(VALID_BRANCH_NAME.test("main"), "VALID_BRANCH_NAME accepts 'main'");
|
|
1427
|
+
assertTrue(VALID_BRANCH_NAME.test("master"), "VALID_BRANCH_NAME accepts 'master'");
|
|
1428
|
+
assertTrue(VALID_BRANCH_NAME.test("develop"), "VALID_BRANCH_NAME accepts 'develop'");
|
|
1429
|
+
assertTrue(VALID_BRANCH_NAME.test("feature/foo"), "VALID_BRANCH_NAME accepts 'feature/foo'");
|
|
1430
|
+
assertTrue(VALID_BRANCH_NAME.test("release-1.0"), "VALID_BRANCH_NAME accepts 'release-1.0'");
|
|
1431
|
+
assertTrue(VALID_BRANCH_NAME.test("my_branch"), "VALID_BRANCH_NAME accepts 'my_branch'");
|
|
1432
|
+
assertTrue(VALID_BRANCH_NAME.test("v2.0.1"), "VALID_BRANCH_NAME accepts 'v2.0.1'");
|
|
1433
|
+
|
|
1434
|
+
// Invalid / injection attempts
|
|
1435
|
+
assertTrue(!VALID_BRANCH_NAME.test("main; rm -rf /"), "VALID_BRANCH_NAME rejects shell injection");
|
|
1436
|
+
assertTrue(!VALID_BRANCH_NAME.test("main && echo pwned"), "VALID_BRANCH_NAME rejects && injection");
|
|
1437
|
+
assertTrue(!VALID_BRANCH_NAME.test(""), "VALID_BRANCH_NAME rejects empty string");
|
|
1438
|
+
assertTrue(!VALID_BRANCH_NAME.test("branch name"), "VALID_BRANCH_NAME rejects spaces");
|
|
1439
|
+
assertTrue(!VALID_BRANCH_NAME.test("branch`cmd`"), "VALID_BRANCH_NAME rejects backticks");
|
|
1440
|
+
assertTrue(!VALID_BRANCH_NAME.test("branch$(cmd)"), "VALID_BRANCH_NAME rejects $() subshell");
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// ─── getMainBranch: configured main_branch preference ──────────────────
|
|
1444
|
+
|
|
1445
|
+
console.log("\n=== getMainBranch: configured main_branch ===");
|
|
1446
|
+
|
|
1447
|
+
{
|
|
1448
|
+
const repo = initBranchTestRepo();
|
|
1449
|
+
const svc = new GitServiceImpl(repo, { main_branch: "trunk" });
|
|
1450
|
+
|
|
1451
|
+
assertEq(svc.getMainBranch(), "trunk", "getMainBranch returns configured main_branch preference");
|
|
1452
|
+
|
|
1453
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// ─── getMainBranch: falls back to auto-detection when not set ──────────
|
|
1457
|
+
|
|
1458
|
+
console.log("\n=== getMainBranch: fallback to auto-detection ===");
|
|
1459
|
+
|
|
1460
|
+
{
|
|
1461
|
+
const repo = initBranchTestRepo();
|
|
1462
|
+
const svc = new GitServiceImpl(repo, {});
|
|
1463
|
+
|
|
1464
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch falls back to auto-detection when main_branch not set");
|
|
1465
|
+
|
|
1466
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// ─── getMainBranch: ignores invalid branch names ───────────────────────
|
|
1470
|
+
|
|
1471
|
+
console.log("\n=== getMainBranch: ignores invalid branch name ===");
|
|
1472
|
+
|
|
1473
|
+
{
|
|
1474
|
+
const repo = initBranchTestRepo();
|
|
1475
|
+
const svc = new GitServiceImpl(repo, { main_branch: "main; rm -rf /" });
|
|
1476
|
+
|
|
1477
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch ignores invalid branch name and falls back to auto-detection");
|
|
1478
|
+
|
|
1479
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// ─── PreMergeCheckResult type export compile check ─────────────────────
|
|
1483
|
+
|
|
1484
|
+
console.log("\n=== PreMergeCheckResult type export ===");
|
|
1485
|
+
|
|
1486
|
+
{
|
|
1487
|
+
const _checkResult: PreMergeCheckResult = { passed: true, skipped: false };
|
|
1488
|
+
assertTrue(true, "PreMergeCheckResult type exported and usable");
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1492
|
+
// Integration branch — feature-branch workflow support
|
|
1493
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1494
|
+
|
|
1495
|
+
// ─── writeIntegrationBranch / readIntegrationBranch: round-trip ────────
|
|
1496
|
+
|
|
1497
|
+
console.log("\n=== Integration branch: write and read ===");
|
|
1498
|
+
|
|
1499
|
+
{
|
|
1500
|
+
const repo = initBranchTestRepo();
|
|
1501
|
+
|
|
1502
|
+
// Initially no integration branch
|
|
1503
|
+
assertEq(readIntegrationBranch(repo, "M001"), null, "readIntegrationBranch returns null when no metadata");
|
|
1504
|
+
|
|
1505
|
+
// Write integration branch
|
|
1506
|
+
writeIntegrationBranch(repo, "M001", "f-123-new-thing");
|
|
1507
|
+
assertEq(readIntegrationBranch(repo, "M001"), "f-123-new-thing", "readIntegrationBranch returns written branch");
|
|
1508
|
+
|
|
1509
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// ─── writeIntegrationBranch: updates when branch changes (#300) ──────
|
|
1513
|
+
|
|
1514
|
+
console.log("\n=== Integration branch: updates on branch change ===");
|
|
1515
|
+
|
|
1516
|
+
{
|
|
1517
|
+
const repo = initBranchTestRepo();
|
|
1518
|
+
|
|
1519
|
+
writeIntegrationBranch(repo, "M001", "f-123-first");
|
|
1520
|
+
writeIntegrationBranch(repo, "M001", "f-456-second"); // updates to new branch (#300)
|
|
1521
|
+
|
|
1522
|
+
assertEq(readIntegrationBranch(repo, "M001"), "f-456-second", "second write updates integration branch to new value");
|
|
1523
|
+
|
|
1524
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// ─── writeIntegrationBranch: same branch is idempotent ─────────────────
|
|
1528
|
+
|
|
1529
|
+
console.log("\n=== Integration branch: same branch is idempotent ===");
|
|
1530
|
+
|
|
1531
|
+
{
|
|
1532
|
+
const repo = initBranchTestRepo();
|
|
1533
|
+
|
|
1534
|
+
writeIntegrationBranch(repo, "M001", "f-123-first");
|
|
1535
|
+
writeIntegrationBranch(repo, "M001", "f-123-first"); // same branch — no-op
|
|
1536
|
+
|
|
1537
|
+
assertEq(readIntegrationBranch(repo, "M001"), "f-123-first", "same branch write is idempotent");
|
|
1538
|
+
|
|
1539
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// ─── writeIntegrationBranch: rejects slice branches ───────────────────
|
|
1543
|
+
|
|
1544
|
+
console.log("\n=== Integration branch: rejects slice branches ===");
|
|
1545
|
+
|
|
1546
|
+
{
|
|
1547
|
+
const repo = initBranchTestRepo();
|
|
1548
|
+
|
|
1549
|
+
writeIntegrationBranch(repo, "M001", "gsd/M001/S01");
|
|
1550
|
+
assertEq(readIntegrationBranch(repo, "M001"), null, "slice branches are not recorded as integration branch");
|
|
1551
|
+
|
|
1552
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// ─── writeIntegrationBranch: rejects invalid branch names ─────────────
|
|
1556
|
+
|
|
1557
|
+
console.log("\n=== Integration branch: rejects invalid names ===");
|
|
1558
|
+
|
|
1559
|
+
{
|
|
1560
|
+
const repo = initBranchTestRepo();
|
|
1561
|
+
|
|
1562
|
+
writeIntegrationBranch(repo, "M001", "bad; rm -rf /");
|
|
1563
|
+
assertEq(readIntegrationBranch(repo, "M001"), null, "invalid branch name is not recorded");
|
|
1564
|
+
|
|
1565
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// ─── getMainBranch: uses integration branch when milestone set ────────
|
|
1569
|
+
|
|
1570
|
+
console.log("\n=== getMainBranch: integration branch from milestone metadata ===");
|
|
1571
|
+
|
|
1572
|
+
{
|
|
1573
|
+
const repo = initBranchTestRepo();
|
|
1574
|
+
|
|
1575
|
+
// Create a feature branch
|
|
1576
|
+
run("git checkout -b f-123-feature", repo);
|
|
1577
|
+
run("git checkout main", repo);
|
|
1578
|
+
|
|
1579
|
+
// Write integration branch metadata
|
|
1580
|
+
writeIntegrationBranch(repo, "M001", "f-123-feature");
|
|
1581
|
+
|
|
1582
|
+
// Without milestone set, getMainBranch returns "main"
|
|
1583
|
+
const svc = new GitServiceImpl(repo);
|
|
1584
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch returns main when no milestone set");
|
|
1585
|
+
|
|
1586
|
+
// With milestone set, getMainBranch returns the integration branch
|
|
1587
|
+
svc.setMilestoneId("M001");
|
|
1588
|
+
assertEq(svc.getMainBranch(), "f-123-feature", "getMainBranch returns integration branch when milestone set");
|
|
1589
|
+
|
|
1590
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// ─── getMainBranch: main_branch pref still takes priority ─────────────
|
|
1594
|
+
|
|
1595
|
+
console.log("\n=== getMainBranch: main_branch pref overrides integration branch ===");
|
|
1596
|
+
|
|
1597
|
+
{
|
|
1598
|
+
const repo = initBranchTestRepo();
|
|
1599
|
+
|
|
1600
|
+
run("git checkout -b f-123-feature", repo);
|
|
1601
|
+
run("git checkout -b trunk", repo);
|
|
1602
|
+
run("git checkout main", repo);
|
|
1603
|
+
|
|
1604
|
+
writeIntegrationBranch(repo, "M001", "f-123-feature");
|
|
1605
|
+
|
|
1606
|
+
// Explicit preference still wins
|
|
1607
|
+
const svc = new GitServiceImpl(repo, { main_branch: "trunk" });
|
|
1608
|
+
svc.setMilestoneId("M001");
|
|
1609
|
+
assertEq(svc.getMainBranch(), "trunk", "main_branch preference overrides integration branch");
|
|
1610
|
+
|
|
1611
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// ─── getMainBranch: falls back when integration branch deleted ────────
|
|
1615
|
+
|
|
1616
|
+
console.log("\n=== getMainBranch: fallback when integration branch deleted ===");
|
|
1617
|
+
|
|
1618
|
+
{
|
|
1619
|
+
const repo = initBranchTestRepo();
|
|
1620
|
+
|
|
1621
|
+
// Write metadata pointing to a branch that doesn't exist
|
|
1622
|
+
writeIntegrationBranch(repo, "M001", "deleted-branch");
|
|
1623
|
+
|
|
1624
|
+
const svc = new GitServiceImpl(repo);
|
|
1625
|
+
svc.setMilestoneId("M001");
|
|
1626
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch falls back to main when integration branch no longer exists");
|
|
1627
|
+
|
|
1628
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// ─── End-to-end: feature branch workflow ──────────────────────────────
|
|
1632
|
+
|
|
1633
|
+
console.log("\n=== End-to-end: feature branch workflow ===");
|
|
1634
|
+
|
|
1635
|
+
{
|
|
1636
|
+
const repo = initBranchTestRepo();
|
|
1637
|
+
|
|
1638
|
+
// Simulate: user creates feature branch and starts GSD
|
|
1639
|
+
run("git checkout -b f-123-new-thing", repo);
|
|
1640
|
+
createFile(repo, "setup.txt", "initial setup");
|
|
1641
|
+
run("git add -A", repo);
|
|
1642
|
+
run('git commit -m "initial feature setup"', repo);
|
|
1643
|
+
|
|
1644
|
+
// Record integration branch (this is what auto.ts does at startup)
|
|
1645
|
+
writeIntegrationBranch(repo, "M001", "f-123-new-thing");
|
|
1646
|
+
|
|
1647
|
+
// Create GitServiceImpl with milestone set
|
|
1648
|
+
const svc = new GitServiceImpl(repo);
|
|
1649
|
+
svc.setMilestoneId("M001");
|
|
1650
|
+
|
|
1651
|
+
// Verify getMainBranch returns the feature branch, not "main"
|
|
1652
|
+
assertEq(svc.getMainBranch(), "f-123-new-thing", "e2e: getMainBranch returns feature branch");
|
|
1653
|
+
|
|
1654
|
+
// Create slice branch — should branch from f-123-new-thing (current)
|
|
1655
|
+
svc.ensureSliceBranch("M001", "S01");
|
|
1656
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "e2e: slice branch created");
|
|
1657
|
+
|
|
1658
|
+
// The slice branch should have the feature branch's commit
|
|
1659
|
+
const log = run("git log --oneline", repo);
|
|
1660
|
+
assertTrue(log.includes("initial feature setup"), "e2e: slice branch inherits feature branch content");
|
|
1661
|
+
|
|
1662
|
+
// Do work on the slice branch
|
|
1663
|
+
createFile(repo, "src/feature.ts", "export const feature = true;");
|
|
1664
|
+
svc.commit({ message: "feat: add feature module" });
|
|
1665
|
+
|
|
1666
|
+
// switchToMain should go to feature branch
|
|
1667
|
+
svc.switchToMain();
|
|
1668
|
+
assertEq(svc.getCurrentBranch(), "f-123-new-thing", "e2e: switchToMain goes to feature branch, not main");
|
|
1669
|
+
|
|
1670
|
+
// mergeSliceToMain should merge into feature branch
|
|
1671
|
+
const result = svc.mergeSliceToMain("M001", "S01", "Add feature module");
|
|
1672
|
+
assertEq(result.mergedCommitMessage, "feat(M001/S01): Add feature module", "e2e: merge commit message correct");
|
|
1673
|
+
assertEq(svc.getCurrentBranch(), "f-123-new-thing", "e2e: after merge, still on feature branch");
|
|
1674
|
+
|
|
1675
|
+
// The feature branch should have the merged work
|
|
1676
|
+
const files = run("git ls-files", repo);
|
|
1677
|
+
assertTrue(files.includes("src/feature.ts"), "e2e: merged file exists on feature branch");
|
|
1678
|
+
|
|
1679
|
+
// Main should NOT have the merged work
|
|
1680
|
+
run("git checkout main", repo);
|
|
1681
|
+
const mainFiles = run("git ls-files", repo);
|
|
1682
|
+
assertTrue(!mainFiles.includes("src/feature.ts"), "e2e: main does NOT have merged work — it stays on the feature branch");
|
|
1683
|
+
|
|
1684
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// ─── Per-milestone isolation: different milestones, different targets ──
|
|
1688
|
+
|
|
1689
|
+
console.log("\n=== Integration branch: per-milestone isolation ===");
|
|
1690
|
+
|
|
1691
|
+
{
|
|
1692
|
+
const repo = initBranchTestRepo();
|
|
1693
|
+
|
|
1694
|
+
run("git checkout -b feature-a", repo);
|
|
1695
|
+
run("git checkout -b feature-b", repo);
|
|
1696
|
+
run("git checkout main", repo);
|
|
1697
|
+
|
|
1698
|
+
writeIntegrationBranch(repo, "M001", "feature-a");
|
|
1699
|
+
writeIntegrationBranch(repo, "M002", "feature-b");
|
|
1700
|
+
|
|
1701
|
+
const svc = new GitServiceImpl(repo);
|
|
1702
|
+
|
|
1703
|
+
svc.setMilestoneId("M001");
|
|
1704
|
+
assertEq(svc.getMainBranch(), "feature-a", "M001 integration branch is feature-a");
|
|
1705
|
+
|
|
1706
|
+
svc.setMilestoneId("M002");
|
|
1707
|
+
assertEq(svc.getMainBranch(), "feature-b", "M002 integration branch is feature-b");
|
|
1708
|
+
|
|
1709
|
+
svc.setMilestoneId(null);
|
|
1710
|
+
assertEq(svc.getMainBranch(), "main", "no milestone set → falls back to main");
|
|
1711
|
+
|
|
1712
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// ─── Backward compatibility: no metadata → existing behavior ──────────
|
|
1716
|
+
|
|
1717
|
+
console.log("\n=== Integration branch: backward compat ===");
|
|
1718
|
+
|
|
1719
|
+
{
|
|
1720
|
+
const repo = initBranchTestRepo();
|
|
1721
|
+
const svc = new GitServiceImpl(repo);
|
|
1722
|
+
|
|
1723
|
+
// Set milestone but no metadata file exists
|
|
1724
|
+
svc.setMilestoneId("M001");
|
|
1725
|
+
assertEq(svc.getMainBranch(), "main", "backward compat: no metadata file → falls back to main");
|
|
1726
|
+
|
|
1727
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// ─── untrackRuntimeFiles: removes tracked runtime files from index ───
|
|
1731
|
+
|
|
1732
|
+
console.log("\n=== untrackRuntimeFiles ===");
|
|
1733
|
+
|
|
1734
|
+
{
|
|
1735
|
+
const { untrackRuntimeFiles } = await import("../gitignore.ts");
|
|
1736
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-untrack-"));
|
|
1737
|
+
run("git init -b main", repo);
|
|
1738
|
+
run("git config user.email test@test.com", repo);
|
|
1739
|
+
run("git config user.name Test", repo);
|
|
1740
|
+
|
|
1741
|
+
// Create and track runtime files (simulates pre-.gitignore state)
|
|
1742
|
+
mkdirSync(join(repo, ".gsd", "activity"), { recursive: true });
|
|
1743
|
+
mkdirSync(join(repo, ".gsd", "runtime"), { recursive: true });
|
|
1744
|
+
writeFileSync(join(repo, ".gsd", "completed-units.json"), '["u1"]');
|
|
1745
|
+
writeFileSync(join(repo, ".gsd", "metrics.json"), '{}');
|
|
1746
|
+
writeFileSync(join(repo, ".gsd", "STATE.md"), "# State");
|
|
1747
|
+
writeFileSync(join(repo, ".gsd", "activity", "log.jsonl"), "{}");
|
|
1748
|
+
writeFileSync(join(repo, ".gsd", "runtime", "data.json"), "{}");
|
|
1749
|
+
writeFileSync(join(repo, "src.ts"), "code");
|
|
1750
|
+
run("git add -A", repo);
|
|
1751
|
+
run("git commit -m init", repo);
|
|
1752
|
+
|
|
1753
|
+
// Precondition: runtime files are tracked
|
|
1754
|
+
const trackedBefore = run("git ls-files .gsd/", repo);
|
|
1755
|
+
assertTrue(trackedBefore.includes("completed-units.json"), "untrack: precondition — completed-units tracked");
|
|
1756
|
+
assertTrue(trackedBefore.includes("metrics.json"), "untrack: precondition — metrics tracked");
|
|
1757
|
+
|
|
1758
|
+
// Run untrackRuntimeFiles
|
|
1759
|
+
untrackRuntimeFiles(repo);
|
|
1760
|
+
|
|
1761
|
+
// Runtime files should be removed from the index
|
|
1762
|
+
const trackedAfter = run("git ls-files .gsd/", repo);
|
|
1763
|
+
assertEq(trackedAfter, "", "untrack: all runtime files removed from index");
|
|
1764
|
+
|
|
1765
|
+
// Non-runtime files remain tracked
|
|
1766
|
+
const srcTracked = run("git ls-files src.ts", repo);
|
|
1767
|
+
assertTrue(srcTracked.includes("src.ts"), "untrack: non-runtime files remain tracked");
|
|
1768
|
+
|
|
1769
|
+
// Files still exist on disk
|
|
1770
|
+
assertTrue(existsSync(join(repo, ".gsd", "completed-units.json")),
|
|
1771
|
+
"untrack: completed-units.json still on disk");
|
|
1772
|
+
assertTrue(existsSync(join(repo, ".gsd", "metrics.json")),
|
|
1773
|
+
"untrack: metrics.json still on disk");
|
|
1774
|
+
|
|
1775
|
+
// Idempotent — running again doesn't error
|
|
1776
|
+
untrackRuntimeFiles(repo);
|
|
1777
|
+
assertTrue(true, "untrack: second call is idempotent (no error)");
|
|
1778
|
+
|
|
1779
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
report();
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
main().catch((error) => {
|
|
1786
|
+
console.error(error);
|
|
1787
|
+
process.exit(1);
|
|
1788
|
+
});
|