gsd-pi 2.44.0-dev.d25d507 → 2.45.0-dev.6b9da3e
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/resources/extensions/gsd/activity-log.js +7 -0
- package/dist/resources/extensions/gsd/auto/infra-errors.js +3 -0
- package/dist/resources/extensions/gsd/auto/phases.js +37 -36
- package/dist/resources/extensions/gsd/auto-prompts.js +24 -1
- package/dist/resources/extensions/gsd/auto-start.js +21 -2
- package/dist/resources/extensions/gsd/auto-timers.js +57 -3
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +9 -6
- package/dist/resources/extensions/gsd/auto.js +30 -3
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +156 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +2 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +10 -0
- package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
- package/dist/resources/extensions/gsd/db-writer.js +34 -16
- package/dist/resources/extensions/gsd/doctor.js +8 -0
- package/dist/resources/extensions/gsd/git-service.js +8 -3
- package/dist/resources/extensions/gsd/gsd-db.js +12 -1
- package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
- package/dist/resources/extensions/gsd/prompts/rethink.md +78 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
- package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
- package/dist/resources/extensions/gsd/repo-identity.js +45 -7
- package/dist/resources/extensions/gsd/rethink.js +115 -0
- package/dist/resources/extensions/gsd/state.js +41 -3
- package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
- package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
- package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
- package/dist/resources/extensions/gsd/worktree-manager.js +32 -2
- package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +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 +2 -2
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +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 +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/page.js +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.11ca5c01938e5948.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{3721.bf31263de6d5fa46.js → 485.243af25f0cdf50d6.js} +2 -2
- package/dist/web/standalone/.next/static/chunks/app/{page-b9367c5ae13b99c6.js → page-6654a8cca61a3d1c.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/webpack-0a4cd455ec4197d2.js +1 -0
- package/dist/web/standalone/.next/static/css/dd4ae3f58ac9b600.css +1 -0
- package/package.json +1 -1
- package/packages/native/dist/stream-process/index.js +2 -2
- package/packages/native/src/__tests__/stream-process.test.mjs +34 -0
- package/packages/native/src/stream-process/index.ts +2 -2
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -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 +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -0
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +17 -8
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.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 +7 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
- package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
- package/packages/pi-coding-agent/src/main.ts +19 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/activity-log.ts +1 -0
- package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
- package/src/resources/extensions/gsd/auto/phases.ts +46 -48
- package/src/resources/extensions/gsd/auto-prompts.ts +24 -1
- package/src/resources/extensions/gsd/auto-start.ts +25 -2
- package/src/resources/extensions/gsd/auto-timers.ts +64 -3
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +5 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +9 -6
- package/src/resources/extensions/gsd/auto.ts +37 -3
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +148 -0
- package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +2 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
- package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
- package/src/resources/extensions/gsd/db-writer.ts +39 -17
- package/src/resources/extensions/gsd/doctor.ts +7 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -2
- package/src/resources/extensions/gsd/gsd-db.ts +16 -1
- package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
- package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
- package/src/resources/extensions/gsd/prompts/rethink.md +78 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
- package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
- package/src/resources/extensions/gsd/repo-identity.ts +46 -7
- package/src/resources/extensions/gsd/rethink.ts +154 -0
- package/src/resources/extensions/gsd/state.ts +41 -1
- package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +121 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +60 -0
- package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
- package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +121 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
- package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
- package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +65 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
- package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +43 -2
- package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
- package/src/resources/extensions/mcp-client/index.ts +20 -0
- package/dist/web/standalone/.next/static/chunks/4024.0de81b543b28b9fe.js +0 -9
- package/dist/web/standalone/.next/static/chunks/webpack-9014b5adb127a98a.js +0 -1
- package/dist/web/standalone/.next/static/css/8a727f372cf53002.css +0 -1
- /package/dist/web/standalone/.next/static/{tokoGmfkYfWf1_Yl_Gz7i → rzO54ZboyINyEt7cVM_uS}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{tokoGmfkYfWf1_Yl_Gz7i → rzO54ZboyINyEt7cVM_uS}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* derive-state-db-disk-reconcile.test.ts — #2416
|
|
3
|
+
*
|
|
4
|
+
* After migration to DB-backed state, milestones that exist on disk
|
|
5
|
+
* (in .gsd/milestones/) but were never imported into the DB become
|
|
6
|
+
* invisible to deriveStateFromDb(). This test verifies that
|
|
7
|
+
* deriveStateFromDb reconciles disk milestones with DB milestones.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
import { deriveStateFromDb, invalidateStateCache } from "../state.ts";
|
|
15
|
+
import {
|
|
16
|
+
openDatabase,
|
|
17
|
+
closeDatabase,
|
|
18
|
+
insertMilestone,
|
|
19
|
+
insertSlice,
|
|
20
|
+
insertTask,
|
|
21
|
+
} from "../gsd-db.ts";
|
|
22
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
23
|
+
|
|
24
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
25
|
+
|
|
26
|
+
function createFixtureBase(): string {
|
|
27
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-disk-reconcile-"));
|
|
28
|
+
mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
|
|
29
|
+
return base;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeFile(base: string, relativePath: string, content: string): void {
|
|
33
|
+
const full = join(base, ".gsd", relativePath);
|
|
34
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
35
|
+
writeFileSync(full, content);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cleanup(base: string): void {
|
|
39
|
+
rmSync(base, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const CONTEXT_CONTENT = `# M002: Disk-Only Milestone
|
|
43
|
+
|
|
44
|
+
This milestone exists on disk but not in the DB.
|
|
45
|
+
|
|
46
|
+
## Must-Haves
|
|
47
|
+
- Something important
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const ROADMAP_CONTENT = `# M002: Disk-Only Milestone
|
|
51
|
+
|
|
52
|
+
**Vision:** Test disk reconciliation.
|
|
53
|
+
|
|
54
|
+
## Slices
|
|
55
|
+
|
|
56
|
+
- [ ] **S01: First Slice** \`risk:low\` \`depends:[]\`
|
|
57
|
+
> Do something.
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
async function main(): Promise<void> {
|
|
61
|
+
console.log("\n=== #2416: deriveStateFromDb reconciles disk milestones ===");
|
|
62
|
+
|
|
63
|
+
// Set up: M001 in DB, M002 on disk only
|
|
64
|
+
const base = createFixtureBase();
|
|
65
|
+
const dbPath = join(base, ".gsd", "gsd.db");
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
openDatabase(dbPath);
|
|
69
|
+
|
|
70
|
+
// M001 is in the DB with a complete status
|
|
71
|
+
insertMilestone({ id: "M001", title: "M001: DB Milestone", status: "complete", depends_on: [] });
|
|
72
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "S01: Done Slice", status: "complete", depends: [] });
|
|
73
|
+
|
|
74
|
+
// Write M001 summary on disk (marks it complete on filesystem too)
|
|
75
|
+
writeFile(base, "milestones/M001/SUMMARY.md", "# M001: DB Milestone\n\nDone.");
|
|
76
|
+
|
|
77
|
+
// M002 exists ONLY on disk, not in DB
|
|
78
|
+
writeFile(base, "milestones/M002/CONTEXT.md", CONTEXT_CONTENT);
|
|
79
|
+
writeFile(base, "milestones/M002/ROADMAP.md", ROADMAP_CONTENT);
|
|
80
|
+
|
|
81
|
+
invalidateStateCache();
|
|
82
|
+
const state = await deriveStateFromDb(base);
|
|
83
|
+
|
|
84
|
+
// M002 should be visible in the registry
|
|
85
|
+
const m002Entry = state.registry.find((m) => m.id === "M002");
|
|
86
|
+
assertTrue(
|
|
87
|
+
m002Entry !== undefined,
|
|
88
|
+
"M002 (disk-only milestone) should appear in state.registry (#2416)",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// M001 should still be in the registry
|
|
92
|
+
const m001Entry = state.registry.find((m) => m.id === "M001");
|
|
93
|
+
assertTrue(
|
|
94
|
+
m001Entry !== undefined,
|
|
95
|
+
"M001 (DB milestone) should still appear in state.registry",
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// The active milestone should be M002 (since M001 is complete)
|
|
99
|
+
assertTrue(
|
|
100
|
+
state.activeMilestone !== null,
|
|
101
|
+
"There should be an active milestone",
|
|
102
|
+
);
|
|
103
|
+
if (state.activeMilestone) {
|
|
104
|
+
assertEq(
|
|
105
|
+
state.activeMilestone.id,
|
|
106
|
+
"M002",
|
|
107
|
+
"Active milestone should be M002 (disk-only, not complete) (#2416)",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
closeDatabase();
|
|
112
|
+
cleanup(base);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
report();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main().catch((err) => {
|
|
119
|
+
console.error(err);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
insertArtifact,
|
|
12
12
|
isDbAvailable,
|
|
13
13
|
insertMilestone,
|
|
14
|
+
getAllMilestones,
|
|
14
15
|
insertSlice,
|
|
15
16
|
insertTask,
|
|
16
17
|
} from '../gsd-db.ts';
|
|
@@ -962,4 +963,63 @@ describe('derive-state-db', async () => {
|
|
|
962
963
|
cleanup(base);
|
|
963
964
|
}
|
|
964
965
|
});
|
|
966
|
+
|
|
967
|
+
// ─── Regression: disk-only milestones synced into DB (#2416) ─────────
|
|
968
|
+
test('derive-state-db: disk-only milestone auto-synced into DB (#2416)', async () => {
|
|
969
|
+
const base = createFixtureBase();
|
|
970
|
+
try {
|
|
971
|
+
// M001 is complete and exists in DB. M002 was queued on disk only — no DB row.
|
|
972
|
+
writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Summary\n\nDone.');
|
|
973
|
+
writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002: Queued\n\nQueued milestone.');
|
|
974
|
+
|
|
975
|
+
openDatabase(':memory:');
|
|
976
|
+
// Only insert M001 — simulates the state after migration guard ran then /gsd queue added M002
|
|
977
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'complete' });
|
|
978
|
+
|
|
979
|
+
invalidateStateCache();
|
|
980
|
+
const state = await deriveStateFromDb(base);
|
|
981
|
+
|
|
982
|
+
// Before the fix, M002 was invisible: getAllMilestones() returned only M001
|
|
983
|
+
// (complete) → phase='complete' → auto-mode stopped.
|
|
984
|
+
// After the fix, deriveStateFromDb reconciles disk dirs and inserts M002.
|
|
985
|
+
assert.deepStrictEqual(state.phase, 'pre-planning', 'disk-sync-2416: phase is pre-planning, not complete');
|
|
986
|
+
assert.deepStrictEqual(state.registry.length, 2, 'disk-sync-2416: both milestones visible in registry');
|
|
987
|
+
assert.deepStrictEqual(state.registry[0]?.id, 'M001', 'disk-sync-2416: registry[0] is M001');
|
|
988
|
+
assert.deepStrictEqual(state.registry[0]?.status, 'complete', 'disk-sync-2416: M001 is complete');
|
|
989
|
+
assert.deepStrictEqual(state.registry[1]?.id, 'M002', 'disk-sync-2416: registry[1] is M002');
|
|
990
|
+
assert.deepStrictEqual(state.registry[1]?.status, 'active', 'disk-sync-2416: M002 is active');
|
|
991
|
+
assert.deepStrictEqual(state.activeMilestone?.id, 'M002', 'disk-sync-2416: activeMilestone is M002');
|
|
992
|
+
|
|
993
|
+
closeDatabase();
|
|
994
|
+
} finally {
|
|
995
|
+
closeDatabase();
|
|
996
|
+
cleanup(base);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// ─── Queued milestone row not clobbered by later plan (#2416 root cause) ──
|
|
1001
|
+
test('derive-state-db: queued milestone row survives gsd_plan_milestone INSERT OR IGNORE', async () => {
|
|
1002
|
+
try {
|
|
1003
|
+
openDatabase(':memory:');
|
|
1004
|
+
|
|
1005
|
+
// Simulates gsd_milestone_generate_id inserting a minimal queued row
|
|
1006
|
+
insertMilestone({ id: 'M001', status: 'queued' });
|
|
1007
|
+
|
|
1008
|
+
const before = getAllMilestones();
|
|
1009
|
+
assert.equal(before.length, 1, 'queued-row: one row after generate_id');
|
|
1010
|
+
assert.equal(before[0]!.status, 'queued', 'queued-row: status is queued');
|
|
1011
|
+
|
|
1012
|
+
// Simulates gsd_plan_milestone calling insertMilestone (INSERT OR IGNORE)
|
|
1013
|
+
insertMilestone({ id: 'M001', title: 'Planned Title', status: 'active' });
|
|
1014
|
+
|
|
1015
|
+
const after = getAllMilestones();
|
|
1016
|
+
assert.equal(after.length, 1, 'queued-row: still one row after plan');
|
|
1017
|
+
// INSERT OR IGNORE keeps the original row — status stays 'queued'
|
|
1018
|
+
assert.equal(after[0]!.status, 'queued', 'queued-row: INSERT OR IGNORE preserves original status');
|
|
1019
|
+
|
|
1020
|
+
closeDatabase();
|
|
1021
|
+
} finally {
|
|
1022
|
+
closeDatabase();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
965
1025
|
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* est-annotation-timeout.test.ts — Regression tests for #2243.
|
|
3
|
+
*
|
|
4
|
+
* Tasks with `est: 30m` or `est: 2h` annotations should get extended
|
|
5
|
+
* supervision timeouts. The parseEstimateMinutes helper should parse
|
|
6
|
+
* estimate strings, and startUnitSupervision should use them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const timersSrcPath = join(import.meta.dirname, "..", "auto-timers.ts");
|
|
15
|
+
const timersSrc = readFileSync(timersSrcPath, "utf-8");
|
|
16
|
+
|
|
17
|
+
// ─── Source analysis: parseEstimateMinutes exists and is exported ────────────
|
|
18
|
+
|
|
19
|
+
test("#2243: auto-timers.ts should export parseEstimateMinutes", () => {
|
|
20
|
+
assert.ok(
|
|
21
|
+
timersSrc.includes("export function parseEstimateMinutes"),
|
|
22
|
+
"parseEstimateMinutes should be exported from auto-timers.ts",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ─── Inline unit test of parseEstimateMinutes logic ─────────────────────────
|
|
27
|
+
// Since importing the module pulls in heavy deps, test the parsing logic inline.
|
|
28
|
+
|
|
29
|
+
function parseEstimateMinutes(estimate: string): number | null {
|
|
30
|
+
if (!estimate || typeof estimate !== "string") return null;
|
|
31
|
+
const trimmed = estimate.trim();
|
|
32
|
+
if (!trimmed) return null;
|
|
33
|
+
|
|
34
|
+
let totalMinutes = 0;
|
|
35
|
+
let matched = false;
|
|
36
|
+
|
|
37
|
+
const hoursMatch = trimmed.match(/(\d+)\s*h/i);
|
|
38
|
+
if (hoursMatch) {
|
|
39
|
+
totalMinutes += Number(hoursMatch[1]) * 60;
|
|
40
|
+
matched = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const minutesMatch = trimmed.match(/(\d+)\s*m/i);
|
|
44
|
+
if (minutesMatch) {
|
|
45
|
+
totalMinutes += Number(minutesMatch[1]);
|
|
46
|
+
matched = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return matched ? totalMinutes : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("#2243: parseEstimateMinutes parses '30m' correctly", () => {
|
|
53
|
+
assert.equal(parseEstimateMinutes("30m"), 30);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("#2243: parseEstimateMinutes parses '2h' correctly", () => {
|
|
57
|
+
assert.equal(parseEstimateMinutes("2h"), 120);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("#2243: parseEstimateMinutes parses '1h30m' correctly", () => {
|
|
61
|
+
assert.equal(parseEstimateMinutes("1h30m"), 90);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("#2243: parseEstimateMinutes parses '15m' correctly", () => {
|
|
65
|
+
assert.equal(parseEstimateMinutes("15m"), 15);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("#2243: parseEstimateMinutes returns null for empty string", () => {
|
|
69
|
+
assert.equal(parseEstimateMinutes(""), null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("#2243: parseEstimateMinutes returns null for invalid string", () => {
|
|
73
|
+
assert.equal(parseEstimateMinutes("not a time"), null);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── Source analysis: startUnitSupervision uses task estimates ───────────────
|
|
77
|
+
|
|
78
|
+
test("#2243: startUnitSupervision should reference task estimates for timeout scaling", () => {
|
|
79
|
+
const usesEstimate =
|
|
80
|
+
timersSrc.includes("parseEstimateMinutes") &&
|
|
81
|
+
timersSrc.includes("estimateMinutes") &&
|
|
82
|
+
timersSrc.includes("taskEstimate");
|
|
83
|
+
|
|
84
|
+
assert.ok(
|
|
85
|
+
usesEstimate,
|
|
86
|
+
"startUnitSupervision should use task estimate annotations for timeout scaling",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("#2243: SupervisionContext should accept an optional taskEstimate field", () => {
|
|
91
|
+
const ctxIdx = timersSrc.indexOf("SupervisionContext");
|
|
92
|
+
assert.ok(ctxIdx !== -1, "SupervisionContext interface exists");
|
|
93
|
+
|
|
94
|
+
const ctxEnd = timersSrc.indexOf("}", ctxIdx);
|
|
95
|
+
const ctxBlock = timersSrc.slice(ctxIdx, ctxEnd);
|
|
96
|
+
|
|
97
|
+
assert.ok(
|
|
98
|
+
ctxBlock.includes("taskEstimate"),
|
|
99
|
+
"SupervisionContext should include a taskEstimate field",
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("#2243: timeouts should be scaled by estimate (timeoutScale in source)", () => {
|
|
104
|
+
assert.ok(
|
|
105
|
+
timersSrc.includes("timeoutScale"),
|
|
106
|
+
"auto-timers.ts should use a timeoutScale factor derived from est: annotations",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("#2243: idle timeout should NOT be scaled (idle is idle regardless of estimate)", () => {
|
|
111
|
+
// Find the idleTimeoutMs line
|
|
112
|
+
const idleIdx = timersSrc.indexOf("const idleTimeoutMs");
|
|
113
|
+
assert.ok(idleIdx !== -1, "idleTimeoutMs variable exists");
|
|
114
|
+
|
|
115
|
+
const idleLine = timersSrc.slice(idleIdx, timersSrc.indexOf("\n", idleIdx));
|
|
116
|
+
assert.ok(
|
|
117
|
+
!idleLine.includes("timeoutScale"),
|
|
118
|
+
"idleTimeoutMs should NOT be scaled — idle is idle",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
@@ -7,10 +7,13 @@ import { isInfrastructureError, INFRA_ERROR_CODES } from "../auto/infra-errors.j
|
|
|
7
7
|
// ── INFRA_ERROR_CODES constant ───────────────────────────────────────────────
|
|
8
8
|
|
|
9
9
|
test("INFRA_ERROR_CODES contains the expected codes", () => {
|
|
10
|
-
for (const code of [
|
|
10
|
+
for (const code of [
|
|
11
|
+
"ENOSPC", "ENOMEM", "EROFS", "EDQUOT", "EMFILE", "ENFILE",
|
|
12
|
+
"ECONNREFUSED", "ENOTFOUND", "ENETUNREACH",
|
|
13
|
+
]) {
|
|
11
14
|
assert.ok(INFRA_ERROR_CODES.has(code), `missing ${code}`);
|
|
12
15
|
}
|
|
13
|
-
assert.equal(INFRA_ERROR_CODES.size,
|
|
16
|
+
assert.equal(INFRA_ERROR_CODES.size, 9, "unexpected extra codes");
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
// ── isInfrastructureError: code property detection ───────────────────────────
|
|
@@ -45,6 +48,21 @@ test("detects ENFILE via code property", () => {
|
|
|
45
48
|
assert.equal(isInfrastructureError(err), "ENFILE");
|
|
46
49
|
});
|
|
47
50
|
|
|
51
|
+
test("detects ECONNREFUSED via code property", () => {
|
|
52
|
+
const err = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:3000"), { code: "ECONNREFUSED" });
|
|
53
|
+
assert.equal(isInfrastructureError(err), "ECONNREFUSED");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("detects ENOTFOUND via code property", () => {
|
|
57
|
+
const err = Object.assign(new Error("getaddrinfo ENOTFOUND api.example.com"), { code: "ENOTFOUND" });
|
|
58
|
+
assert.equal(isInfrastructureError(err), "ENOTFOUND");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("detects ENETUNREACH via code property", () => {
|
|
62
|
+
const err = Object.assign(new Error("connect ENETUNREACH 2607:f8b0:4004::"), { code: "ENETUNREACH" });
|
|
63
|
+
assert.equal(isInfrastructureError(err), "ENETUNREACH");
|
|
64
|
+
});
|
|
65
|
+
|
|
48
66
|
// ── isInfrastructureError: message fallback ──────────────────────────────────
|
|
49
67
|
|
|
50
68
|
test("falls back to message scanning when no code property", () => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inherited-repo-home-dir.test.ts — Regression test for #2393.
|
|
3
|
+
*
|
|
4
|
+
* When the user's home directory IS a git repo (common with dotfile
|
|
5
|
+
* managers like yadm), isInheritedRepo() must not treat ~/.gsd (the
|
|
6
|
+
* global GSD state directory) as a project .gsd belonging to the home
|
|
7
|
+
* repo. Without the fix, isInheritedRepo() returns false for project
|
|
8
|
+
* subdirectories because it sees ~/.gsd and concludes the parent repo
|
|
9
|
+
* has already been initialised with GSD — causing the wrong project
|
|
10
|
+
* state to be loaded.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import {
|
|
16
|
+
mkdtempSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
realpathSync,
|
|
21
|
+
symlinkSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { execFileSync } from "node:child_process";
|
|
26
|
+
|
|
27
|
+
import { isInheritedRepo } from "../repo-identity.ts";
|
|
28
|
+
|
|
29
|
+
function run(cmd: string, args: string[], cwd: string): string {
|
|
30
|
+
return execFileSync(cmd, args, {
|
|
31
|
+
cwd,
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
}).trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("isInheritedRepo when git root is HOME (#2393)", () => {
|
|
38
|
+
let fakeHome: string;
|
|
39
|
+
let stateDir: string;
|
|
40
|
+
let origGsdHome: string | undefined;
|
|
41
|
+
let origGsdStateDir: string | undefined;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// Create a fake HOME that is itself a git repo (dotfile manager scenario).
|
|
45
|
+
fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-home-repo-")));
|
|
46
|
+
run("git", ["init", "-b", "main"], fakeHome);
|
|
47
|
+
run("git", ["config", "user.name", "Test"], fakeHome);
|
|
48
|
+
run("git", ["config", "user.email", "test@example.com"], fakeHome);
|
|
49
|
+
writeFileSync(join(fakeHome, ".bashrc"), "# dotfiles\n", "utf-8");
|
|
50
|
+
run("git", ["add", ".bashrc"], fakeHome);
|
|
51
|
+
run("git", ["commit", "-m", "init dotfiles"], fakeHome);
|
|
52
|
+
|
|
53
|
+
// Create a plain ~/.gsd directory at fakeHome — this simulates the
|
|
54
|
+
// global GSD home directory, NOT a project .gsd.
|
|
55
|
+
mkdirSync(join(fakeHome, ".gsd", "projects"), { recursive: true });
|
|
56
|
+
|
|
57
|
+
// Save and override env. Point GSD_HOME at fakeHome/.gsd so the
|
|
58
|
+
// function recognizes it as the global state directory.
|
|
59
|
+
origGsdHome = process.env.GSD_HOME;
|
|
60
|
+
origGsdStateDir = process.env.GSD_STATE_DIR;
|
|
61
|
+
process.env.GSD_HOME = join(fakeHome, ".gsd");
|
|
62
|
+
stateDir = mkdtempSync(join(tmpdir(), "gsd-state-"));
|
|
63
|
+
process.env.GSD_STATE_DIR = stateDir;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
if (origGsdHome !== undefined) process.env.GSD_HOME = origGsdHome;
|
|
68
|
+
else delete process.env.GSD_HOME;
|
|
69
|
+
if (origGsdStateDir !== undefined) process.env.GSD_STATE_DIR = origGsdStateDir;
|
|
70
|
+
else delete process.env.GSD_STATE_DIR;
|
|
71
|
+
|
|
72
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
73
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("subdirectory of home-as-git-root is detected as inherited even when ~/.gsd exists", () => {
|
|
77
|
+
// Create a project directory inside fake HOME
|
|
78
|
+
const projectDir = join(fakeHome, "projects", "my-app");
|
|
79
|
+
mkdirSync(projectDir, { recursive: true });
|
|
80
|
+
|
|
81
|
+
// The bug: isInheritedRepo sees ~/.gsd and returns false, thinking
|
|
82
|
+
// the home repo is a legitimate GSD project. It should return true
|
|
83
|
+
// because ~/.gsd is the global state dir, not a project .gsd.
|
|
84
|
+
assert.strictEqual(
|
|
85
|
+
isInheritedRepo(projectDir),
|
|
86
|
+
true,
|
|
87
|
+
"project inside home-as-git-root must be detected as inherited repo, " +
|
|
88
|
+
"even when ~/.gsd (global state dir) exists",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("subdirectory with a real project .gsd symlink at git root is NOT inherited", () => {
|
|
93
|
+
// Simulate a legitimately initialised GSD project at the home repo root:
|
|
94
|
+
// .gsd is a symlink to an external state directory.
|
|
95
|
+
const externalState = join(stateDir, "projects", "home-project");
|
|
96
|
+
mkdirSync(externalState, { recursive: true });
|
|
97
|
+
const gsdDir = join(fakeHome, ".gsd");
|
|
98
|
+
|
|
99
|
+
// Remove the plain directory and replace with a symlink (real project .gsd)
|
|
100
|
+
rmSync(gsdDir, { recursive: true, force: true });
|
|
101
|
+
symlinkSync(externalState, gsdDir);
|
|
102
|
+
|
|
103
|
+
const projectDir = join(fakeHome, "projects", "my-app");
|
|
104
|
+
mkdirSync(projectDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
// When .gsd at root IS a project symlink, subdirectories are legitimate children
|
|
107
|
+
assert.strictEqual(
|
|
108
|
+
isInheritedRepo(projectDir),
|
|
109
|
+
false,
|
|
110
|
+
"subdirectory of a legitimately-initialised GSD project should NOT be inherited",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("home-as-git-root itself is never inherited", () => {
|
|
115
|
+
assert.strictEqual(
|
|
116
|
+
isInheritedRepo(fakeHome),
|
|
117
|
+
false,
|
|
118
|
+
"the git root itself is never inherited",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test, { describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
formatMcpStatusReport,
|
|
6
|
+
formatMcpServerDetail,
|
|
7
|
+
type McpServerStatus,
|
|
8
|
+
} from "../commands-mcp-status.ts";
|
|
9
|
+
|
|
10
|
+
// ─── formatMcpStatusReport ──────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe("formatMcpStatusReport", () => {
|
|
13
|
+
test("returns no-servers message when list is empty", () => {
|
|
14
|
+
const result = formatMcpStatusReport([]);
|
|
15
|
+
assert.match(result, /no mcp servers configured/i);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("lists all servers with connection status", () => {
|
|
19
|
+
const servers: McpServerStatus[] = [
|
|
20
|
+
{ name: "railway", transport: "stdio", connected: true, toolCount: 5, error: undefined },
|
|
21
|
+
{ name: "linear", transport: "http", connected: false, toolCount: 0, error: undefined },
|
|
22
|
+
];
|
|
23
|
+
const result = formatMcpStatusReport(servers);
|
|
24
|
+
assert.match(result, /railway/);
|
|
25
|
+
assert.match(result, /linear/);
|
|
26
|
+
assert.match(result, /connected/i);
|
|
27
|
+
assert.match(result, /disconnected/i);
|
|
28
|
+
assert.match(result, /5 tools/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("shows error state for servers with errors", () => {
|
|
32
|
+
const servers: McpServerStatus[] = [
|
|
33
|
+
{ name: "broken", transport: "stdio", connected: false, toolCount: 0, error: "Connection refused" },
|
|
34
|
+
];
|
|
35
|
+
const result = formatMcpStatusReport(servers);
|
|
36
|
+
assert.match(result, /error/i);
|
|
37
|
+
assert.match(result, /Connection refused/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("includes server count in header", () => {
|
|
41
|
+
const servers: McpServerStatus[] = [
|
|
42
|
+
{ name: "a", transport: "stdio", connected: true, toolCount: 3, error: undefined },
|
|
43
|
+
{ name: "b", transport: "http", connected: true, toolCount: 2, error: undefined },
|
|
44
|
+
];
|
|
45
|
+
const result = formatMcpStatusReport(servers);
|
|
46
|
+
assert.match(result, /2/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─── formatMcpServerDetail ──────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
describe("formatMcpServerDetail", () => {
|
|
53
|
+
test("shows server name and transport", () => {
|
|
54
|
+
const result = formatMcpServerDetail({
|
|
55
|
+
name: "railway",
|
|
56
|
+
transport: "stdio",
|
|
57
|
+
connected: true,
|
|
58
|
+
toolCount: 3,
|
|
59
|
+
tools: ["railway_list_projects", "railway_deploy", "railway_logs"],
|
|
60
|
+
error: undefined,
|
|
61
|
+
});
|
|
62
|
+
assert.match(result, /railway/);
|
|
63
|
+
assert.match(result, /stdio/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("lists individual tools when available", () => {
|
|
67
|
+
const result = formatMcpServerDetail({
|
|
68
|
+
name: "railway",
|
|
69
|
+
transport: "stdio",
|
|
70
|
+
connected: true,
|
|
71
|
+
toolCount: 2,
|
|
72
|
+
tools: ["railway_list_projects", "railway_deploy"],
|
|
73
|
+
error: undefined,
|
|
74
|
+
});
|
|
75
|
+
assert.match(result, /railway_list_projects/);
|
|
76
|
+
assert.match(result, /railway_deploy/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("shows error message for failed servers", () => {
|
|
80
|
+
const result = formatMcpServerDetail({
|
|
81
|
+
name: "broken",
|
|
82
|
+
transport: "stdio",
|
|
83
|
+
connected: false,
|
|
84
|
+
toolCount: 0,
|
|
85
|
+
tools: [],
|
|
86
|
+
error: "spawn ENOENT",
|
|
87
|
+
});
|
|
88
|
+
assert.match(result, /error/i);
|
|
89
|
+
assert.match(result, /spawn ENOENT/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("shows disconnected status with no tools", () => {
|
|
93
|
+
const result = formatMcpServerDetail({
|
|
94
|
+
name: "offline",
|
|
95
|
+
transport: "http",
|
|
96
|
+
connected: false,
|
|
97
|
+
toolCount: 0,
|
|
98
|
+
tools: [],
|
|
99
|
+
error: undefined,
|
|
100
|
+
});
|
|
101
|
+
assert.match(result, /disconnected/i);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* merge-conflict-stops-loop.test.ts — #2330
|
|
3
|
+
*
|
|
4
|
+
* When a squash merge has real code conflicts (not just .gsd/ files),
|
|
5
|
+
* the merge retries forever because MergeConflictError is caught
|
|
6
|
+
* silently in mergeAndExit. This test verifies that:
|
|
7
|
+
* 1. worktree-resolver re-throws MergeConflictError for code conflicts
|
|
8
|
+
* 2. auto/phases.ts wraps mergeAndExit calls to stop the loop on conflict
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
14
|
+
|
|
15
|
+
const { assertTrue, report } = createTestContext();
|
|
16
|
+
|
|
17
|
+
const resolverPath = join(import.meta.dirname, "..", "worktree-resolver.ts");
|
|
18
|
+
const resolverSrc = readFileSync(resolverPath, "utf-8");
|
|
19
|
+
|
|
20
|
+
const phasesPath = join(import.meta.dirname, "..", "auto", "phases.ts");
|
|
21
|
+
const phasesSrc = readFileSync(phasesPath, "utf-8");
|
|
22
|
+
|
|
23
|
+
console.log("\n=== #2330: Merge conflict stops auto loop ===");
|
|
24
|
+
|
|
25
|
+
// ── Test 1: worktree-resolver re-throws MergeConflictError ──────────────
|
|
26
|
+
|
|
27
|
+
const methodStart = resolverSrc.indexOf("Worktree-mode merge:");
|
|
28
|
+
assertTrue(methodStart > 0, "worktree-resolver has _mergeWorktreeMode method");
|
|
29
|
+
|
|
30
|
+
const methodBody = resolverSrc.slice(methodStart, methodStart + 5000);
|
|
31
|
+
const rethrowsConflict =
|
|
32
|
+
methodBody.includes("MergeConflictError") &&
|
|
33
|
+
methodBody.includes("throw err");
|
|
34
|
+
|
|
35
|
+
assertTrue(
|
|
36
|
+
rethrowsConflict,
|
|
37
|
+
"worktree-resolver._mergeWorktreeMode re-throws MergeConflictError (#2330)",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// ── Test 2: auto/phases.ts imports and uses MergeConflictError ──────────
|
|
41
|
+
|
|
42
|
+
assertTrue(
|
|
43
|
+
phasesSrc.includes("MergeConflictError") && phasesSrc.includes("mergeAndExit"),
|
|
44
|
+
"auto/phases.ts handles MergeConflictError from mergeAndExit (#2330)",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// ── Test 3: The handler stops the loop (doesn't just warn) ──────────────
|
|
48
|
+
|
|
49
|
+
// Find the instanceof MergeConflictError check (not the import line)
|
|
50
|
+
const instanceofIdx = phasesSrc.indexOf("instanceof MergeConflictError");
|
|
51
|
+
assertTrue(instanceofIdx > 0, "auto/phases.ts has instanceof MergeConflictError check");
|
|
52
|
+
|
|
53
|
+
if (instanceofIdx > 0) {
|
|
54
|
+
const afterHandler = phasesSrc.slice(instanceofIdx, instanceofIdx + 500);
|
|
55
|
+
const stopsLoop =
|
|
56
|
+
afterHandler.includes("stopAuto") ||
|
|
57
|
+
afterHandler.includes('action: "break"') ||
|
|
58
|
+
afterHandler.includes("reason: \"merge-conflict\"");
|
|
59
|
+
|
|
60
|
+
assertTrue(
|
|
61
|
+
stopsLoop,
|
|
62
|
+
"auto/phases.ts stops the loop when merge conflict is detected (#2330)",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
report();
|
|
@@ -147,12 +147,12 @@ test("plan-slice prompt no longer frames direct PLAN writes as the source of tru
|
|
|
147
147
|
assert.match(prompt, /Do \*\*not\*\* rely on direct `PLAN\.md` writes as the source of truth/i);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
test("plan-slice prompt explicitly names gsd_plan_slice
|
|
150
|
+
test("plan-slice prompt explicitly names gsd_plan_slice as DB-backed planning tool", () => {
|
|
151
151
|
const prompt = readPrompt("plan-slice");
|
|
152
152
|
assert.match(prompt, /gsd_plan_slice/);
|
|
153
153
|
assert.match(prompt, /gsd_plan_task/);
|
|
154
|
-
// The prompt should describe
|
|
155
|
-
assert.match(prompt, /DB-backed
|
|
154
|
+
// The prompt should describe the DB-backed tool as the canonical write path
|
|
155
|
+
assert.match(prompt, /DB-backed tool is the canonical write path/i);
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
test("plan-slice prompt does not instruct direct file writes as a primary step", () => {
|
|
@@ -161,14 +161,18 @@ test("plan-slice prompt does not instruct direct file writes as a primary step",
|
|
|
161
161
|
assert.doesNotMatch(prompt, /^\d+\.\s+Write `?\{\{outputPath\}\}`?\s*$/m);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
test("plan-slice prompt
|
|
164
|
+
test("plan-slice prompt clarifies gsd_plan_slice handles task persistence", () => {
|
|
165
165
|
const prompt = readPrompt("plan-slice");
|
|
166
|
-
|
|
166
|
+
// gsd_plan_slice persists tasks in its transaction — no separate gsd_plan_task calls needed
|
|
167
|
+
assert.match(prompt, /gsd_plan_task/);
|
|
168
|
+
assert.match(prompt, /gsd_plan_slice` handles task persistence/i);
|
|
167
169
|
});
|
|
168
170
|
|
|
169
|
-
test("replan-slice prompt
|
|
171
|
+
test("replan-slice prompt uses gsd_replan_slice as canonical DB-backed tool", () => {
|
|
170
172
|
const prompt = readPrompt("replan-slice");
|
|
171
|
-
assert.match(prompt, /
|
|
173
|
+
assert.match(prompt, /gsd_replan_slice/);
|
|
174
|
+
// Degraded fallback (direct file writes) was removed — DB tools are always available
|
|
175
|
+
assert.doesNotMatch(prompt, /Degraded fallback/i);
|
|
172
176
|
});
|
|
173
177
|
|
|
174
178
|
test("reassess-roadmap prompt references gsd_reassess_roadmap tool", () => {
|