gsd-pi 2.49.0-dev.de3d9f6 → 2.50.0-dev.9476db8
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/headless-ui.js +12 -2
- package/dist/headless.js +29 -13
- package/dist/resources/extensions/gsd/auto/infra-errors.js +1 -0
- package/dist/resources/extensions/gsd/auto/phases.js +11 -11
- package/dist/resources/extensions/gsd/auto/resolve.js +2 -2
- package/dist/resources/extensions/gsd/auto/run-unit.js +2 -2
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +8 -10
- package/dist/resources/extensions/gsd/auto-dashboard.js +6 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +33 -21
- package/dist/resources/extensions/gsd/auto-post-unit.js +17 -24
- package/dist/resources/extensions/gsd/auto-prompts.js +102 -21
- package/dist/resources/extensions/gsd/auto-recovery.js +62 -184
- package/dist/resources/extensions/gsd/auto-start.js +4 -31
- package/dist/resources/extensions/gsd/auto-timers.js +2 -2
- package/dist/resources/extensions/gsd/auto-verification.js +4 -7
- package/dist/resources/extensions/gsd/auto-worktree.js +257 -113
- package/dist/resources/extensions/gsd/auto.js +7 -5
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +89 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -1
- package/dist/resources/extensions/gsd/branch-patterns.js +13 -0
- package/dist/resources/extensions/gsd/doctor-checks.js +5 -1234
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +168 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +28 -7
- package/dist/resources/extensions/gsd/doctor-git-checks.js +405 -0
- package/dist/resources/extensions/gsd/doctor-global-checks.js +74 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +600 -0
- package/dist/resources/extensions/gsd/doctor.js +9 -1
- package/dist/resources/extensions/gsd/extension-manifest.json +1 -1
- package/dist/resources/extensions/gsd/git-service.js +9 -10
- package/dist/resources/extensions/gsd/gsd-db.js +124 -1
- package/dist/resources/extensions/gsd/guided-flow-queue.js +10 -11
- package/dist/resources/extensions/gsd/markdown-renderer.js +33 -5
- package/dist/resources/extensions/gsd/preferences-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +39 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +27 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +9 -8
- package/dist/resources/extensions/gsd/prompts/execute-task.md +16 -13
- package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/dist/resources/extensions/gsd/prompts/gate-evaluate.md +32 -0
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +8 -3
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +1 -1
- package/dist/resources/extensions/gsd/repo-identity.js +29 -0
- package/dist/resources/extensions/gsd/roadmap-slices.js +2 -2
- package/dist/resources/extensions/gsd/session-forensics.js +6 -11
- package/dist/resources/extensions/gsd/session-lock.js +67 -56
- package/dist/resources/extensions/gsd/state.js +34 -7
- package/dist/resources/extensions/gsd/templates/milestone-summary.md +8 -0
- package/dist/resources/extensions/gsd/templates/plan.md +16 -0
- package/dist/resources/extensions/gsd/templates/roadmap.md +13 -0
- package/dist/resources/extensions/gsd/templates/slice-summary.md +9 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +24 -0
- package/dist/resources/extensions/gsd/tools/plan-slice.js +14 -1
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -3
- package/dist/resources/extensions/gsd/verdict-parser.js +84 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +24 -0
- package/dist/resources/extensions/gsd/worktree.js +3 -2
- package/dist/resources/extensions/remote-questions/config.js +3 -5
- package/dist/resources/extensions/search-the-web/native-search.js +8 -3
- package/dist/resources/extensions/search-the-web/tool-search.js +19 -2
- package/dist/resources/skills/github-workflows/references/gh/SKILL.md +22 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/required-server-files.json +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.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- 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/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- 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 +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/chunks/229.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.7c75ac378de0f2b5.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-0a4cd455ec4197d2.js → webpack-2473ce2c3879fff4.js} +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -1
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +4 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +39 -10
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-codex-responses.ts +39 -8
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/blob-store.js +8 -3
- package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +9 -2
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.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 -32
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/jsonl.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/jsonl.js +5 -0
- package/packages/pi-coding-agent/dist/modes/rpc/jsonl.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +0 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/blob-store.ts +6 -3
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +9 -2
- package/packages/pi-coding-agent/src/core/retry-handler.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +7 -32
- package/packages/pi-coding-agent/src/modes/rpc/jsonl.ts +6 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +0 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/infra-errors.ts +1 -0
- package/src/resources/extensions/gsd/auto/phases.ts +10 -11
- package/src/resources/extensions/gsd/auto/resolve.ts +3 -3
- package/src/resources/extensions/gsd/auto/run-unit.ts +2 -2
- package/src/resources/extensions/gsd/auto/session.ts +5 -0
- package/src/resources/extensions/gsd/auto/types.ts +13 -0
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +19 -21
- package/src/resources/extensions/gsd/auto-dashboard.ts +5 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +39 -21
- package/src/resources/extensions/gsd/auto-loop.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +18 -28
- package/src/resources/extensions/gsd/auto-prompts.ts +113 -19
- package/src/resources/extensions/gsd/auto-recovery.ts +65 -199
- package/src/resources/extensions/gsd/auto-start.ts +7 -27
- package/src/resources/extensions/gsd/auto-timers.ts +2 -2
- package/src/resources/extensions/gsd/auto-verification.ts +4 -7
- package/src/resources/extensions/gsd/auto-worktree.ts +305 -108
- package/src/resources/extensions/gsd/auto.ts +11 -10
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +93 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
- package/src/resources/extensions/gsd/branch-patterns.ts +16 -0
- package/src/resources/extensions/gsd/doctor-checks.ts +5 -1291
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +182 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +30 -7
- package/src/resources/extensions/gsd/doctor-git-checks.ts +415 -0
- package/src/resources/extensions/gsd/doctor-global-checks.ts +84 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +626 -0
- package/src/resources/extensions/gsd/doctor.ts +9 -1
- package/src/resources/extensions/gsd/extension-manifest.json +1 -1
- package/src/resources/extensions/gsd/git-service.ts +7 -15
- package/src/resources/extensions/gsd/gsd-db.ts +150 -2
- package/src/resources/extensions/gsd/guided-flow-queue.ts +11 -12
- package/src/resources/extensions/gsd/markdown-renderer.ts +37 -4
- package/src/resources/extensions/gsd/preferences-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +37 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +27 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +9 -8
- package/src/resources/extensions/gsd/prompts/execute-task.md +16 -13
- package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/src/resources/extensions/gsd/prompts/gate-evaluate.md +32 -0
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +8 -3
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
- package/src/resources/extensions/gsd/repo-identity.ts +28 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +2 -2
- package/src/resources/extensions/gsd/session-forensics.ts +6 -11
- package/src/resources/extensions/gsd/session-lock.ts +92 -64
- package/src/resources/extensions/gsd/state.ts +38 -5
- package/src/resources/extensions/gsd/templates/milestone-summary.md +8 -0
- package/src/resources/extensions/gsd/templates/plan.md +16 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +13 -0
- package/src/resources/extensions/gsd/templates/slice-summary.md +9 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +24 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +1 -81
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +9 -12
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +65 -1
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +189 -0
- package/src/resources/extensions/gsd/tests/gate-storage.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/infra-error.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/quality-gates.test.ts +347 -0
- package/src/resources/extensions/gsd/tests/queue-completed-milestone-perf.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +20 -16
- package/src/resources/extensions/gsd/tests/session-lock-transient-read.test.ts +223 -0
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +44 -4
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +0 -16
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +67 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +204 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +16 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +3 -3
- package/src/resources/extensions/gsd/types.ts +30 -0
- package/src/resources/extensions/gsd/verdict-parser.ts +95 -0
- package/src/resources/extensions/gsd/verification-gate.ts +0 -2
- package/src/resources/extensions/gsd/worktree-resolver.ts +31 -0
- package/src/resources/extensions/gsd/worktree.ts +3 -2
- package/src/resources/extensions/remote-questions/config.ts +3 -5
- package/src/resources/extensions/search-the-web/native-search.ts +8 -3
- package/src/resources/extensions/search-the-web/tool-search.ts +22 -2
- package/src/resources/skills/github-workflows/references/gh/SKILL.md +22 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +0 -191
- package/dist/resources/extensions/gsd/resource-version.js +0 -97
- package/dist/web/standalone/.next/static/chunks/4024.11ca5c01938e5948.js +0 -9
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +0 -234
- package/src/resources/extensions/gsd/resource-version.ts +0 -101
- /package/dist/web/standalone/.next/static/{ceckLbAMjhzHaQ3RPtJnT → MkE9kzqUGny3-cSE0GNnm}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ceckLbAMjhzHaQ3RPtJnT → MkE9kzqUGny3-cSE0GNnm}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-lock-transient-read.test.ts — Tests for transient lock file unreadability (#2324).
|
|
3
|
+
*
|
|
4
|
+
* Regression coverage for:
|
|
5
|
+
* #2324 onCompromised declares lock lost when the lock file is temporarily
|
|
6
|
+
* unreadable (NFS/CIFS latency, macOS APFS snapshot, concurrent process
|
|
7
|
+
* briefly holding the file).
|
|
8
|
+
*
|
|
9
|
+
* Tests:
|
|
10
|
+
* - readExistingLockDataWithRetry retries on transient read failure
|
|
11
|
+
* - readExistingLockDataWithRetry returns data when file becomes readable after retries
|
|
12
|
+
* - readExistingLockDataWithRetry returns null only when ALL retries exhausted
|
|
13
|
+
* - onCompromised does not declare compromise when lock file is transiently unreadable
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, renameSync, unlinkSync, chmodSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { execSync, spawn } from 'node:child_process';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
acquireSessionLock,
|
|
23
|
+
getSessionLockStatus,
|
|
24
|
+
releaseSessionLock,
|
|
25
|
+
readExistingLockDataWithRetry,
|
|
26
|
+
type SessionLockData,
|
|
27
|
+
} from '../session-lock.ts';
|
|
28
|
+
import { gsdRoot } from '../paths.ts';
|
|
29
|
+
import { createTestContext } from './test-helpers.ts';
|
|
30
|
+
|
|
31
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
32
|
+
|
|
33
|
+
async function main(): Promise<void> {
|
|
34
|
+
|
|
35
|
+
// ─── 1. readExistingLockDataWithRetry succeeds on first read when file is fine ─
|
|
36
|
+
console.log('\n=== 1. readExistingLockDataWithRetry reads file normally ===');
|
|
37
|
+
{
|
|
38
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-transient-'));
|
|
39
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const lockFile = join(gsdRoot(base), 'auto.lock');
|
|
43
|
+
const lockData: SessionLockData = {
|
|
44
|
+
pid: process.pid,
|
|
45
|
+
startedAt: new Date().toISOString(),
|
|
46
|
+
unitType: 'execute-task',
|
|
47
|
+
unitId: 'M001/S01/T01',
|
|
48
|
+
unitStartedAt: new Date().toISOString(),
|
|
49
|
+
sessionFile: 'test-session.json',
|
|
50
|
+
};
|
|
51
|
+
writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
|
|
52
|
+
|
|
53
|
+
const result = readExistingLockDataWithRetry(lockFile);
|
|
54
|
+
assertTrue(result !== null, 'data returned for readable file');
|
|
55
|
+
assertEq(result!.pid, process.pid, 'correct PID read');
|
|
56
|
+
assertEq(result!.sessionFile, 'test-session.json', 'correct sessionFile read');
|
|
57
|
+
} finally {
|
|
58
|
+
rmSync(base, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── 2. readExistingLockDataWithRetry returns null for truly missing file ──
|
|
63
|
+
console.log('\n=== 2. readExistingLockDataWithRetry returns null for missing file ===');
|
|
64
|
+
{
|
|
65
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-transient-'));
|
|
66
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const lockFile = join(gsdRoot(base), 'auto.lock');
|
|
70
|
+
// File doesn't exist
|
|
71
|
+
const result = readExistingLockDataWithRetry(lockFile, { maxAttempts: 2, delayMs: 10 });
|
|
72
|
+
assertEq(result, null, 'null for truly missing file after retries');
|
|
73
|
+
} finally {
|
|
74
|
+
rmSync(base, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── 3. readExistingLockDataWithRetry recovers after transient rename ──────
|
|
79
|
+
console.log('\n=== 3. readExistingLockDataWithRetry recovers after transient unavailability ===');
|
|
80
|
+
{
|
|
81
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-transient-'));
|
|
82
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const lockFile = join(gsdRoot(base), 'auto.lock');
|
|
86
|
+
const tmpFile = lockFile + '.hidden';
|
|
87
|
+
const lockData: SessionLockData = {
|
|
88
|
+
pid: process.pid,
|
|
89
|
+
startedAt: new Date().toISOString(),
|
|
90
|
+
unitType: 'execute-task',
|
|
91
|
+
unitId: 'M001/S01/T01',
|
|
92
|
+
unitStartedAt: new Date().toISOString(),
|
|
93
|
+
sessionFile: 'recovery-session.json',
|
|
94
|
+
};
|
|
95
|
+
writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
|
|
96
|
+
|
|
97
|
+
// Simulate transient unavailability: move file away, spawn a child process
|
|
98
|
+
// to restore it after 100ms. The child runs outside our event loop so it
|
|
99
|
+
// fires even during busy-wait retries.
|
|
100
|
+
renameSync(lockFile, tmpFile);
|
|
101
|
+
spawn('bash', ['-c', `sleep 0.1 && mv "${tmpFile}" "${lockFile}"`], { stdio: 'ignore', detached: true }).unref();
|
|
102
|
+
|
|
103
|
+
// With retries (3 attempts, 200ms delay), it should recover on 2nd or 3rd attempt
|
|
104
|
+
const result = readExistingLockDataWithRetry(lockFile, { maxAttempts: 3, delayMs: 200 });
|
|
105
|
+
assertTrue(result !== null, 'data recovered after transient unavailability');
|
|
106
|
+
if (result) {
|
|
107
|
+
assertEq(result.pid, process.pid, 'correct PID after recovery');
|
|
108
|
+
assertEq(result.sessionFile, 'recovery-session.json', 'correct sessionFile after recovery');
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
rmSync(base, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── 4. readExistingLockDataWithRetry recovers from transient permission error ─
|
|
116
|
+
console.log('\n=== 4. readExistingLockDataWithRetry recovers from transient permission error ===');
|
|
117
|
+
{
|
|
118
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-transient-'));
|
|
119
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const lockFile = join(gsdRoot(base), 'auto.lock');
|
|
123
|
+
const lockData: SessionLockData = {
|
|
124
|
+
pid: process.pid,
|
|
125
|
+
startedAt: new Date().toISOString(),
|
|
126
|
+
unitType: 'execute-task',
|
|
127
|
+
unitId: 'M001/S01/T01',
|
|
128
|
+
unitStartedAt: new Date().toISOString(),
|
|
129
|
+
sessionFile: 'perm-session.json',
|
|
130
|
+
};
|
|
131
|
+
writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
|
|
132
|
+
|
|
133
|
+
// Remove read permission to simulate NFS/CIFS latency, then spawn a child
|
|
134
|
+
// to restore permissions after 100ms (runs outside our event loop).
|
|
135
|
+
chmodSync(lockFile, 0o000);
|
|
136
|
+
spawn('bash', ['-c', `sleep 0.1 && chmod 644 "${lockFile}"`], { stdio: 'ignore', detached: true }).unref();
|
|
137
|
+
|
|
138
|
+
const result = readExistingLockDataWithRetry(lockFile, { maxAttempts: 3, delayMs: 200 });
|
|
139
|
+
assertTrue(result !== null, 'data recovered after transient permission error');
|
|
140
|
+
if (result) {
|
|
141
|
+
assertEq(result.pid, process.pid, 'correct PID after permission recovery');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Ensure permissions restored for cleanup
|
|
145
|
+
try { chmodSync(lockFile, 0o644); } catch { /* best-effort */ }
|
|
146
|
+
} finally {
|
|
147
|
+
rmSync(base, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── 5. getSessionLockStatus does not false-positive on transient read failure ─
|
|
152
|
+
console.log('\n=== 5. getSessionLockStatus tolerates transient lock file unavailability ===');
|
|
153
|
+
{
|
|
154
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-transient-'));
|
|
155
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const result = acquireSessionLock(base);
|
|
159
|
+
assertTrue(result.acquired, 'lock acquired');
|
|
160
|
+
|
|
161
|
+
// Validate works initially
|
|
162
|
+
const status1 = getSessionLockStatus(base);
|
|
163
|
+
assertTrue(status1.valid, 'lock valid before transient failure');
|
|
164
|
+
|
|
165
|
+
// Temporarily hide the lock file
|
|
166
|
+
const lockFile = join(gsdRoot(base), 'auto.lock');
|
|
167
|
+
const tmpFile = lockFile + '.hidden';
|
|
168
|
+
renameSync(lockFile, tmpFile);
|
|
169
|
+
|
|
170
|
+
// Schedule restoration
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
try { renameSync(tmpFile, lockFile); } catch { /* best-effort */ }
|
|
173
|
+
}, 30);
|
|
174
|
+
|
|
175
|
+
// Small delay to ensure restoration runs, then check — with the OS lock
|
|
176
|
+
// still held, getSessionLockStatus should return valid=true even if the
|
|
177
|
+
// lock file was briefly missing (it checks _releaseFunction first).
|
|
178
|
+
await new Promise(r => setTimeout(r, 60));
|
|
179
|
+
const status2 = getSessionLockStatus(base);
|
|
180
|
+
assertTrue(status2.valid, 'lock still valid after transient file disappearance (OS lock held)');
|
|
181
|
+
|
|
182
|
+
// Restore if not yet restored
|
|
183
|
+
try { renameSync(tmpFile, lockFile); } catch { /* already restored */ }
|
|
184
|
+
|
|
185
|
+
releaseSessionLock(base);
|
|
186
|
+
} finally {
|
|
187
|
+
rmSync(base, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── 6. Retry defaults: 3 attempts with 200ms delay ────────────────────────
|
|
192
|
+
console.log('\n=== 6. Default retry params: function works with defaults ===');
|
|
193
|
+
{
|
|
194
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-transient-'));
|
|
195
|
+
mkdirSync(join(base, '.gsd'), { recursive: true });
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const lockFile = join(gsdRoot(base), 'auto.lock');
|
|
199
|
+
const lockData: SessionLockData = {
|
|
200
|
+
pid: process.pid,
|
|
201
|
+
startedAt: new Date().toISOString(),
|
|
202
|
+
unitType: 'execute-task',
|
|
203
|
+
unitId: 'M001/S01/T01',
|
|
204
|
+
unitStartedAt: new Date().toISOString(),
|
|
205
|
+
sessionFile: 'status-session.json',
|
|
206
|
+
};
|
|
207
|
+
writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
|
|
208
|
+
|
|
209
|
+
// Call with no options — uses defaults (3 attempts, 200ms)
|
|
210
|
+
const result = readExistingLockDataWithRetry(lockFile);
|
|
211
|
+
assertTrue(result !== null, 'default params work for readable file');
|
|
212
|
+
} finally {
|
|
213
|
+
rmSync(base, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
report();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main().catch((error) => {
|
|
221
|
+
console.error(error);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
@@ -75,7 +75,7 @@ test("buildSkillActivationBlock activates skills via prefer_skills when context
|
|
|
75
75
|
prefer_skills: ["react"],
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
assert.match(result, /Call Skill\('react'\)/);
|
|
78
|
+
assert.match(result, /Call Skill\(\{ skill: 'react' \}\)/);
|
|
79
79
|
assert.doesNotMatch(result, /swiftui/);
|
|
80
80
|
} finally {
|
|
81
81
|
cleanup(base);
|
|
@@ -92,7 +92,7 @@ test("buildSkillActivationBlock includes always_use_skills from preferences usin
|
|
|
92
92
|
always_use_skills: ["swift-testing"],
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
assert.equal(result, "<skill_activation>Call Skill('swift-testing').</skill_activation>");
|
|
95
|
+
assert.equal(result, "<skill_activation>Call Skill({ skill: 'swift-testing' }).</skill_activation>");
|
|
96
96
|
} finally {
|
|
97
97
|
cleanup(base);
|
|
98
98
|
}
|
|
@@ -120,8 +120,8 @@ test("buildSkillActivationBlock includes skill_rules matches and task-plan skill
|
|
|
120
120
|
skill_rules: [{ when: "prisma database schema", use: ["prisma"] }],
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
assert.match(result, /Call Skill\('accessibility'\)/);
|
|
124
|
-
assert.match(result, /Call Skill\('prisma'\)/);
|
|
123
|
+
assert.match(result, /Call Skill\(\{ skill: 'accessibility' \}\)/);
|
|
124
|
+
assert.match(result, /Call Skill\(\{ skill: 'prisma' \}\)/);
|
|
125
125
|
} finally {
|
|
126
126
|
cleanup(base);
|
|
127
127
|
}
|
|
@@ -191,3 +191,43 @@ test("buildSkillActivationBlock does not activate skills from extraContext or ta
|
|
|
191
191
|
cleanup(base);
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
|
+
|
|
195
|
+
test("buildSkillActivationBlock rejects skill names with special characters", () => {
|
|
196
|
+
const base = makeTempBase();
|
|
197
|
+
try {
|
|
198
|
+
// Skill names with quotes, braces, or other non-alphanumeric characters are
|
|
199
|
+
// rejected by the SAFE_SKILL_NAME guard to prevent prompt injection.
|
|
200
|
+
writeSkill(base, "my-skill's", "Skill with apostrophe in name.");
|
|
201
|
+
loadOnlyTestSkills(base);
|
|
202
|
+
|
|
203
|
+
const result = buildBlock(base, {}, {
|
|
204
|
+
always_use_skills: ["my-skill's"],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Unsafe skill name is filtered out — empty result
|
|
208
|
+
assert.equal(result, "");
|
|
209
|
+
} finally {
|
|
210
|
+
cleanup(base);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("buildSkillActivationBlock allows valid skill names and rejects invalid ones", () => {
|
|
215
|
+
const base = makeTempBase();
|
|
216
|
+
try {
|
|
217
|
+
writeSkill(base, "react", "React skill.");
|
|
218
|
+
writeSkill(base, "bad'name", "Injection attempt.");
|
|
219
|
+
writeSkill(base, "good-skill-2", "Another valid skill.");
|
|
220
|
+
loadOnlyTestSkills(base);
|
|
221
|
+
|
|
222
|
+
const result = buildBlock(base, {}, {
|
|
223
|
+
always_use_skills: ["react", "bad'name", "good-skill-2"],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
assert.match(result, /skill_activation/);
|
|
227
|
+
assert.match(result, /Call Skill\(\{ skill: 'react' \}\)/);
|
|
228
|
+
assert.match(result, /Call Skill\(\{ skill: 'good-skill-2' \}\)/);
|
|
229
|
+
assert.doesNotMatch(result, /bad'name/);
|
|
230
|
+
} finally {
|
|
231
|
+
cleanup(base);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
@@ -44,7 +44,7 @@ console.log('\n── Tool naming: registration count ──');
|
|
|
44
44
|
const pi = makeMockPi();
|
|
45
45
|
registerDbTools(pi);
|
|
46
46
|
|
|
47
|
-
assert.deepStrictEqual(pi.tools.length,
|
|
47
|
+
assert.deepStrictEqual(pi.tools.length, 27, 'Should register exactly 27 tools (13 canonical + 13 aliases + 1 gate tool)');
|
|
48
48
|
|
|
49
49
|
// ─── Both names exist for each pair ──────────────────────────────────────────
|
|
50
50
|
|
|
@@ -6,7 +6,8 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
8
|
import { deriveState, isValidationTerminal } from "../state.ts";
|
|
9
|
-
import { resolveExpectedArtifactPath,
|
|
9
|
+
import { resolveExpectedArtifactPath, diagnoseExpectedArtifact } from "../auto-artifact-paths.ts";
|
|
10
|
+
import { verifyExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
|
|
10
11
|
import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
|
|
11
12
|
import type { GSDState } from "../types.ts";
|
|
12
13
|
import { clearPathCache } from "../paths.ts";
|
|
@@ -226,8 +226,6 @@ describe("verification-gate: execution", () => {
|
|
|
226
226
|
|
|
227
227
|
test("all commands pass → gate passes", () => {
|
|
228
228
|
const result = runVerificationGate({
|
|
229
|
-
basePath: tmp,
|
|
230
|
-
unitId: "T01",
|
|
231
229
|
cwd: tmp,
|
|
232
230
|
preferenceCommands: ["echo hello", "echo world"],
|
|
233
231
|
});
|
|
@@ -243,8 +241,6 @@ describe("verification-gate: execution", () => {
|
|
|
243
241
|
|
|
244
242
|
test("one command fails → gate fails with exit code + stderr", () => {
|
|
245
243
|
const result = runVerificationGate({
|
|
246
|
-
basePath: tmp,
|
|
247
|
-
unitId: "T01",
|
|
248
244
|
cwd: tmp,
|
|
249
245
|
preferenceCommands: ["echo ok", "sh -c 'echo err >&2; exit 1'"],
|
|
250
246
|
});
|
|
@@ -257,8 +253,6 @@ describe("verification-gate: execution", () => {
|
|
|
257
253
|
|
|
258
254
|
test("no commands discovered → gate passes with 0 checks", () => {
|
|
259
255
|
const result = runVerificationGate({
|
|
260
|
-
basePath: tmp,
|
|
261
|
-
unitId: "T01",
|
|
262
256
|
cwd: tmp,
|
|
263
257
|
});
|
|
264
258
|
assert.equal(result.passed, true);
|
|
@@ -268,8 +262,6 @@ describe("verification-gate: execution", () => {
|
|
|
268
262
|
|
|
269
263
|
test("command not found → exit code 127", () => {
|
|
270
264
|
const result = runVerificationGate({
|
|
271
|
-
basePath: tmp,
|
|
272
|
-
unitId: "T01",
|
|
273
265
|
cwd: tmp,
|
|
274
266
|
preferenceCommands: ["__nonexistent_command_xyz_42__"],
|
|
275
267
|
});
|
|
@@ -289,8 +281,6 @@ describe("verification-gate: execution", () => {
|
|
|
289
281
|
const script = [
|
|
290
282
|
`import { runVerificationGate } from ${JSON.stringify(pathToFileURL(gatePath).href)};`,
|
|
291
283
|
`runVerificationGate({`,
|
|
292
|
-
` basePath: ${JSON.stringify(tmp)},`,
|
|
293
|
-
` unitId: "T-DEP",`,
|
|
294
284
|
` cwd: ${JSON.stringify(tmp)},`,
|
|
295
285
|
` preferenceCommands: ["echo dep0190-check"],`,
|
|
296
286
|
`});`,
|
|
@@ -317,8 +307,6 @@ describe("verification-gate: execution", () => {
|
|
|
317
307
|
|
|
318
308
|
test("each check has durationMs", () => {
|
|
319
309
|
const result = runVerificationGate({
|
|
320
|
-
basePath: tmp,
|
|
321
|
-
unitId: "T01",
|
|
322
310
|
cwd: tmp,
|
|
323
311
|
preferenceCommands: ["echo fast"],
|
|
324
312
|
});
|
|
@@ -330,8 +318,6 @@ describe("verification-gate: execution", () => {
|
|
|
330
318
|
test("one command fails — remaining commands still run (non-short-circuit)", () => {
|
|
331
319
|
// First fails, second and third should still execute
|
|
332
320
|
const result = runVerificationGate({
|
|
333
|
-
basePath: tmp,
|
|
334
|
-
unitId: "T02",
|
|
335
321
|
cwd: tmp,
|
|
336
322
|
preferenceCommands: [
|
|
337
323
|
"sh -c 'exit 1'",
|
|
@@ -351,8 +337,6 @@ describe("verification-gate: execution", () => {
|
|
|
351
337
|
test("gate execution uses cwd for spawnSync", () => {
|
|
352
338
|
// pwd should report the temp dir
|
|
353
339
|
const result = runVerificationGate({
|
|
354
|
-
basePath: tmp,
|
|
355
|
-
unitId: "T02",
|
|
356
340
|
cwd: tmp,
|
|
357
341
|
preferenceCommands: ["pwd"],
|
|
358
342
|
});
|
|
@@ -846,3 +846,70 @@ test("GitService is rebuilt with originalBasePath after exitMilestone", () => {
|
|
|
846
846
|
|
|
847
847
|
assert.equal(gitServiceBasePath, "/project"); // project root, not worktree
|
|
848
848
|
});
|
|
849
|
+
|
|
850
|
+
// ─── Isolation Degradation Tests (#2483) ──────────────────────────────────
|
|
851
|
+
|
|
852
|
+
test("enterMilestone sets isolationDegraded when worktree creation throws (#2483)", () => {
|
|
853
|
+
const s = makeSession();
|
|
854
|
+
const deps = makeDeps({
|
|
855
|
+
getAutoWorktreePath: () => null,
|
|
856
|
+
createAutoWorktree: () => {
|
|
857
|
+
throw new Error("empty repo");
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
const ctx = makeNotifyCtx();
|
|
861
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
862
|
+
|
|
863
|
+
resolver.enterMilestone("M001", ctx);
|
|
864
|
+
|
|
865
|
+
assert.equal(s.isolationDegraded, true);
|
|
866
|
+
assert.equal(s.basePath, "/project"); // unchanged — error recovery
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test("enterMilestone is no-op when isolationDegraded is true (#2483)", () => {
|
|
870
|
+
const s = makeSession();
|
|
871
|
+
s.isolationDegraded = true;
|
|
872
|
+
const deps = makeDeps();
|
|
873
|
+
const ctx = makeNotifyCtx();
|
|
874
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
875
|
+
|
|
876
|
+
resolver.enterMilestone("M001", ctx);
|
|
877
|
+
|
|
878
|
+
assert.equal(s.basePath, "/project"); // unchanged
|
|
879
|
+
assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
|
|
880
|
+
assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
|
|
881
|
+
assert.equal(findCalls(deps.calls, "shouldUseWorktreeIsolation").length, 0);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
test("mergeAndExit is no-op when isolationDegraded is true (#2483)", () => {
|
|
885
|
+
const s = makeSession({
|
|
886
|
+
basePath: "/project",
|
|
887
|
+
originalBasePath: "/project",
|
|
888
|
+
});
|
|
889
|
+
s.isolationDegraded = true;
|
|
890
|
+
const deps = makeDeps({
|
|
891
|
+
getIsolationMode: () => "worktree",
|
|
892
|
+
});
|
|
893
|
+
const ctx = makeNotifyCtx();
|
|
894
|
+
const resolver = new WorktreeResolver(s, deps);
|
|
895
|
+
|
|
896
|
+
resolver.mergeAndExit("M001", ctx);
|
|
897
|
+
|
|
898
|
+
assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
|
|
899
|
+
assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0);
|
|
900
|
+
assert.equal(findCalls(deps.calls, "getIsolationMode").length, 0);
|
|
901
|
+
assert.ok(
|
|
902
|
+
ctx.messages.some(
|
|
903
|
+
(m) => m.level === "info" && m.msg.includes("isolation was degraded"),
|
|
904
|
+
),
|
|
905
|
+
);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
test("isolationDegraded is reset by session.reset() (#2483)", () => {
|
|
909
|
+
const s = new AutoSession();
|
|
910
|
+
s.isolationDegraded = true;
|
|
911
|
+
|
|
912
|
+
s.reset();
|
|
913
|
+
|
|
914
|
+
assert.equal(s.isolationDegraded, false);
|
|
915
|
+
});
|
|
@@ -27,7 +27,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync
|
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
import { tmpdir } from 'node:os';
|
|
29
29
|
|
|
30
|
-
import { syncProjectRootToWorktree } from '../auto-worktree
|
|
30
|
+
import { syncProjectRootToWorktree } from '../auto-worktree.ts';
|
|
31
31
|
import { syncGsdStateToWorktree, syncWorktreeStateBack } from '../auto-worktree.ts';
|
|
32
32
|
import { describe, test } from 'node:test';
|
|
33
33
|
import assert from 'node:assert/strict';
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-sync-overwrite-loop.test.ts — Regression tests for #1886.
|
|
3
|
+
*
|
|
4
|
+
* Reproduces the infinite validate-milestone loop caused by two bugs
|
|
5
|
+
* in syncProjectRootToWorktree:
|
|
6
|
+
*
|
|
7
|
+
* 1. safeCopyRecursive overwrites worktree-authoritative files (e.g.
|
|
8
|
+
* VALIDATION.md written by validate-milestone gets clobbered by the
|
|
9
|
+
* stale project root copy that lacks the file).
|
|
10
|
+
*
|
|
11
|
+
* 2. completed-units.json is not forward-synced from project root to
|
|
12
|
+
* worktree, so the worktree never learns about already-completed units.
|
|
13
|
+
*
|
|
14
|
+
* Covers:
|
|
15
|
+
* - syncProjectRootToWorktree does NOT overwrite existing worktree files
|
|
16
|
+
* - syncProjectRootToWorktree copies files missing from the worktree
|
|
17
|
+
* - completed-units.json is forward-synced from project root to worktree
|
|
18
|
+
* - completed-units.json sync uses force:true (project root is authoritative)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
mkdtempSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
writeFileSync,
|
|
25
|
+
rmSync,
|
|
26
|
+
existsSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { tmpdir } from "node:os";
|
|
31
|
+
|
|
32
|
+
import { syncProjectRootToWorktree } from "../auto-worktree.ts";
|
|
33
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
34
|
+
|
|
35
|
+
const { assertTrue, assertEq, report } = createTestContext();
|
|
36
|
+
|
|
37
|
+
function createBase(name: string): string {
|
|
38
|
+
const base = mkdtempSync(join(tmpdir(), `gsd-wt-1886-${name}-`));
|
|
39
|
+
mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cleanup(base: string): void {
|
|
44
|
+
rmSync(base, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main(): Promise<void> {
|
|
48
|
+
// ─── 1. Worktree VALIDATION.md must NOT be overwritten by project root ──
|
|
49
|
+
console.log(
|
|
50
|
+
"\n=== 1. #1886: worktree VALIDATION.md preserved (not overwritten) ===",
|
|
51
|
+
);
|
|
52
|
+
{
|
|
53
|
+
const mainBase = createBase("main");
|
|
54
|
+
const wtBase = createBase("wt");
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Project root has an older CONTEXT but no VALIDATION
|
|
58
|
+
const prM004 = join(mainBase, ".gsd", "milestones", "M004");
|
|
59
|
+
mkdirSync(prM004, { recursive: true });
|
|
60
|
+
writeFileSync(join(prM004, "M004-CONTEXT.md"), "# old context");
|
|
61
|
+
|
|
62
|
+
// Worktree has CONTEXT + VALIDATION (written by validate-milestone)
|
|
63
|
+
const wtM004 = join(wtBase, ".gsd", "milestones", "M004");
|
|
64
|
+
mkdirSync(wtM004, { recursive: true });
|
|
65
|
+
writeFileSync(join(wtM004, "M004-CONTEXT.md"), "# worktree context");
|
|
66
|
+
writeFileSync(
|
|
67
|
+
join(wtM004, "M004-VALIDATION.md"),
|
|
68
|
+
"verdict: pass\nremediation_round: 1",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
syncProjectRootToWorktree(mainBase, wtBase, "M004");
|
|
72
|
+
|
|
73
|
+
// VALIDATION.md must still exist in worktree
|
|
74
|
+
assertTrue(
|
|
75
|
+
existsSync(join(wtM004, "M004-VALIDATION.md")),
|
|
76
|
+
"#1886: VALIDATION.md still exists after sync",
|
|
77
|
+
);
|
|
78
|
+
assertEq(
|
|
79
|
+
readFileSync(join(wtM004, "M004-VALIDATION.md"), "utf-8"),
|
|
80
|
+
"verdict: pass\nremediation_round: 1",
|
|
81
|
+
"#1886: VALIDATION.md content preserved",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// CONTEXT.md should NOT be overwritten — worktree version is authoritative
|
|
85
|
+
assertEq(
|
|
86
|
+
readFileSync(join(wtM004, "M004-CONTEXT.md"), "utf-8"),
|
|
87
|
+
"# worktree context",
|
|
88
|
+
"#1886: existing worktree CONTEXT.md not overwritten",
|
|
89
|
+
);
|
|
90
|
+
} finally {
|
|
91
|
+
cleanup(mainBase);
|
|
92
|
+
cleanup(wtBase);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── 2. Missing files ARE still copied from project root ────────────────
|
|
97
|
+
console.log("\n=== 2. #1886: missing worktree files still copied ===");
|
|
98
|
+
{
|
|
99
|
+
const mainBase = createBase("main");
|
|
100
|
+
const wtBase = createBase("wt");
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const prM004 = join(mainBase, ".gsd", "milestones", "M004");
|
|
104
|
+
mkdirSync(prM004, { recursive: true });
|
|
105
|
+
writeFileSync(join(prM004, "M004-CONTEXT.md"), "# from project root");
|
|
106
|
+
writeFileSync(join(prM004, "M004-ROADMAP.md"), "# roadmap");
|
|
107
|
+
|
|
108
|
+
// Worktree has no M004 directory at all
|
|
109
|
+
syncProjectRootToWorktree(mainBase, wtBase, "M004");
|
|
110
|
+
|
|
111
|
+
assertTrue(
|
|
112
|
+
existsSync(join(wtBase, ".gsd", "milestones", "M004", "M004-CONTEXT.md")),
|
|
113
|
+
"#1886: missing CONTEXT.md copied from project root",
|
|
114
|
+
);
|
|
115
|
+
assertTrue(
|
|
116
|
+
existsSync(join(wtBase, ".gsd", "milestones", "M004", "M004-ROADMAP.md")),
|
|
117
|
+
"#1886: missing ROADMAP.md copied from project root",
|
|
118
|
+
);
|
|
119
|
+
} finally {
|
|
120
|
+
cleanup(mainBase);
|
|
121
|
+
cleanup(wtBase);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── 3. completed-units.json forward-synced from project root ───────────
|
|
126
|
+
console.log(
|
|
127
|
+
"\n=== 3. #1886: completed-units.json forward-synced to worktree ===",
|
|
128
|
+
);
|
|
129
|
+
{
|
|
130
|
+
const mainBase = createBase("main");
|
|
131
|
+
const wtBase = createBase("wt");
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// Project root has completed units (authoritative after crash recovery)
|
|
135
|
+
writeFileSync(
|
|
136
|
+
join(mainBase, ".gsd", "completed-units.json"),
|
|
137
|
+
JSON.stringify(["validate-milestone/M004"]),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Worktree has empty completed-units
|
|
141
|
+
writeFileSync(
|
|
142
|
+
join(wtBase, ".gsd", "completed-units.json"),
|
|
143
|
+
JSON.stringify([]),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
syncProjectRootToWorktree(mainBase, wtBase, "M004");
|
|
147
|
+
|
|
148
|
+
const wtCompleted = JSON.parse(
|
|
149
|
+
readFileSync(join(wtBase, ".gsd", "completed-units.json"), "utf-8"),
|
|
150
|
+
);
|
|
151
|
+
assertEq(
|
|
152
|
+
wtCompleted,
|
|
153
|
+
["validate-milestone/M004"],
|
|
154
|
+
"#1886: completed-units.json synced from project root (force:true)",
|
|
155
|
+
);
|
|
156
|
+
} finally {
|
|
157
|
+
cleanup(mainBase);
|
|
158
|
+
cleanup(wtBase);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── 4. completed-units.json: no-op when project root has no file ───────
|
|
163
|
+
console.log(
|
|
164
|
+
"\n=== 4. #1886: completed-units.json no-op when missing in project root ===",
|
|
165
|
+
);
|
|
166
|
+
{
|
|
167
|
+
const mainBase = createBase("main");
|
|
168
|
+
const wtBase = createBase("wt");
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// Project root milestone dir must exist for sync to run
|
|
172
|
+
const prM004 = join(mainBase, ".gsd", "milestones", "M004");
|
|
173
|
+
mkdirSync(prM004, { recursive: true });
|
|
174
|
+
|
|
175
|
+
// No completed-units.json in project root
|
|
176
|
+
// Worktree has its own
|
|
177
|
+
writeFileSync(
|
|
178
|
+
join(wtBase, ".gsd", "completed-units.json"),
|
|
179
|
+
JSON.stringify(["some-unit/M001"]),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
syncProjectRootToWorktree(mainBase, wtBase, "M004");
|
|
183
|
+
|
|
184
|
+
const wtCompleted = JSON.parse(
|
|
185
|
+
readFileSync(join(wtBase, ".gsd", "completed-units.json"), "utf-8"),
|
|
186
|
+
);
|
|
187
|
+
assertEq(
|
|
188
|
+
wtCompleted,
|
|
189
|
+
["some-unit/M001"],
|
|
190
|
+
"#1886: worktree completed-units.json untouched when project root has none",
|
|
191
|
+
);
|
|
192
|
+
} finally {
|
|
193
|
+
cleanup(mainBase);
|
|
194
|
+
cleanup(wtBase);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
report();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main().catch((error) => {
|
|
202
|
+
console.error(error);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
});
|