gsd-pi 2.44.0-dev.848dd4c → 2.44.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/README.md +12 -30
- package/dist/resources/extensions/gsd/auto-start.js +0 -10
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +0 -5
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/chunks/229.js +1 -1
- package/dist/web/standalone/.next/server/chunks/471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-f2a7482d42a5614b.js → page-2f24283c162b6ab3.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-a16c7a7ecdf0c2cf.js → layout-9ecfd95f343793f0.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-7e9530a7122506c5.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +8 -6
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +26 -24
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/fs-utils.test.js +48 -29
- package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +44 -34
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +34 -30
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +12 -10
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +47 -43
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
- package/packages/pi-coding-agent/src/core/fs-utils.test.ts +43 -31
- package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +45 -40
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
- package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
- package/src/resources/extensions/gsd/auto-start.ts +0 -14
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +0 -8
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +16 -14
- package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +57 -43
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +13 -11
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +523 -465
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +75 -73
- package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +56 -34
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +656 -533
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +143 -165
- package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +52 -29
- package/src/resources/extensions/gsd/tests/captures.test.ts +176 -148
- package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +33 -32
- package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +143 -141
- package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
- package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +59 -38
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +263 -228
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +302 -250
- package/src/resources/extensions/gsd/tests/context-store.test.ts +367 -354
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +72 -68
- package/src/resources/extensions/gsd/tests/cost-projection.test.ts +106 -92
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +35 -27
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +237 -220
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +420 -390
- package/src/resources/extensions/gsd/tests/definition-loader.test.ts +92 -76
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +83 -68
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +183 -152
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +101 -78
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +227 -192
- package/src/resources/extensions/gsd/tests/detection.test.ts +278 -232
- package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +34 -30
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +180 -164
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +49 -43
- package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +32 -28
- package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +29 -27
- package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +38 -34
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +75 -54
- package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +32 -21
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +97 -72
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +44 -38
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +145 -104
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +106 -84
- package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +60 -54
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +93 -72
- package/src/resources/extensions/gsd/tests/doctor.test.ts +134 -104
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +131 -123
- package/src/resources/extensions/gsd/tests/exit-command.test.ts +24 -20
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +57 -48
- package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +7 -5
- package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +42 -30
- package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +206 -198
- package/src/resources/extensions/gsd/tests/git-locale.test.ts +27 -13
- package/src/resources/extensions/gsd/tests/git-service.test.ts +388 -285
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +39 -31
- package/src/resources/extensions/gsd/tests/graph-operations.test.ts +69 -63
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +264 -255
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +119 -108
- package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +103 -81
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +262 -229
- package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +37 -29
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +102 -81
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +18 -16
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +46 -41
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +53 -42
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +91 -75
- package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +194 -150
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +125 -101
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +54 -45
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +93 -80
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +66 -57
- package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +93 -83
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +170 -161
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +141 -125
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +131 -107
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +96 -87
- package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +164 -125
- package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +94 -81
- package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +36 -35
- package/src/resources/extensions/gsd/tests/overrides.test.ts +106 -99
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +47 -40
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +28 -25
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +83 -66
- package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +77 -54
- package/src/resources/extensions/gsd/tests/park-milestone.test.ts +115 -68
- package/src/resources/extensions/gsd/tests/parsers.test.ts +611 -546
- package/src/resources/extensions/gsd/tests/paths.test.ts +87 -72
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +117 -77
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
- package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +119 -93
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +82 -70
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +55 -42
- package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +73 -45
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +38 -28
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +80 -73
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +74 -71
- package/src/resources/extensions/gsd/tests/requirements.test.ts +75 -70
- package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +66 -44
- package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +181 -114
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +65 -63
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +128 -66
- package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +25 -18
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +44 -37
- package/src/resources/extensions/gsd/tests/shared-wal.test.ts +26 -19
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +28 -22
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +56 -54
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +25 -23
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +11 -9
- package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +82 -66
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +47 -46
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +22 -20
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +86 -84
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +43 -41
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +96 -94
- package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +13 -11
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +29 -27
- package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +52 -50
- package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +13 -10
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +18 -14
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +39 -38
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +21 -17
- package/src/resources/extensions/gsd/tests/worktree-health.test.ts +30 -25
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +37 -30
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +22 -15
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +66 -59
- package/src/resources/extensions/gsd/tests/worktree.test.ts +50 -44
- package/dist/web/standalone/.next/static/chunks/app/page-b9367c5ae13b99c6.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +0 -1
- package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +0 -100
- package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +0 -63
- /package/dist/web/standalone/.next/static/{-zps1Q9mQmioAKLcQiCr8 → mgkxN0mGP6gSUmGPEzbk_}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-zps1Q9mQmioAKLcQiCr8 → mgkxN0mGP6gSUmGPEzbk_}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { describe, test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
1
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, symlinkSync } from "node:fs";
|
|
4
2
|
import { join, dirname } from "node:path";
|
|
5
3
|
import { tmpdir } from "node:os";
|
|
@@ -22,170 +20,174 @@ import {
|
|
|
22
20
|
type TaskCommitContext,
|
|
23
21
|
} from "../git-service.ts";
|
|
24
22
|
import { nativeAddAllWithExclusions } from "../native-git-bridge.ts";
|
|
23
|
+
import { createTestContext } from './test-helpers.ts';
|
|
24
|
+
|
|
25
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
25
26
|
function run(command: string, cwd: string): string {
|
|
26
27
|
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
async function main(): Promise<void> {
|
|
30
31
|
// ─── inferCommitType ───────────────────────────────────────────────────
|
|
31
32
|
|
|
33
|
+
console.log("\n=== inferCommitType ===");
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
assertEq(
|
|
34
36
|
inferCommitType("Implement user authentication"),
|
|
35
37
|
"feat",
|
|
36
38
|
"generic feature title → feat"
|
|
37
39
|
);
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
assertEq(
|
|
40
42
|
inferCommitType("Add dashboard page"),
|
|
41
43
|
"feat",
|
|
42
44
|
"add-style title → feat"
|
|
43
45
|
);
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
assertEq(
|
|
46
48
|
inferCommitType("Fix login redirect bug"),
|
|
47
49
|
"fix",
|
|
48
50
|
"title with 'fix' → fix"
|
|
49
51
|
);
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
assertEq(
|
|
52
54
|
inferCommitType("Bug in session handling"),
|
|
53
55
|
"fix",
|
|
54
56
|
"title with 'bug' → fix"
|
|
55
57
|
);
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
assertEq(
|
|
58
60
|
inferCommitType("Hotfix for production crash"),
|
|
59
61
|
"fix",
|
|
60
62
|
"title with 'hotfix' → fix"
|
|
61
63
|
);
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
assertEq(
|
|
64
66
|
inferCommitType("Patch memory leak"),
|
|
65
67
|
"fix",
|
|
66
68
|
"title with 'patch' → fix"
|
|
67
69
|
);
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
assertEq(
|
|
70
72
|
inferCommitType("Refactor state management"),
|
|
71
73
|
"refactor",
|
|
72
74
|
"title with 'refactor' → refactor"
|
|
73
75
|
);
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
assertEq(
|
|
76
78
|
inferCommitType("Restructure project layout"),
|
|
77
79
|
"refactor",
|
|
78
80
|
"title with 'restructure' → refactor"
|
|
79
81
|
);
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
assertEq(
|
|
82
84
|
inferCommitType("Reorganize module imports"),
|
|
83
85
|
"refactor",
|
|
84
86
|
"title with 'reorganize' → refactor"
|
|
85
87
|
);
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
assertEq(
|
|
88
90
|
inferCommitType("Update API documentation"),
|
|
89
91
|
"docs",
|
|
90
92
|
"title with 'documentation' → docs"
|
|
91
93
|
);
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
assertEq(
|
|
94
96
|
inferCommitType("Add doc for setup guide"),
|
|
95
97
|
"docs",
|
|
96
98
|
"title with 'doc' → docs"
|
|
97
99
|
);
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
assertEq(
|
|
100
102
|
inferCommitType("Add unit tests for auth"),
|
|
101
103
|
"test",
|
|
102
104
|
"title with 'tests' → test"
|
|
103
105
|
);
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
assertEq(
|
|
106
108
|
inferCommitType("Testing infrastructure setup"),
|
|
107
109
|
"test",
|
|
108
110
|
"title with 'testing' → test"
|
|
109
111
|
);
|
|
110
112
|
|
|
111
|
-
|
|
113
|
+
assertEq(
|
|
112
114
|
inferCommitType("Chore: update dependencies"),
|
|
113
115
|
"chore",
|
|
114
116
|
"title with 'chore' → chore"
|
|
115
117
|
);
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
assertEq(
|
|
118
120
|
inferCommitType("Cleanup unused imports"),
|
|
119
121
|
"chore",
|
|
120
122
|
"title with 'cleanup' → chore"
|
|
121
123
|
);
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
assertEq(
|
|
124
126
|
inferCommitType("Clean up stale branches"),
|
|
125
127
|
"chore",
|
|
126
128
|
"title with 'clean up' → chore"
|
|
127
129
|
);
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
assertEq(
|
|
130
132
|
inferCommitType("Archive old milestones"),
|
|
131
133
|
"chore",
|
|
132
134
|
"title with 'archive' → chore"
|
|
133
135
|
);
|
|
134
136
|
|
|
135
|
-
|
|
137
|
+
assertEq(
|
|
136
138
|
inferCommitType("Remove deprecated endpoints"),
|
|
137
139
|
"chore",
|
|
138
140
|
"title with 'remove' → chore"
|
|
139
141
|
);
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
assertEq(
|
|
142
144
|
inferCommitType("Delete temp files"),
|
|
143
145
|
"chore",
|
|
144
146
|
"title with 'delete' → chore"
|
|
145
147
|
);
|
|
146
148
|
|
|
147
149
|
// Mixed keywords — first match wins
|
|
148
|
-
|
|
150
|
+
assertEq(
|
|
149
151
|
inferCommitType("Fix and refactor the login module"),
|
|
150
152
|
"fix",
|
|
151
153
|
"mixed keywords → first match wins (fix before refactor)"
|
|
152
154
|
);
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
assertEq(
|
|
155
157
|
inferCommitType("Refactor test utilities"),
|
|
156
158
|
"refactor",
|
|
157
159
|
"mixed keywords → first match wins (refactor before test)"
|
|
158
160
|
);
|
|
159
161
|
|
|
160
162
|
// Unknown / unrecognized title → feat
|
|
161
|
-
|
|
163
|
+
assertEq(
|
|
162
164
|
inferCommitType("Build the new pipeline"),
|
|
163
165
|
"feat",
|
|
164
166
|
"unrecognized title → feat"
|
|
165
167
|
);
|
|
166
168
|
|
|
167
|
-
|
|
169
|
+
assertEq(
|
|
168
170
|
inferCommitType(""),
|
|
169
171
|
"feat",
|
|
170
172
|
"empty title → feat"
|
|
171
173
|
);
|
|
172
174
|
|
|
173
175
|
// Word boundary: "testify" should NOT match "test"
|
|
174
|
-
|
|
176
|
+
assertEq(
|
|
175
177
|
inferCommitType("Testify integration"),
|
|
176
178
|
"feat",
|
|
177
179
|
"'testify' does not match 'test' — word boundary prevents partial match"
|
|
178
180
|
);
|
|
179
181
|
|
|
180
182
|
// "documentary" should NOT match "doc" (word boundary)
|
|
181
|
-
|
|
183
|
+
assertEq(
|
|
182
184
|
inferCommitType("Documentary style UI"),
|
|
183
185
|
"feat",
|
|
184
186
|
"'documentary' does not match 'doc' — word boundary prevents partial match"
|
|
185
187
|
);
|
|
186
188
|
|
|
187
189
|
// "prefix" should NOT match "fix" (word boundary)
|
|
188
|
-
|
|
190
|
+
assertEq(
|
|
189
191
|
inferCommitType("Add prefix to all IDs"),
|
|
190
192
|
"feat",
|
|
191
193
|
"'prefix' does not match 'fix' — word boundary prevents partial match"
|
|
@@ -193,14 +195,15 @@ describe('git-service', async () => {
|
|
|
193
195
|
|
|
194
196
|
// ─── inferCommitType with oneLiner ──────────────────────────────────────
|
|
195
197
|
|
|
198
|
+
console.log("\n=== inferCommitType with oneLiner ===");
|
|
196
199
|
|
|
197
|
-
|
|
200
|
+
assertEq(
|
|
198
201
|
inferCommitType("implement dashboard", "Fixed rendering bug in sidebar"),
|
|
199
202
|
"fix",
|
|
200
203
|
"one-liner with 'fixed' overrides generic title → fix"
|
|
201
204
|
);
|
|
202
205
|
|
|
203
|
-
|
|
206
|
+
assertEq(
|
|
204
207
|
inferCommitType("add search", "Optimized query performance with caching"),
|
|
205
208
|
"perf",
|
|
206
209
|
"one-liner with 'performance' and 'caching' → perf"
|
|
@@ -208,27 +211,29 @@ describe('git-service', async () => {
|
|
|
208
211
|
|
|
209
212
|
// ─── buildTaskCommitMessage ─────────────────────────────────────────────
|
|
210
213
|
|
|
211
|
-
|
|
214
|
+
console.log("\n=== buildTaskCommitMessage ===");
|
|
215
|
+
|
|
216
|
+
{
|
|
212
217
|
const msg = buildTaskCommitMessage({
|
|
213
218
|
taskId: "S01/T02",
|
|
214
219
|
taskTitle: "implement user authentication",
|
|
215
220
|
oneLiner: "Added JWT-based auth with refresh token rotation",
|
|
216
221
|
keyFiles: ["src/auth.ts", "src/middleware/jwt.ts"],
|
|
217
222
|
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
+
assertTrue(msg.startsWith("feat(S01/T02):"), "message starts with type(scope)");
|
|
224
|
+
assertTrue(msg.includes("JWT-based auth"), "message includes one-liner content");
|
|
225
|
+
assertTrue(msg.includes("- src/auth.ts"), "message body includes key files");
|
|
226
|
+
assertTrue(msg.includes("- src/middleware/jwt.ts"), "message body includes second key file");
|
|
227
|
+
}
|
|
223
228
|
|
|
224
229
|
{
|
|
225
230
|
const msg = buildTaskCommitMessage({
|
|
226
231
|
taskId: "S02/T01",
|
|
227
232
|
taskTitle: "fix login redirect bug",
|
|
228
233
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
assertTrue(msg.startsWith("fix(S02/T01):"), "infers fix type from title");
|
|
235
|
+
assertTrue(msg.includes("fix login redirect bug"), "uses task title when no one-liner");
|
|
236
|
+
assertTrue(!msg.includes("\n"), "no body when no key files");
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
{
|
|
@@ -237,13 +242,14 @@ describe('git-service', async () => {
|
|
|
237
242
|
taskTitle: "add tests",
|
|
238
243
|
oneLiner: "Unit tests for auth module with coverage",
|
|
239
244
|
});
|
|
240
|
-
|
|
245
|
+
assertTrue(msg.startsWith("test(S01/T03):"), "infers test type");
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
// ─── RUNTIME_EXCLUSION_PATHS ───────────────────────────────────────────
|
|
244
249
|
|
|
250
|
+
console.log("\n=== RUNTIME_EXCLUSION_PATHS ===");
|
|
245
251
|
|
|
246
|
-
|
|
252
|
+
assertEq(
|
|
247
253
|
RUNTIME_EXCLUSION_PATHS.length,
|
|
248
254
|
13,
|
|
249
255
|
"exactly 13 runtime exclusion paths"
|
|
@@ -265,23 +271,24 @@ describe('git-service', async () => {
|
|
|
265
271
|
".gsd/DISCUSSION-MANIFEST.json",
|
|
266
272
|
];
|
|
267
273
|
|
|
268
|
-
|
|
274
|
+
assertEq(
|
|
269
275
|
[...RUNTIME_EXCLUSION_PATHS],
|
|
270
276
|
expectedPaths,
|
|
271
277
|
"paths match expected set in order"
|
|
272
278
|
);
|
|
273
279
|
|
|
274
|
-
|
|
280
|
+
assertTrue(
|
|
275
281
|
RUNTIME_EXCLUSION_PATHS.includes(".gsd/activity/"),
|
|
276
282
|
"includes .gsd/activity/"
|
|
277
283
|
);
|
|
278
|
-
|
|
284
|
+
assertTrue(
|
|
279
285
|
RUNTIME_EXCLUSION_PATHS.includes(".gsd/STATE.md"),
|
|
280
286
|
"includes .gsd/STATE.md"
|
|
281
287
|
);
|
|
282
288
|
|
|
283
289
|
// ─── runGit ────────────────────────────────────────────────────────────
|
|
284
290
|
|
|
291
|
+
console.log("\n=== runGit ===");
|
|
285
292
|
|
|
286
293
|
const tempDir = mkdtempSync(join(tmpdir(), "gsd-git-service-test-"));
|
|
287
294
|
run("git init -b main", tempDir);
|
|
@@ -290,11 +297,11 @@ describe('git-service', async () => {
|
|
|
290
297
|
|
|
291
298
|
// runGit should work on a valid repo
|
|
292
299
|
const branch = runGit(tempDir, ["branch", "--show-current"]);
|
|
293
|
-
|
|
300
|
+
assertEq(branch, "main", "runGit returns current branch");
|
|
294
301
|
|
|
295
302
|
// runGit allowFailure returns empty string on failure
|
|
296
303
|
const result = runGit(tempDir, ["log", "--oneline"], { allowFailure: true });
|
|
297
|
-
|
|
304
|
+
assertEq(result, "", "runGit allowFailure returns empty on error (no commits yet)");
|
|
298
305
|
|
|
299
306
|
// runGit throws on failure without allowFailure
|
|
300
307
|
let threw = false;
|
|
@@ -302,21 +309,22 @@ describe('git-service', async () => {
|
|
|
302
309
|
runGit(tempDir, ["log", "--oneline"]);
|
|
303
310
|
} catch (e) {
|
|
304
311
|
threw = true;
|
|
305
|
-
|
|
312
|
+
assertTrue(
|
|
306
313
|
(e as Error).message.includes("git log --oneline failed"),
|
|
307
314
|
"error message includes command and path"
|
|
308
315
|
);
|
|
309
316
|
}
|
|
310
|
-
|
|
317
|
+
assertTrue(threw, "runGit throws without allowFailure on error");
|
|
311
318
|
|
|
312
319
|
// ─── Type exports compile check ────────────────────────────────────────
|
|
313
320
|
|
|
321
|
+
console.log("\n=== Type exports ===");
|
|
314
322
|
|
|
315
323
|
// These are compile-time checks — if we got here, the types import fine
|
|
316
324
|
const _prefs: GitPreferences = { auto_push: true, remote: "origin" };
|
|
317
325
|
const _opts: CommitOptions = { message: "test" };
|
|
318
|
-
|
|
319
|
-
|
|
326
|
+
assertTrue(true, "GitPreferences type exported and usable");
|
|
327
|
+
assertTrue(true, "CommitOptions type exported and usable");
|
|
320
328
|
|
|
321
329
|
// Cleanup T01 temp dir
|
|
322
330
|
rmSync(tempDir, { recursive: true, force: true });
|
|
@@ -343,7 +351,9 @@ describe('git-service', async () => {
|
|
|
343
351
|
|
|
344
352
|
// ─── GitServiceImpl: smart staging ─────────────────────────────────────
|
|
345
353
|
|
|
346
|
-
|
|
354
|
+
console.log("\n=== GitServiceImpl: smart staging ===");
|
|
355
|
+
|
|
356
|
+
{
|
|
347
357
|
const repo = initTempRepo();
|
|
348
358
|
const svc = new GitServiceImpl(repo);
|
|
349
359
|
|
|
@@ -360,32 +370,34 @@ describe('git-service', async () => {
|
|
|
360
370
|
|
|
361
371
|
const result = svc.commit({ message: "test: smart staging" });
|
|
362
372
|
|
|
363
|
-
|
|
373
|
+
assertEq(result, "test: smart staging", "commit returns the commit message");
|
|
364
374
|
|
|
365
375
|
// Verify only src/code.ts is in the commit
|
|
366
376
|
const showStat = run("git show --stat --format= HEAD", repo);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
assertTrue(showStat.includes("src/code.ts"), "src/code.ts is in the commit");
|
|
378
|
+
assertTrue(!showStat.includes(".gsd/activity"), ".gsd/activity/ excluded from commit");
|
|
379
|
+
assertTrue(!showStat.includes(".gsd/runtime"), ".gsd/runtime/ excluded from commit");
|
|
380
|
+
assertTrue(!showStat.includes("STATE.md"), ".gsd/STATE.md excluded from commit");
|
|
381
|
+
assertTrue(!showStat.includes("auto.lock"), ".gsd/auto.lock excluded from commit");
|
|
382
|
+
assertTrue(!showStat.includes("metrics.json"), ".gsd/metrics.json excluded from commit");
|
|
383
|
+
assertTrue(!showStat.includes(".gsd/worktrees"), ".gsd/worktrees/ excluded from commit");
|
|
374
384
|
|
|
375
385
|
// Verify runtime files are still untracked
|
|
376
386
|
// git status --short may collapse to "?? .gsd/" or show individual files
|
|
377
387
|
// Use --untracked-files=all to force individual listing
|
|
378
388
|
const statusOut = run("git status --short --untracked-files=all", repo);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
389
|
+
assertTrue(statusOut.includes(".gsd/activity/"), "activity still untracked after commit");
|
|
390
|
+
assertTrue(statusOut.includes(".gsd/runtime/"), "runtime still untracked after commit");
|
|
391
|
+
assertTrue(statusOut.includes(".gsd/STATE.md"), "STATE.md still untracked after commit");
|
|
382
392
|
|
|
383
393
|
rmSync(repo, { recursive: true, force: true });
|
|
384
|
-
}
|
|
394
|
+
}
|
|
385
395
|
|
|
386
396
|
// ─── GitServiceImpl: smart staging excludes tracked runtime files ──────
|
|
387
397
|
|
|
388
|
-
|
|
398
|
+
console.log("\n=== GitServiceImpl: smart staging excludes tracked runtime files ===");
|
|
399
|
+
|
|
400
|
+
{
|
|
389
401
|
// Reproduces the real bug: .gsd/ runtime files that are already tracked
|
|
390
402
|
// (in the git index) must be excluded from staging even when .gsd/ is
|
|
391
403
|
// in .gitignore. The old pathspec-exclude approach failed silently in
|
|
@@ -415,9 +427,9 @@ describe('git-service', async () => {
|
|
|
415
427
|
|
|
416
428
|
// Verify runtime files are tracked (precondition)
|
|
417
429
|
const tracked = run("git ls-files .gsd/", repo);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
430
|
+
assertTrue(tracked.includes("metrics.json"), "precondition: metrics.json tracked");
|
|
431
|
+
assertTrue(tracked.includes("completed-units.json"), "precondition: completed-units.json tracked");
|
|
432
|
+
assertTrue(tracked.includes("activity/log.jsonl"), "precondition: activity log tracked");
|
|
421
433
|
|
|
422
434
|
// Now modify both runtime and real files
|
|
423
435
|
createFile(repo, ".gsd/metrics.json", '{"version":2}');
|
|
@@ -428,15 +440,15 @@ describe('git-service', async () => {
|
|
|
428
440
|
// autoCommit should commit real.ts. The first call also runs auto-cleanup
|
|
429
441
|
// which removes runtime files from the index via a dedicated commit.
|
|
430
442
|
const msg = svc.autoCommit("execute-task", "M001/S01/T01");
|
|
431
|
-
|
|
443
|
+
assertTrue(msg !== null, "autoCommit produces a commit");
|
|
432
444
|
|
|
433
445
|
const show = run("git show --stat HEAD", repo);
|
|
434
|
-
|
|
446
|
+
assertTrue(show.includes("src/real.ts"), "real files are committed");
|
|
435
447
|
|
|
436
448
|
// After the commit, runtime files must no longer be in the git index.
|
|
437
449
|
// They remain on disk but are untracked (protected by .gitignore).
|
|
438
450
|
const trackedAfter = run("git ls-files .gsd/", repo);
|
|
439
|
-
|
|
451
|
+
assertEq(trackedAfter, "", "no .gsd/ runtime files remain in the index");
|
|
440
452
|
|
|
441
453
|
// Verify a second autoCommit with changed runtime files does NOT stage them
|
|
442
454
|
createFile(repo, ".gsd/metrics.json", '{"version":3}');
|
|
@@ -444,33 +456,37 @@ describe('git-service', async () => {
|
|
|
444
456
|
createFile(repo, "src/real.ts", "third version");
|
|
445
457
|
|
|
446
458
|
const msg2 = svc.autoCommit("execute-task", "M001/S01/T02");
|
|
447
|
-
|
|
459
|
+
assertTrue(msg2 !== null, "second autoCommit produces a commit");
|
|
448
460
|
|
|
449
461
|
const show2 = run("git show --stat HEAD", repo);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
462
|
+
assertTrue(show2.includes("src/real.ts"), "real files committed in second commit");
|
|
463
|
+
assertTrue(!show2.includes("metrics"), "metrics.json not in second commit");
|
|
464
|
+
assertTrue(!show2.includes("completed-units"), "completed-units.json not in second commit");
|
|
465
|
+
assertTrue(!show2.includes("activity"), "activity not in second commit");
|
|
454
466
|
|
|
455
467
|
rmSync(repo, { recursive: true, force: true });
|
|
456
|
-
}
|
|
468
|
+
}
|
|
457
469
|
|
|
458
470
|
// ─── GitServiceImpl: autoCommit on clean repo ──────────────────────────
|
|
459
471
|
|
|
460
|
-
|
|
472
|
+
console.log("\n=== GitServiceImpl: autoCommit ===");
|
|
473
|
+
|
|
474
|
+
{
|
|
461
475
|
const repo = initTempRepo();
|
|
462
476
|
const svc = new GitServiceImpl(repo);
|
|
463
477
|
|
|
464
478
|
// Clean repo — autoCommit should return null
|
|
465
479
|
const cleanResult = svc.autoCommit("task", "T01");
|
|
466
|
-
|
|
480
|
+
assertEq(cleanResult, null, "autoCommit on clean repo returns null");
|
|
467
481
|
|
|
468
482
|
rmSync(repo, { recursive: true, force: true });
|
|
469
|
-
}
|
|
483
|
+
}
|
|
470
484
|
|
|
471
485
|
// ─── GitServiceImpl: autoCommit on dirty repo ──────────────────────────
|
|
472
486
|
|
|
473
|
-
|
|
487
|
+
console.log("\n=== GitServiceImpl: autoCommit on dirty repo ===");
|
|
488
|
+
|
|
489
|
+
{
|
|
474
490
|
const repo = initTempRepo();
|
|
475
491
|
const svc = new GitServiceImpl(repo);
|
|
476
492
|
|
|
@@ -478,10 +494,10 @@ describe('git-service', async () => {
|
|
|
478
494
|
|
|
479
495
|
// Without task context, autoCommit uses generic chore message
|
|
480
496
|
const msg = svc.autoCommit("task", "T01");
|
|
481
|
-
|
|
497
|
+
assertEq(msg, "chore(T01): auto-commit after task", "autoCommit returns generic format without task context");
|
|
482
498
|
|
|
483
499
|
const log = run("git log --oneline -1", repo);
|
|
484
|
-
|
|
500
|
+
assertTrue(log.includes("chore(T01): auto-commit after task"), "generic commit message is in git log");
|
|
485
501
|
|
|
486
502
|
// With task context, autoCommit uses meaningful message
|
|
487
503
|
createFile(repo, "src/auth.ts", "export function login() {}");
|
|
@@ -491,16 +507,18 @@ describe('git-service', async () => {
|
|
|
491
507
|
oneLiner: "Added JWT-based auth with refresh token rotation",
|
|
492
508
|
keyFiles: ["src/auth.ts"],
|
|
493
509
|
});
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
510
|
+
assertTrue(msg2 !== null, "autoCommit with task context returns a message");
|
|
511
|
+
assertTrue(msg2!.startsWith("feat(S01/T02):"), "meaningful commit uses feat type and scope");
|
|
512
|
+
assertTrue(msg2!.includes("JWT-based auth"), "meaningful commit includes one-liner content");
|
|
497
513
|
|
|
498
514
|
rmSync(repo, { recursive: true, force: true });
|
|
499
|
-
}
|
|
515
|
+
}
|
|
500
516
|
|
|
501
517
|
// ─── GitServiceImpl: empty-after-staging guard ─────────────────────────
|
|
502
518
|
|
|
503
|
-
|
|
519
|
+
console.log("\n=== GitServiceImpl: empty-after-staging guard ===");
|
|
520
|
+
|
|
521
|
+
{
|
|
504
522
|
const repo = initTempRepo();
|
|
505
523
|
const svc = new GitServiceImpl(repo);
|
|
506
524
|
|
|
@@ -508,18 +526,20 @@ describe('git-service', async () => {
|
|
|
508
526
|
createFile(repo, ".gsd/activity/x.jsonl", "data");
|
|
509
527
|
|
|
510
528
|
const result = svc.autoCommit("task", "T02");
|
|
511
|
-
|
|
529
|
+
assertEq(result, null, "autoCommit returns null when only runtime files are dirty");
|
|
512
530
|
|
|
513
531
|
// Verify no new commit was created (should still be at init commit)
|
|
514
532
|
const logCount = run("git rev-list --count HEAD", repo);
|
|
515
|
-
|
|
533
|
+
assertEq(logCount, "1", "no new commit created when only runtime files changed");
|
|
516
534
|
|
|
517
535
|
rmSync(repo, { recursive: true, force: true });
|
|
518
|
-
}
|
|
536
|
+
}
|
|
519
537
|
|
|
520
538
|
// ─── GitServiceImpl: autoCommit with extraExclusions ───────────────────
|
|
521
539
|
|
|
522
|
-
|
|
540
|
+
console.log("\n=== GitServiceImpl: autoCommit with extraExclusions ===");
|
|
541
|
+
|
|
542
|
+
{
|
|
523
543
|
const repo = initTempRepo();
|
|
524
544
|
const svc = new GitServiceImpl(repo);
|
|
525
545
|
|
|
@@ -529,19 +549,21 @@ describe('git-service', async () => {
|
|
|
529
549
|
|
|
530
550
|
// Auto-commit with .gsd/ excluded (simulates pre-switch)
|
|
531
551
|
const msg = svc.autoCommit("pre-switch", "main", [".gsd/"]);
|
|
532
|
-
|
|
552
|
+
assertEq(msg, "chore(main): auto-commit after pre-switch", "pre-switch autoCommit with .gsd/ exclusion commits");
|
|
533
553
|
|
|
534
554
|
// Verify .gsd/ file was NOT committed
|
|
535
555
|
const show = run("git show --stat HEAD", repo);
|
|
536
|
-
|
|
537
|
-
|
|
556
|
+
assertTrue(!show.includes("ROADMAP"), ".gsd/ files excluded from pre-switch auto-commit");
|
|
557
|
+
assertTrue(show.includes("feature.ts"), "non-.gsd/ files included in pre-switch auto-commit");
|
|
538
558
|
|
|
539
559
|
rmSync(repo, { recursive: true, force: true });
|
|
540
|
-
}
|
|
560
|
+
}
|
|
541
561
|
|
|
542
562
|
// ─── GitServiceImpl: autoCommit extraExclusions — only .gsd/ dirty ────
|
|
543
563
|
|
|
544
|
-
|
|
564
|
+
console.log("\n=== GitServiceImpl: autoCommit extraExclusions — only .gsd/ dirty ===");
|
|
565
|
+
|
|
566
|
+
{
|
|
545
567
|
const repo = initTempRepo();
|
|
546
568
|
const svc = new GitServiceImpl(repo);
|
|
547
569
|
|
|
@@ -551,23 +573,25 @@ describe('git-service', async () => {
|
|
|
551
573
|
|
|
552
574
|
// Auto-commit with .gsd/ excluded — nothing else to commit
|
|
553
575
|
const result = svc.autoCommit("pre-switch", "main", [".gsd/"]);
|
|
554
|
-
|
|
576
|
+
assertEq(result, null, "autoCommit returns null when only .gsd/ files are dirty and excluded");
|
|
555
577
|
|
|
556
578
|
rmSync(repo, { recursive: true, force: true });
|
|
557
|
-
}
|
|
579
|
+
}
|
|
558
580
|
|
|
559
581
|
// ─── GitServiceImpl: commit returns null when nothing staged ───────────
|
|
560
582
|
|
|
561
|
-
|
|
583
|
+
console.log("\n=== GitServiceImpl: commit empty ===");
|
|
584
|
+
|
|
585
|
+
{
|
|
562
586
|
const repo = initTempRepo();
|
|
563
587
|
const svc = new GitServiceImpl(repo);
|
|
564
588
|
|
|
565
589
|
// Nothing dirty, commit should return null
|
|
566
590
|
const result = svc.commit({ message: "should not commit" });
|
|
567
|
-
|
|
591
|
+
assertEq(result, null, "commit returns null when nothing to stage");
|
|
568
592
|
|
|
569
593
|
rmSync(repo, { recursive: true, force: true });
|
|
570
|
-
}
|
|
594
|
+
}
|
|
571
595
|
|
|
572
596
|
// ─── Helper: create repo for branch tests ────────────────────────────
|
|
573
597
|
|
|
@@ -584,32 +608,36 @@ describe('git-service', async () => {
|
|
|
584
608
|
|
|
585
609
|
// ─── getCurrentBranch ────────────────────────────────────────────────
|
|
586
610
|
|
|
587
|
-
|
|
611
|
+
console.log("\n=== Branch queries ===");
|
|
612
|
+
|
|
613
|
+
{
|
|
588
614
|
const repo = initBranchTestRepo();
|
|
589
615
|
const svc = new GitServiceImpl(repo);
|
|
590
616
|
|
|
591
|
-
|
|
617
|
+
assertEq(svc.getCurrentBranch(), "main", "getCurrentBranch returns main on main branch");
|
|
592
618
|
|
|
593
619
|
run("git checkout -b gsd/M001/S01", repo);
|
|
594
|
-
|
|
620
|
+
assertEq(svc.getCurrentBranch(), "gsd/M001/S01", "getCurrentBranch returns slice branch name");
|
|
595
621
|
|
|
596
622
|
run("git checkout -b feature/foo", repo);
|
|
597
|
-
|
|
623
|
+
assertEq(svc.getCurrentBranch(), "feature/foo", "getCurrentBranch returns feature branch name");
|
|
598
624
|
|
|
599
625
|
rmSync(repo, { recursive: true, force: true });
|
|
600
|
-
}
|
|
626
|
+
}
|
|
601
627
|
|
|
602
628
|
// ─── getMainBranch ────────────────────────────────────────────────────
|
|
603
629
|
|
|
604
|
-
|
|
630
|
+
console.log("\n=== getMainBranch ===");
|
|
631
|
+
|
|
632
|
+
{
|
|
605
633
|
const repo = initBranchTestRepo();
|
|
606
634
|
const svc = new GitServiceImpl(repo);
|
|
607
635
|
|
|
608
636
|
// Basic case: repo has "main" branch
|
|
609
|
-
|
|
637
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch returns main when main exists");
|
|
610
638
|
|
|
611
639
|
rmSync(repo, { recursive: true, force: true });
|
|
612
|
-
}
|
|
640
|
+
}
|
|
613
641
|
|
|
614
642
|
{
|
|
615
643
|
// master-only repo
|
|
@@ -622,7 +650,7 @@ describe('git-service', async () => {
|
|
|
622
650
|
run('git commit -m "init"', repo);
|
|
623
651
|
|
|
624
652
|
const svc = new GitServiceImpl(repo);
|
|
625
|
-
|
|
653
|
+
assertEq(svc.getMainBranch(), "master", "getMainBranch returns master when only master exists");
|
|
626
654
|
|
|
627
655
|
rmSync(repo, { recursive: true, force: true });
|
|
628
656
|
}
|
|
@@ -633,7 +661,9 @@ describe('git-service', async () => {
|
|
|
633
661
|
|
|
634
662
|
// ─── createSnapshot: prefs enabled ─────────────────────────────────────
|
|
635
663
|
|
|
636
|
-
|
|
664
|
+
console.log("\n=== createSnapshot: enabled ===");
|
|
665
|
+
|
|
666
|
+
{
|
|
637
667
|
const repo = initBranchTestRepo();
|
|
638
668
|
const svc = new GitServiceImpl(repo, { snapshots: true });
|
|
639
669
|
|
|
@@ -647,14 +677,16 @@ describe('git-service', async () => {
|
|
|
647
677
|
|
|
648
678
|
// Verify ref exists under refs/gsd/snapshots/
|
|
649
679
|
const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
|
|
650
|
-
|
|
680
|
+
assertTrue(refs.includes("refs/gsd/snapshots/gsd/M001/S01/"), "snapshot ref created under refs/gsd/snapshots/");
|
|
651
681
|
|
|
652
682
|
rmSync(repo, { recursive: true, force: true });
|
|
653
|
-
}
|
|
683
|
+
}
|
|
654
684
|
|
|
655
685
|
// ─── createSnapshot: prefs disabled ────────────────────────────────────
|
|
656
686
|
|
|
657
|
-
|
|
687
|
+
console.log("\n=== createSnapshot: disabled ===");
|
|
688
|
+
|
|
689
|
+
{
|
|
658
690
|
const repo = initBranchTestRepo();
|
|
659
691
|
const svc = new GitServiceImpl(repo, { snapshots: false });
|
|
660
692
|
|
|
@@ -666,14 +698,16 @@ describe('git-service', async () => {
|
|
|
666
698
|
svc.createSnapshot("gsd/M001/S01");
|
|
667
699
|
|
|
668
700
|
const refs = run("git for-each-ref refs/gsd/snapshots/", repo);
|
|
669
|
-
|
|
701
|
+
assertEq(refs, "", "no snapshot ref created when prefs.snapshots is false");
|
|
670
702
|
|
|
671
703
|
rmSync(repo, { recursive: true, force: true });
|
|
672
|
-
}
|
|
704
|
+
}
|
|
673
705
|
|
|
674
706
|
// ─── runPreMergeCheck: pass ────────────────────────────────────────────
|
|
675
707
|
|
|
676
|
-
|
|
708
|
+
console.log("\n=== runPreMergeCheck: pass ===");
|
|
709
|
+
|
|
710
|
+
{
|
|
677
711
|
const repo = initBranchTestRepo();
|
|
678
712
|
// Create package.json with passing test script
|
|
679
713
|
createFile(repo, "package.json", JSON.stringify({
|
|
@@ -686,15 +720,17 @@ describe('git-service', async () => {
|
|
|
686
720
|
const svc = new GitServiceImpl(repo, { pre_merge_check: true });
|
|
687
721
|
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
688
722
|
|
|
689
|
-
|
|
690
|
-
|
|
723
|
+
assertEq(result.passed, true, "runPreMergeCheck returns passed:true when tests pass");
|
|
724
|
+
assertTrue(!result.skipped, "runPreMergeCheck is not skipped when enabled");
|
|
691
725
|
|
|
692
726
|
rmSync(repo, { recursive: true, force: true });
|
|
693
|
-
}
|
|
727
|
+
}
|
|
694
728
|
|
|
695
729
|
// ─── runPreMergeCheck: fail ────────────────────────────────────────────
|
|
696
730
|
|
|
697
|
-
|
|
731
|
+
console.log("\n=== runPreMergeCheck: fail ===");
|
|
732
|
+
|
|
733
|
+
{
|
|
698
734
|
const repo = initBranchTestRepo();
|
|
699
735
|
// Create package.json with failing test script
|
|
700
736
|
createFile(repo, "package.json", JSON.stringify({
|
|
@@ -707,15 +743,17 @@ describe('git-service', async () => {
|
|
|
707
743
|
const svc = new GitServiceImpl(repo, { pre_merge_check: true });
|
|
708
744
|
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
709
745
|
|
|
710
|
-
|
|
711
|
-
|
|
746
|
+
assertEq(result.passed, false, "runPreMergeCheck returns passed:false when tests fail");
|
|
747
|
+
assertTrue(!result.skipped, "runPreMergeCheck is not skipped when enabled");
|
|
712
748
|
|
|
713
749
|
rmSync(repo, { recursive: true, force: true });
|
|
714
|
-
}
|
|
750
|
+
}
|
|
715
751
|
|
|
716
752
|
// ─── runPreMergeCheck: disabled ────────────────────────────────────────
|
|
717
753
|
|
|
718
|
-
|
|
754
|
+
console.log("\n=== runPreMergeCheck: disabled ===");
|
|
755
|
+
|
|
756
|
+
{
|
|
719
757
|
const repo = initBranchTestRepo();
|
|
720
758
|
createFile(repo, "package.json", JSON.stringify({
|
|
721
759
|
name: "test-disabled",
|
|
@@ -727,86 +765,98 @@ describe('git-service', async () => {
|
|
|
727
765
|
const svc = new GitServiceImpl(repo, { pre_merge_check: false });
|
|
728
766
|
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
729
767
|
|
|
730
|
-
|
|
731
|
-
|
|
768
|
+
assertEq(result.skipped, true, "runPreMergeCheck skipped when pre_merge_check is false");
|
|
769
|
+
assertEq(result.passed, true, "runPreMergeCheck returns passed:true when skipped (no block)");
|
|
732
770
|
|
|
733
771
|
rmSync(repo, { recursive: true, force: true });
|
|
734
|
-
}
|
|
772
|
+
}
|
|
735
773
|
|
|
736
774
|
// ─── runPreMergeCheck: custom command ──────────────────────────────────
|
|
737
775
|
|
|
738
|
-
|
|
776
|
+
console.log("\n=== runPreMergeCheck: custom command ===");
|
|
777
|
+
|
|
778
|
+
{
|
|
739
779
|
const repo = initBranchTestRepo();
|
|
740
780
|
// Custom command string overrides auto-detection
|
|
741
781
|
const svc = new GitServiceImpl(repo, { pre_merge_check: 'node -e "process.exit(0)"' });
|
|
742
782
|
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
743
783
|
|
|
744
|
-
|
|
745
|
-
|
|
784
|
+
assertEq(result.passed, true, "runPreMergeCheck passes with custom command that exits 0");
|
|
785
|
+
assertTrue(!result.skipped, "custom command is not skipped");
|
|
746
786
|
|
|
747
787
|
rmSync(repo, { recursive: true, force: true });
|
|
748
|
-
}
|
|
788
|
+
}
|
|
749
789
|
|
|
750
790
|
// ─── VALID_BRANCH_NAME regex ──────────────────────────────────────────
|
|
751
791
|
|
|
752
|
-
|
|
792
|
+
console.log("\n=== VALID_BRANCH_NAME regex ===");
|
|
793
|
+
|
|
794
|
+
{
|
|
753
795
|
// Valid branch names
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
796
|
+
assertTrue(VALID_BRANCH_NAME.test("main"), "VALID_BRANCH_NAME accepts 'main'");
|
|
797
|
+
assertTrue(VALID_BRANCH_NAME.test("master"), "VALID_BRANCH_NAME accepts 'master'");
|
|
798
|
+
assertTrue(VALID_BRANCH_NAME.test("develop"), "VALID_BRANCH_NAME accepts 'develop'");
|
|
799
|
+
assertTrue(VALID_BRANCH_NAME.test("feature/foo"), "VALID_BRANCH_NAME accepts 'feature/foo'");
|
|
800
|
+
assertTrue(VALID_BRANCH_NAME.test("release-1.0"), "VALID_BRANCH_NAME accepts 'release-1.0'");
|
|
801
|
+
assertTrue(VALID_BRANCH_NAME.test("my_branch"), "VALID_BRANCH_NAME accepts 'my_branch'");
|
|
802
|
+
assertTrue(VALID_BRANCH_NAME.test("v2.0.1"), "VALID_BRANCH_NAME accepts 'v2.0.1'");
|
|
761
803
|
|
|
762
804
|
// Invalid / injection attempts
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
805
|
+
assertTrue(!VALID_BRANCH_NAME.test("main; rm -rf /"), "VALID_BRANCH_NAME rejects shell injection");
|
|
806
|
+
assertTrue(!VALID_BRANCH_NAME.test("main && echo pwned"), "VALID_BRANCH_NAME rejects && injection");
|
|
807
|
+
assertTrue(!VALID_BRANCH_NAME.test(""), "VALID_BRANCH_NAME rejects empty string");
|
|
808
|
+
assertTrue(!VALID_BRANCH_NAME.test("branch name"), "VALID_BRANCH_NAME rejects spaces");
|
|
809
|
+
assertTrue(!VALID_BRANCH_NAME.test("branch`cmd`"), "VALID_BRANCH_NAME rejects backticks");
|
|
810
|
+
assertTrue(!VALID_BRANCH_NAME.test("branch$(cmd)"), "VALID_BRANCH_NAME rejects $() subshell");
|
|
811
|
+
}
|
|
770
812
|
|
|
771
813
|
// ─── getMainBranch: configured main_branch preference ──────────────────
|
|
772
814
|
|
|
773
|
-
|
|
815
|
+
console.log("\n=== getMainBranch: configured main_branch ===");
|
|
816
|
+
|
|
817
|
+
{
|
|
774
818
|
const repo = initBranchTestRepo();
|
|
775
819
|
const svc = new GitServiceImpl(repo, { main_branch: "trunk" });
|
|
776
820
|
|
|
777
|
-
|
|
821
|
+
assertEq(svc.getMainBranch(), "trunk", "getMainBranch returns configured main_branch preference");
|
|
778
822
|
|
|
779
823
|
rmSync(repo, { recursive: true, force: true });
|
|
780
|
-
}
|
|
824
|
+
}
|
|
781
825
|
|
|
782
826
|
// ─── getMainBranch: falls back to auto-detection when not set ──────────
|
|
783
827
|
|
|
784
|
-
|
|
828
|
+
console.log("\n=== getMainBranch: fallback to auto-detection ===");
|
|
829
|
+
|
|
830
|
+
{
|
|
785
831
|
const repo = initBranchTestRepo();
|
|
786
832
|
const svc = new GitServiceImpl(repo, {});
|
|
787
833
|
|
|
788
|
-
|
|
834
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch falls back to auto-detection when main_branch not set");
|
|
789
835
|
|
|
790
836
|
rmSync(repo, { recursive: true, force: true });
|
|
791
|
-
}
|
|
837
|
+
}
|
|
792
838
|
|
|
793
839
|
// ─── getMainBranch: ignores invalid branch names ───────────────────────
|
|
794
840
|
|
|
795
|
-
|
|
841
|
+
console.log("\n=== getMainBranch: ignores invalid branch name ===");
|
|
842
|
+
|
|
843
|
+
{
|
|
796
844
|
const repo = initBranchTestRepo();
|
|
797
845
|
const svc = new GitServiceImpl(repo, { main_branch: "main; rm -rf /" });
|
|
798
846
|
|
|
799
|
-
|
|
847
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch ignores invalid branch name and falls back to auto-detection");
|
|
800
848
|
|
|
801
849
|
rmSync(repo, { recursive: true, force: true });
|
|
802
|
-
}
|
|
850
|
+
}
|
|
803
851
|
|
|
804
852
|
// ─── PreMergeCheckResult type export compile check ─────────────────────
|
|
805
853
|
|
|
806
|
-
|
|
854
|
+
console.log("\n=== PreMergeCheckResult type export ===");
|
|
855
|
+
|
|
856
|
+
{
|
|
807
857
|
const _checkResult: PreMergeCheckResult = { passed: true, skipped: false };
|
|
808
|
-
|
|
809
|
-
}
|
|
858
|
+
assertTrue(true, "PreMergeCheckResult type exported and usable");
|
|
859
|
+
}
|
|
810
860
|
|
|
811
861
|
// ═══════════════════════════════════════════════════════════════════════
|
|
812
862
|
// Integration branch — feature-branch workflow support
|
|
@@ -814,70 +864,82 @@ describe('git-service', async () => {
|
|
|
814
864
|
|
|
815
865
|
// ─── writeIntegrationBranch / readIntegrationBranch: round-trip ────────
|
|
816
866
|
|
|
817
|
-
|
|
867
|
+
console.log("\n=== Integration branch: write and read ===");
|
|
868
|
+
|
|
869
|
+
{
|
|
818
870
|
const repo = initBranchTestRepo();
|
|
819
871
|
|
|
820
872
|
// Initially no integration branch
|
|
821
|
-
|
|
873
|
+
assertEq(readIntegrationBranch(repo, "M001"), null, "readIntegrationBranch returns null when no metadata");
|
|
822
874
|
|
|
823
875
|
// Write integration branch
|
|
824
876
|
writeIntegrationBranch(repo, "M001", "f-123-new-thing");
|
|
825
|
-
|
|
877
|
+
assertEq(readIntegrationBranch(repo, "M001"), "f-123-new-thing", "readIntegrationBranch returns written branch");
|
|
826
878
|
|
|
827
879
|
rmSync(repo, { recursive: true, force: true });
|
|
828
|
-
}
|
|
880
|
+
}
|
|
829
881
|
|
|
830
882
|
// ─── writeIntegrationBranch: updates when branch changes (#300) ──────
|
|
831
883
|
|
|
832
|
-
|
|
884
|
+
console.log("\n=== Integration branch: updates on branch change ===");
|
|
885
|
+
|
|
886
|
+
{
|
|
833
887
|
const repo = initBranchTestRepo();
|
|
834
888
|
|
|
835
889
|
writeIntegrationBranch(repo, "M001", "f-123-first");
|
|
836
890
|
writeIntegrationBranch(repo, "M001", "f-456-second"); // updates to new branch (#300)
|
|
837
891
|
|
|
838
|
-
|
|
892
|
+
assertEq(readIntegrationBranch(repo, "M001"), "f-456-second", "second write updates integration branch to new value");
|
|
839
893
|
|
|
840
894
|
rmSync(repo, { recursive: true, force: true });
|
|
841
|
-
}
|
|
895
|
+
}
|
|
842
896
|
|
|
843
897
|
// ─── writeIntegrationBranch: same branch is idempotent ─────────────────
|
|
844
898
|
|
|
845
|
-
|
|
899
|
+
console.log("\n=== Integration branch: same branch is idempotent ===");
|
|
900
|
+
|
|
901
|
+
{
|
|
846
902
|
const repo = initBranchTestRepo();
|
|
847
903
|
|
|
848
904
|
writeIntegrationBranch(repo, "M001", "f-123-first");
|
|
849
905
|
writeIntegrationBranch(repo, "M001", "f-123-first"); // same branch — no-op
|
|
850
906
|
|
|
851
|
-
|
|
907
|
+
assertEq(readIntegrationBranch(repo, "M001"), "f-123-first", "same branch write is idempotent");
|
|
852
908
|
|
|
853
909
|
rmSync(repo, { recursive: true, force: true });
|
|
854
|
-
}
|
|
910
|
+
}
|
|
855
911
|
|
|
856
912
|
// ─── writeIntegrationBranch: rejects slice branches ───────────────────
|
|
857
913
|
|
|
858
|
-
|
|
914
|
+
console.log("\n=== Integration branch: rejects slice branches ===");
|
|
915
|
+
|
|
916
|
+
{
|
|
859
917
|
const repo = initBranchTestRepo();
|
|
860
918
|
|
|
861
919
|
writeIntegrationBranch(repo, "M001", "gsd/M001/S01");
|
|
862
|
-
|
|
920
|
+
assertEq(readIntegrationBranch(repo, "M001"), null, "slice branches are not recorded as integration branch");
|
|
863
921
|
|
|
864
922
|
rmSync(repo, { recursive: true, force: true });
|
|
865
|
-
}
|
|
923
|
+
}
|
|
866
924
|
|
|
867
925
|
// ─── writeIntegrationBranch: rejects invalid branch names ─────────────
|
|
868
926
|
|
|
869
|
-
|
|
927
|
+
console.log("\n=== Integration branch: rejects invalid names ===");
|
|
928
|
+
|
|
929
|
+
{
|
|
870
930
|
const repo = initBranchTestRepo();
|
|
871
931
|
|
|
872
932
|
writeIntegrationBranch(repo, "M001", "bad; rm -rf /");
|
|
873
|
-
|
|
933
|
+
assertEq(readIntegrationBranch(repo, "M001"), null, "invalid branch name is not recorded");
|
|
874
934
|
|
|
875
935
|
rmSync(repo, { recursive: true, force: true });
|
|
876
|
-
}
|
|
936
|
+
}
|
|
877
937
|
|
|
878
938
|
// ─── getMainBranch: uses integration branch when milestone set ────────
|
|
879
939
|
|
|
880
|
-
|
|
940
|
+
console.log("\n=== getMainBranch: integration branch from milestone metadata ===");
|
|
941
|
+
|
|
942
|
+
{
|
|
881
943
|
const repo = initBranchTestRepo();
|
|
882
944
|
|
|
883
945
|
// Create a feature branch
|
|
@@ -889,18 +951,20 @@ describe('git-service', async () => {
|
|
|
889
951
|
|
|
890
952
|
// Without milestone set, getMainBranch returns "main"
|
|
891
953
|
const svc = new GitServiceImpl(repo);
|
|
892
|
-
|
|
954
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch returns main when no milestone set");
|
|
893
955
|
|
|
894
956
|
// With milestone set, getMainBranch returns the integration branch
|
|
895
957
|
svc.setMilestoneId("M001");
|
|
896
|
-
|
|
958
|
+
assertEq(svc.getMainBranch(), "f-123-feature", "getMainBranch returns integration branch when milestone set");
|
|
897
959
|
|
|
898
960
|
rmSync(repo, { recursive: true, force: true });
|
|
899
|
-
}
|
|
961
|
+
}
|
|
900
962
|
|
|
901
963
|
// ─── getMainBranch: main_branch pref still takes priority ─────────────
|
|
902
964
|
|
|
903
|
-
|
|
965
|
+
console.log("\n=== getMainBranch: main_branch pref overrides integration branch ===");
|
|
966
|
+
|
|
967
|
+
{
|
|
904
968
|
const repo = initBranchTestRepo();
|
|
905
969
|
|
|
906
970
|
run("git checkout -b f-123-feature", repo);
|
|
@@ -912,14 +976,16 @@ describe('git-service', async () => {
|
|
|
912
976
|
// Explicit preference still wins
|
|
913
977
|
const svc = new GitServiceImpl(repo, { main_branch: "trunk" });
|
|
914
978
|
svc.setMilestoneId("M001");
|
|
915
|
-
|
|
979
|
+
assertEq(svc.getMainBranch(), "trunk", "main_branch preference overrides integration branch");
|
|
916
980
|
|
|
917
981
|
rmSync(repo, { recursive: true, force: true });
|
|
918
|
-
}
|
|
982
|
+
}
|
|
919
983
|
|
|
920
984
|
// ─── getMainBranch: falls back when integration branch deleted ────────
|
|
921
985
|
|
|
922
|
-
|
|
986
|
+
console.log("\n=== getMainBranch: fallback when integration branch deleted ===");
|
|
987
|
+
|
|
988
|
+
{
|
|
923
989
|
const repo = initBranchTestRepo();
|
|
924
990
|
|
|
925
991
|
// Write metadata pointing to a branch that doesn't exist
|
|
@@ -927,67 +993,75 @@ describe('git-service', async () => {
|
|
|
927
993
|
|
|
928
994
|
const svc = new GitServiceImpl(repo);
|
|
929
995
|
svc.setMilestoneId("M001");
|
|
930
|
-
|
|
996
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch falls back to main when integration branch no longer exists");
|
|
931
997
|
|
|
932
998
|
rmSync(repo, { recursive: true, force: true });
|
|
933
|
-
}
|
|
999
|
+
}
|
|
934
1000
|
|
|
935
1001
|
// ─── resolveMilestoneIntegrationBranch: recorded branch wins when it exists ───
|
|
936
1002
|
|
|
937
|
-
|
|
1003
|
+
console.log("\n=== Integration branch: resolver prefers recorded branch ===");
|
|
1004
|
+
|
|
1005
|
+
{
|
|
938
1006
|
const repo = initBranchTestRepo();
|
|
939
1007
|
run("git checkout -b feature/live", repo);
|
|
940
1008
|
run("git checkout main", repo);
|
|
941
1009
|
writeIntegrationBranch(repo, "M001", "feature/live");
|
|
942
1010
|
|
|
943
1011
|
const resolved = resolveMilestoneIntegrationBranch(repo, "M001");
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1012
|
+
assertEq(resolved.status, "recorded", "resolver reports recorded branch when metadata branch exists");
|
|
1013
|
+
assertEq(resolved.recordedBranch, "feature/live", "resolver includes recorded branch");
|
|
1014
|
+
assertEq(resolved.effectiveBranch, "feature/live", "resolver uses recorded branch as effective branch");
|
|
947
1015
|
|
|
948
1016
|
rmSync(repo, { recursive: true, force: true });
|
|
949
|
-
}
|
|
1017
|
+
}
|
|
950
1018
|
|
|
951
1019
|
// ─── resolveMilestoneIntegrationBranch: falls back to detected default ────────
|
|
952
1020
|
|
|
953
|
-
|
|
1021
|
+
console.log("\n=== Integration branch: resolver falls back to detected default ===");
|
|
1022
|
+
|
|
1023
|
+
{
|
|
954
1024
|
const repo = initBranchTestRepo();
|
|
955
1025
|
writeIntegrationBranch(repo, "M001", "deleted-branch");
|
|
956
1026
|
|
|
957
1027
|
const resolved = resolveMilestoneIntegrationBranch(repo, "M001");
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1028
|
+
assertEq(resolved.status, "fallback", "resolver reports fallback when recorded branch is stale");
|
|
1029
|
+
assertEq(resolved.recordedBranch, "deleted-branch", "resolver preserves stale recorded branch for diagnostics");
|
|
1030
|
+
assertEq(resolved.effectiveBranch, "main", "resolver falls back to detected default branch");
|
|
1031
|
+
assertTrue(
|
|
962
1032
|
resolved.reason.includes("deleted-branch") && resolved.reason.includes("main"),
|
|
963
1033
|
"resolver reason mentions stale recorded branch and fallback branch",
|
|
964
1034
|
);
|
|
965
1035
|
|
|
966
1036
|
rmSync(repo, { recursive: true, force: true });
|
|
967
|
-
}
|
|
1037
|
+
}
|
|
968
1038
|
|
|
969
1039
|
// ─── resolveMilestoneIntegrationBranch: configured main_branch is fallback ─────
|
|
970
1040
|
|
|
971
|
-
|
|
1041
|
+
console.log("\n=== Integration branch: resolver uses configured fallback branch ===");
|
|
1042
|
+
|
|
1043
|
+
{
|
|
972
1044
|
const repo = initBranchTestRepo();
|
|
973
1045
|
run("git checkout -b trunk", repo);
|
|
974
1046
|
run("git checkout main", repo);
|
|
975
1047
|
writeIntegrationBranch(repo, "M001", "deleted-branch");
|
|
976
1048
|
|
|
977
1049
|
const resolved = resolveMilestoneIntegrationBranch(repo, "M001", { main_branch: "trunk" });
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1050
|
+
assertEq(resolved.status, "fallback", "resolver reports fallback when using configured main_branch");
|
|
1051
|
+
assertEq(resolved.effectiveBranch, "trunk", "resolver prefers configured main_branch as fallback");
|
|
1052
|
+
assertTrue(
|
|
981
1053
|
resolved.reason.includes("deleted-branch") && resolved.reason.includes("trunk"),
|
|
982
1054
|
"configured fallback reason mentions stale branch and configured branch",
|
|
983
1055
|
);
|
|
984
1056
|
|
|
985
1057
|
rmSync(repo, { recursive: true, force: true });
|
|
986
|
-
}
|
|
1058
|
+
}
|
|
987
1059
|
|
|
988
1060
|
// ─── Per-milestone isolation: different milestones, different targets ──
|
|
989
1061
|
|
|
990
|
-
|
|
1062
|
+
console.log("\n=== Integration branch: per-milestone isolation ===");
|
|
1063
|
+
|
|
1064
|
+
{
|
|
991
1065
|
const repo = initBranchTestRepo();
|
|
992
1066
|
|
|
993
1067
|
run("git checkout -b feature-a", repo);
|
|
@@ -1000,33 +1074,37 @@ describe('git-service', async () => {
|
|
|
1000
1074
|
const svc = new GitServiceImpl(repo);
|
|
1001
1075
|
|
|
1002
1076
|
svc.setMilestoneId("M001");
|
|
1003
|
-
|
|
1077
|
+
assertEq(svc.getMainBranch(), "feature-a", "M001 integration branch is feature-a");
|
|
1004
1078
|
|
|
1005
1079
|
svc.setMilestoneId("M002");
|
|
1006
|
-
|
|
1080
|
+
assertEq(svc.getMainBranch(), "feature-b", "M002 integration branch is feature-b");
|
|
1007
1081
|
|
|
1008
1082
|
svc.setMilestoneId(null);
|
|
1009
|
-
|
|
1083
|
+
assertEq(svc.getMainBranch(), "main", "no milestone set → falls back to main");
|
|
1010
1084
|
|
|
1011
1085
|
rmSync(repo, { recursive: true, force: true });
|
|
1012
|
-
}
|
|
1086
|
+
}
|
|
1013
1087
|
|
|
1014
1088
|
// ─── Backward compatibility: no metadata → existing behavior ──────────
|
|
1015
1089
|
|
|
1016
|
-
|
|
1090
|
+
console.log("\n=== Integration branch: backward compat ===");
|
|
1091
|
+
|
|
1092
|
+
{
|
|
1017
1093
|
const repo = initBranchTestRepo();
|
|
1018
1094
|
const svc = new GitServiceImpl(repo);
|
|
1019
1095
|
|
|
1020
1096
|
// Set milestone but no metadata file exists
|
|
1021
1097
|
svc.setMilestoneId("M001");
|
|
1022
|
-
|
|
1098
|
+
assertEq(svc.getMainBranch(), "main", "backward compat: no metadata file → falls back to main");
|
|
1023
1099
|
|
|
1024
1100
|
rmSync(repo, { recursive: true, force: true });
|
|
1025
|
-
}
|
|
1101
|
+
}
|
|
1026
1102
|
|
|
1027
1103
|
// ─── untrackRuntimeFiles: removes tracked runtime files from index ───
|
|
1028
1104
|
|
|
1029
|
-
|
|
1105
|
+
console.log("\n=== untrackRuntimeFiles ===");
|
|
1106
|
+
|
|
1107
|
+
{
|
|
1030
1108
|
const { untrackRuntimeFiles } = await import("../gitignore.ts");
|
|
1031
1109
|
const repo = mkdtempSync(join(tmpdir(), "gsd-untrack-"));
|
|
1032
1110
|
run("git init -b main", repo);
|
|
@@ -1047,36 +1125,38 @@ describe('git-service', async () => {
|
|
|
1047
1125
|
|
|
1048
1126
|
// Precondition: runtime files are tracked
|
|
1049
1127
|
const trackedBefore = run("git ls-files .gsd/", repo);
|
|
1050
|
-
|
|
1051
|
-
|
|
1128
|
+
assertTrue(trackedBefore.includes("completed-units.json"), "untrack: precondition — completed-units tracked");
|
|
1129
|
+
assertTrue(trackedBefore.includes("metrics.json"), "untrack: precondition — metrics tracked");
|
|
1052
1130
|
|
|
1053
1131
|
// Run untrackRuntimeFiles
|
|
1054
1132
|
untrackRuntimeFiles(repo);
|
|
1055
1133
|
|
|
1056
1134
|
// Runtime files should be removed from the index
|
|
1057
1135
|
const trackedAfter = run("git ls-files .gsd/", repo);
|
|
1058
|
-
|
|
1136
|
+
assertEq(trackedAfter, "", "untrack: all runtime files removed from index");
|
|
1059
1137
|
|
|
1060
1138
|
// Non-runtime files remain tracked
|
|
1061
1139
|
const srcTracked = run("git ls-files src.ts", repo);
|
|
1062
|
-
|
|
1140
|
+
assertTrue(srcTracked.includes("src.ts"), "untrack: non-runtime files remain tracked");
|
|
1063
1141
|
|
|
1064
1142
|
// Files still exist on disk
|
|
1065
|
-
|
|
1143
|
+
assertTrue(existsSync(join(repo, ".gsd", "completed-units.json")),
|
|
1066
1144
|
"untrack: completed-units.json still on disk");
|
|
1067
|
-
|
|
1145
|
+
assertTrue(existsSync(join(repo, ".gsd", "metrics.json")),
|
|
1068
1146
|
"untrack: metrics.json still on disk");
|
|
1069
1147
|
|
|
1070
1148
|
// Idempotent — running again doesn't error
|
|
1071
1149
|
untrackRuntimeFiles(repo);
|
|
1072
|
-
|
|
1150
|
+
assertTrue(true, "untrack: second call is idempotent (no error)");
|
|
1073
1151
|
|
|
1074
1152
|
rmSync(repo, { recursive: true, force: true });
|
|
1075
|
-
}
|
|
1153
|
+
}
|
|
1076
1154
|
|
|
1077
1155
|
// ─── smartStage excludes runtime files but allows milestone artifacts ──
|
|
1078
1156
|
|
|
1079
|
-
|
|
1157
|
+
console.log("\n=== smartStage excludes runtime files, allows milestone artifacts ===");
|
|
1158
|
+
|
|
1159
|
+
{
|
|
1080
1160
|
const repo = mkdtempSync(join(tmpdir(), "gsd-smart-stage-excludes-"));
|
|
1081
1161
|
run("git init -b main", repo);
|
|
1082
1162
|
run("git config user.email test@test.com", repo);
|
|
@@ -1098,65 +1178,71 @@ describe('git-service', async () => {
|
|
|
1098
1178
|
// smartStage excludes only runtime paths, not all of .gsd/ (#1326)
|
|
1099
1179
|
const svc = new GitServiceImpl(repo);
|
|
1100
1180
|
const msg = svc.commit({ message: "test commit" });
|
|
1101
|
-
|
|
1181
|
+
assertTrue(msg !== null, "smartStage: commit succeeds");
|
|
1102
1182
|
|
|
1103
1183
|
const committed = run("git show --name-only HEAD", repo);
|
|
1104
|
-
|
|
1184
|
+
assertTrue(committed.includes("src.ts"), "smartStage: source files ARE in commit");
|
|
1105
1185
|
// Runtime files should NOT be committed
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1186
|
+
assertTrue(!committed.includes(".gsd/STATE.md"), "smartStage: STATE.md excluded (runtime)");
|
|
1187
|
+
assertTrue(!committed.includes(".gsd/runtime/"), "smartStage: runtime/ excluded");
|
|
1188
|
+
assertTrue(!committed.includes(".gsd/activity/"), "smartStage: activity/ excluded");
|
|
1109
1189
|
// Milestone artifacts SHOULD be committed when not gitignored (#1326)
|
|
1110
|
-
|
|
1190
|
+
assertTrue(committed.includes(".gsd/milestones/"), "smartStage: milestone artifacts ARE committed");
|
|
1111
1191
|
|
|
1112
1192
|
rmSync(repo, { recursive: true, force: true });
|
|
1113
|
-
}
|
|
1193
|
+
}
|
|
1114
1194
|
|
|
1115
1195
|
// ─── writeIntegrationBranch: no commit (metadata in external storage) ──
|
|
1116
1196
|
|
|
1117
|
-
|
|
1197
|
+
console.log("\n=== writeIntegrationBranch: no commit ===");
|
|
1198
|
+
|
|
1199
|
+
{
|
|
1118
1200
|
const repo = initBranchTestRepo();
|
|
1119
1201
|
const commitsBefore = run("git rev-list --count HEAD", repo);
|
|
1120
1202
|
|
|
1121
1203
|
writeIntegrationBranch(repo, "M001", "f-123-new-thing");
|
|
1122
1204
|
|
|
1123
1205
|
// File should still be written to disk
|
|
1124
|
-
|
|
1206
|
+
assertEq(readIntegrationBranch(repo, "M001"), "f-123-new-thing",
|
|
1125
1207
|
"writeIntegrationBranch: metadata file exists on disk");
|
|
1126
1208
|
|
|
1127
1209
|
// No commit — .gsd/ is managed externally
|
|
1128
1210
|
const commitsAfter = run("git rev-list --count HEAD", repo);
|
|
1129
|
-
|
|
1211
|
+
assertEq(commitsBefore, commitsAfter,
|
|
1130
1212
|
"writeIntegrationBranch: no git commit created for integration branch");
|
|
1131
1213
|
|
|
1132
1214
|
rmSync(repo, { recursive: true, force: true });
|
|
1133
|
-
}
|
|
1215
|
+
}
|
|
1134
1216
|
|
|
1135
1217
|
// ─── ensureGitignore: always adds .gsd to gitignore ──────────────────
|
|
1136
1218
|
|
|
1137
|
-
|
|
1219
|
+
console.log("\n=== ensureGitignore: adds .gsd entry ===");
|
|
1220
|
+
|
|
1221
|
+
{
|
|
1138
1222
|
const { ensureGitignore } = await import("../gitignore.ts");
|
|
1139
1223
|
const repo = mkdtempSync(join(tmpdir(), "gsd-gitignore-external-state-"));
|
|
1140
1224
|
|
|
1141
1225
|
// Should add .gsd to gitignore (external state dir is a symlink)
|
|
1142
1226
|
const modified = ensureGitignore(repo);
|
|
1143
|
-
|
|
1227
|
+
assertTrue(modified, "ensureGitignore: gitignore was modified");
|
|
1144
1228
|
|
|
1145
1229
|
const { readFileSync } = await import("node:fs");
|
|
1146
1230
|
const content = readFileSync(join(repo, ".gitignore"), "utf-8");
|
|
1147
1231
|
const lines = content.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("#"));
|
|
1148
|
-
|
|
1232
|
+
assertTrue(lines.includes(".gsd"), "ensureGitignore: .gitignore contains .gsd");
|
|
1149
1233
|
|
|
1150
1234
|
// Idempotent — calling again doesn't add duplicates
|
|
1151
1235
|
const modified2 = ensureGitignore(repo);
|
|
1152
|
-
|
|
1236
|
+
assertTrue(!modified2, "ensureGitignore: second call is idempotent");
|
|
1153
1237
|
|
|
1154
1238
|
rmSync(repo, { recursive: true, force: true });
|
|
1155
|
-
}
|
|
1239
|
+
}
|
|
1156
1240
|
|
|
1157
1241
|
// ─── nativeAddAllWithExclusions: symlinked .gsd fallback ───────────────
|
|
1158
1242
|
|
|
1159
|
-
|
|
1243
|
+
console.log("\n=== nativeAddAllWithExclusions: symlinked .gsd fallback ===");
|
|
1244
|
+
|
|
1245
|
+
{
|
|
1160
1246
|
// When .gsd is a symlink, git rejects `:!.gsd/...` pathspecs with
|
|
1161
1247
|
// "fatal: pathspec '...' is beyond a symbolic link". The fix falls
|
|
1162
1248
|
// back to plain `git add -A`, which respects .gitignore.
|
|
@@ -1185,20 +1271,22 @@ describe('git-service', async () => {
|
|
|
1185
1271
|
threw = true;
|
|
1186
1272
|
console.error(" unexpected error:", e);
|
|
1187
1273
|
}
|
|
1188
|
-
|
|
1274
|
+
assertTrue(!threw, "nativeAddAllWithExclusions does not throw with symlinked .gsd");
|
|
1189
1275
|
|
|
1190
1276
|
// Verify the real file was staged
|
|
1191
1277
|
const staged = run("git diff --cached --name-only", repo);
|
|
1192
|
-
|
|
1193
|
-
|
|
1278
|
+
assertTrue(staged.includes("src/app.ts"), "real file staged despite symlinked .gsd");
|
|
1279
|
+
assertTrue(!staged.includes(".gsd"), ".gsd content not staged");
|
|
1194
1280
|
|
|
1195
1281
|
rmSync(repo, { recursive: true, force: true });
|
|
1196
1282
|
rmSync(externalGsd, { recursive: true, force: true });
|
|
1197
|
-
}
|
|
1283
|
+
}
|
|
1198
1284
|
|
|
1199
1285
|
// ─── nativeAddAllWithExclusions: non-symlinked .gsd still works ───────
|
|
1200
1286
|
|
|
1201
|
-
|
|
1287
|
+
console.log("\n=== nativeAddAllWithExclusions: non-symlinked .gsd still works ===");
|
|
1288
|
+
|
|
1289
|
+
{
|
|
1202
1290
|
// Verify the normal (non-symlink) case still works with pathspec exclusions
|
|
1203
1291
|
const repo = initTempRepo();
|
|
1204
1292
|
|
|
@@ -1212,91 +1300,96 @@ describe('git-service', async () => {
|
|
|
1212
1300
|
} catch {
|
|
1213
1301
|
threw = true;
|
|
1214
1302
|
}
|
|
1215
|
-
|
|
1303
|
+
assertTrue(!threw, "nativeAddAllWithExclusions works with normal .gsd directory");
|
|
1216
1304
|
|
|
1217
1305
|
const staged = run("git diff --cached --name-only", repo);
|
|
1218
|
-
|
|
1306
|
+
assertTrue(staged.includes("src/code.ts"), "real file staged with normal .gsd");
|
|
1219
1307
|
|
|
1220
1308
|
rmSync(repo, { recursive: true, force: true });
|
|
1221
|
-
}
|
|
1309
|
+
}
|
|
1222
1310
|
|
|
1223
1311
|
// ─── MergeConflictError: constructor fields ───────────────────────────────
|
|
1224
1312
|
|
|
1225
|
-
|
|
1313
|
+
console.log("\n=== MergeConflictError: constructor fields ===");
|
|
1314
|
+
{
|
|
1226
1315
|
const err = new MergeConflictError(
|
|
1227
1316
|
["src/foo.ts", "src/bar.ts"],
|
|
1228
1317
|
"squash",
|
|
1229
1318
|
"gsd/M001/S01",
|
|
1230
1319
|
"main",
|
|
1231
1320
|
);
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1321
|
+
assertEq(err.conflictedFiles, ["src/foo.ts", "src/bar.ts"], "MergeConflictError.conflictedFiles populated");
|
|
1322
|
+
assertEq(err.strategy, "squash", "MergeConflictError.strategy set");
|
|
1323
|
+
assertEq(err.branch, "gsd/M001/S01", "MergeConflictError.branch set");
|
|
1324
|
+
assertEq(err.mainBranch, "main", "MergeConflictError.mainBranch set");
|
|
1325
|
+
assertEq(err.name, "MergeConflictError", "MergeConflictError.name is MergeConflictError");
|
|
1326
|
+
assertTrue(err.message.includes("src/foo.ts"), "MergeConflictError message lists conflicted files");
|
|
1327
|
+
assertTrue(err.message.toLowerCase().includes("squash"), "MergeConflictError message mentions strategy");
|
|
1328
|
+
assertTrue(err instanceof MergeConflictError, "MergeConflictError is an instanceof MergeConflictError");
|
|
1329
|
+
assertTrue(err instanceof Error, "MergeConflictError is an Error instance");
|
|
1330
|
+
}
|
|
1242
1331
|
|
|
1243
1332
|
// ─── Integration branch: rejects gsd/quick/* branches ────────────────────
|
|
1244
1333
|
|
|
1245
|
-
|
|
1334
|
+
console.log("\n=== Integration branch: rejects gsd/quick/* branches ===");
|
|
1335
|
+
{
|
|
1246
1336
|
const repo = initBranchTestRepo();
|
|
1247
1337
|
|
|
1248
1338
|
writeIntegrationBranch(repo, "M001", "gsd/quick/1234-some-task");
|
|
1249
|
-
|
|
1339
|
+
assertEq(readIntegrationBranch(repo, "M001"), null, "gsd/quick/* branches are not recorded as integration branch");
|
|
1250
1340
|
|
|
1251
1341
|
rmSync(repo, { recursive: true, force: true });
|
|
1252
|
-
}
|
|
1342
|
+
}
|
|
1253
1343
|
|
|
1254
1344
|
// ─── Integration branch: resolver returns missing when no metadata ────────
|
|
1255
1345
|
|
|
1256
|
-
|
|
1346
|
+
console.log("\n=== Integration branch: resolver returns missing when no metadata ===");
|
|
1347
|
+
{
|
|
1257
1348
|
const repo = initBranchTestRepo();
|
|
1258
1349
|
|
|
1259
1350
|
// No writeIntegrationBranch call — no metadata file exists
|
|
1260
1351
|
const resolved = resolveMilestoneIntegrationBranch(repo, "M999");
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1352
|
+
assertEq(resolved.status, "missing", "resolver reports missing when no metadata file");
|
|
1353
|
+
assertEq(resolved.recordedBranch, null, "resolver recordedBranch is null when no metadata");
|
|
1354
|
+
assertEq(resolved.effectiveBranch, null, "resolver effectiveBranch is null when no metadata");
|
|
1355
|
+
assertTrue(resolved.reason.includes("M999"), "resolver reason mentions the milestone ID");
|
|
1265
1356
|
|
|
1266
1357
|
rmSync(repo, { recursive: true, force: true });
|
|
1267
|
-
}
|
|
1358
|
+
}
|
|
1268
1359
|
|
|
1269
1360
|
// ─── Integration branch: resolver missing when both recorded and configured branches gone ───
|
|
1270
1361
|
|
|
1271
|
-
|
|
1362
|
+
console.log("\n=== Integration branch: resolver missing when both recorded and configured branches gone ===");
|
|
1363
|
+
{
|
|
1272
1364
|
const repo = initBranchTestRepo();
|
|
1273
1365
|
|
|
1274
1366
|
// Record a branch that doesn't exist
|
|
1275
1367
|
writeIntegrationBranch(repo, "M001", "deleted-feature");
|
|
1276
1368
|
// configured main_branch also doesn't exist
|
|
1277
1369
|
const resolved = resolveMilestoneIntegrationBranch(repo, "M001", { main_branch: "nonexistent-branch" });
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1370
|
+
assertEq(resolved.status, "missing", "resolver reports missing when recorded branch and configured main_branch both absent");
|
|
1371
|
+
assertEq(resolved.recordedBranch, "deleted-feature", "resolver preserves stale recorded branch");
|
|
1372
|
+
assertEq(resolved.effectiveBranch, null, "resolver effectiveBranch is null when no safe fallback");
|
|
1373
|
+
assertTrue(
|
|
1282
1374
|
resolved.reason.includes("deleted-feature") && resolved.reason.includes("nonexistent-branch"),
|
|
1283
1375
|
"reason mentions both stale branch and unavailable configured branch",
|
|
1284
1376
|
);
|
|
1285
1377
|
|
|
1286
1378
|
rmSync(repo, { recursive: true, force: true });
|
|
1287
|
-
}
|
|
1379
|
+
}
|
|
1288
1380
|
|
|
1289
1381
|
// ─── buildTaskCommitMessage: issueNumber appends Resolves trailer ─────────
|
|
1290
1382
|
|
|
1291
|
-
|
|
1383
|
+
console.log("\n=== buildTaskCommitMessage: issueNumber appends Resolves trailer ===");
|
|
1384
|
+
{
|
|
1292
1385
|
const msg = buildTaskCommitMessage({
|
|
1293
1386
|
taskId: "S01/T03",
|
|
1294
1387
|
taskTitle: "fix login redirect",
|
|
1295
1388
|
issueNumber: 42,
|
|
1296
1389
|
});
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
}
|
|
1390
|
+
assertTrue(msg.includes("Resolves #42"), "buildTaskCommitMessage includes Resolves #N trailer when issueNumber is set");
|
|
1391
|
+
assertTrue(msg.startsWith("fix(S01/T03):"), "buildTaskCommitMessage infers fix type");
|
|
1392
|
+
}
|
|
1300
1393
|
|
|
1301
1394
|
{
|
|
1302
1395
|
// No issueNumber — no Resolves trailer
|
|
@@ -1304,26 +1397,29 @@ describe('git-service', async () => {
|
|
|
1304
1397
|
taskId: "S01/T04",
|
|
1305
1398
|
taskTitle: "add dashboard widget",
|
|
1306
1399
|
});
|
|
1307
|
-
|
|
1400
|
+
assertTrue(!msg.includes("Resolves"), "buildTaskCommitMessage omits Resolves trailer when issueNumber is absent");
|
|
1308
1401
|
}
|
|
1309
1402
|
|
|
1310
1403
|
// ─── runPreMergeCheck: skips when no package.json ────────────────────────
|
|
1311
1404
|
|
|
1312
|
-
|
|
1405
|
+
console.log("\n=== runPreMergeCheck: skips when no package.json ===");
|
|
1406
|
+
{
|
|
1313
1407
|
const repo = initBranchTestRepo();
|
|
1314
1408
|
// No package.json created — auto-detect should skip gracefully
|
|
1315
1409
|
const svc = new GitServiceImpl(repo, { pre_merge_check: true });
|
|
1316
1410
|
const result: PreMergeCheckResult = svc.runPreMergeCheck();
|
|
1317
1411
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1412
|
+
assertEq(result.passed, true, "runPreMergeCheck passes when no package.json (skip)");
|
|
1413
|
+
assertEq(result.skipped, true, "runPreMergeCheck skips when no package.json found");
|
|
1320
1414
|
|
|
1321
1415
|
rmSync(repo, { recursive: true, force: true });
|
|
1322
|
-
}
|
|
1416
|
+
}
|
|
1323
1417
|
|
|
1324
1418
|
// ─── autoCommit: symlinked .gsd does NOT stage milestone artifacts (#2247) ──
|
|
1325
1419
|
|
|
1326
|
-
|
|
1420
|
+
console.log("\n=== autoCommit: symlinked .gsd does NOT stage milestone artifacts (#2247) ===");
|
|
1421
|
+
|
|
1422
|
+
{
|
|
1327
1423
|
// When .gsd is a symlink (external state project), .gsd/ files live outside
|
|
1328
1424
|
// the repo by design. smartStage() must NOT force-stage them into git — the
|
|
1329
1425
|
// .gitignore exclusion is correct and intentional.
|
|
@@ -1352,14 +1448,21 @@ describe('git-service', async () => {
|
|
|
1352
1448
|
|
|
1353
1449
|
const svc = new GitServiceImpl(repo);
|
|
1354
1450
|
const msg = svc.autoCommit("complete-milestone", "M009");
|
|
1355
|
-
|
|
1451
|
+
assertTrue(msg !== null, "symlink autoCommit: commit succeeds");
|
|
1356
1452
|
|
|
1357
1453
|
const committed = run("git show --name-only HEAD", repo);
|
|
1358
|
-
|
|
1359
|
-
|
|
1454
|
+
assertTrue(committed.includes("src/feature.ts"), "symlink autoCommit: source file committed");
|
|
1455
|
+
assertTrue(!committed.includes(".gsd/milestones/"),
|
|
1360
1456
|
"symlink autoCommit: .gsd/milestones/ files are NOT staged (external state stays external)");
|
|
1361
1457
|
|
|
1362
1458
|
try { rmSync(repo, { recursive: true, force: true }); } catch {}
|
|
1363
1459
|
try { rmSync(externalGsd, { recursive: true, force: true }); } catch {}
|
|
1364
|
-
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
report();
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
main().catch((error) => {
|
|
1466
|
+
console.error(error);
|
|
1467
|
+
process.exit(1);
|
|
1365
1468
|
});
|