gsd-pi 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.js +1 -0
- package/dist/loader.js +50 -6
- package/dist/resource-loader.d.ts +7 -6
- package/dist/resource-loader.js +15 -8
- package/dist/resources/extensions/gsd/auto-worktree.ts +29 -183
- package/dist/resources/extensions/gsd/auto.ts +252 -370
- package/dist/resources/extensions/gsd/commands.ts +118 -34
- package/dist/resources/extensions/gsd/doctor.ts +29 -4
- package/dist/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/dist/resources/extensions/gsd/git-service.ts +8 -431
- package/dist/resources/extensions/gsd/gitignore.ts +11 -4
- package/dist/resources/extensions/gsd/guided-flow.ts +141 -5
- package/dist/resources/extensions/gsd/preferences.ts +18 -17
- package/dist/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +7 -1
- package/dist/resources/extensions/gsd/state.ts +26 -8
- package/dist/resources/extensions/gsd/templates/state.md +0 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
- package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
- package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/dist/resources/extensions/gsd/types.ts +0 -1
- package/dist/resources/extensions/gsd/worktree-manager.ts +7 -3
- package/dist/resources/extensions/gsd/worktree.ts +7 -65
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google.js +12 -4
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +10 -2
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/src/providers/google.ts +20 -8
- package/packages/pi-ai/src/providers/mistral.ts +14 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.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 +12 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
- package/packages/pi-tui/dist/components/input.d.ts +1 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +10 -0
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/src/components/input.ts +11 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +29 -183
- package/src/resources/extensions/gsd/auto.ts +252 -370
- package/src/resources/extensions/gsd/commands.ts +118 -34
- package/src/resources/extensions/gsd/doctor.ts +29 -4
- package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/src/resources/extensions/gsd/git-service.ts +8 -431
- package/src/resources/extensions/gsd/gitignore.ts +11 -4
- package/src/resources/extensions/gsd/guided-flow.ts +141 -5
- package/src/resources/extensions/gsd/preferences.ts +18 -17
- package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/src/resources/extensions/gsd/prompts/queue.md +7 -1
- package/src/resources/extensions/gsd/state.ts +26 -8
- package/src/resources/extensions/gsd/templates/state.md +0 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
- package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +7 -3
- package/src/resources/extensions/gsd/worktree.ts +7 -65
- package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
|
@@ -280,119 +280,6 @@ function cleanup(base: string): void {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
// ═══ verifyExpectedArtifact: fix-merge ════════════════════════════════════════
|
|
284
|
-
|
|
285
|
-
/** Create a real git repo for fix-merge tests */
|
|
286
|
-
function createGitBase(): string {
|
|
287
|
-
const base = mkdtempSync(join(tmpdir(), "gsd-fixmerge-test-"));
|
|
288
|
-
execSync("git init -b main", { cwd: base, stdio: "ignore" });
|
|
289
|
-
execSync("git config user.email test@test.com", { cwd: base, stdio: "ignore" });
|
|
290
|
-
execSync("git config user.name Test", { cwd: base, stdio: "ignore" });
|
|
291
|
-
writeFileSync(join(base, "README.md"), "init\n", "utf-8");
|
|
292
|
-
execSync("git add -A && git commit -m init", { cwd: base, stdio: "ignore" });
|
|
293
|
-
// Create .gsd structure for the fixture
|
|
294
|
-
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
|
|
295
|
-
return base;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
{
|
|
299
|
-
console.log("\n=== verifyExpectedArtifact: fix-merge — clean repo returns true ===");
|
|
300
|
-
const base = createGitBase();
|
|
301
|
-
try {
|
|
302
|
-
const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
|
|
303
|
-
assertTrue(result === true, "clean repo should verify as true");
|
|
304
|
-
} finally {
|
|
305
|
-
cleanup(base);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
{
|
|
310
|
-
console.log("\n=== verifyExpectedArtifact: fix-merge — MERGE_HEAD present returns false ===");
|
|
311
|
-
const base = createGitBase();
|
|
312
|
-
try {
|
|
313
|
-
writeFileSync(join(base, ".git", "MERGE_HEAD"), "abc123\n", "utf-8");
|
|
314
|
-
const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
|
|
315
|
-
assertTrue(result === false, "MERGE_HEAD present should return false");
|
|
316
|
-
} finally {
|
|
317
|
-
cleanup(base);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
{
|
|
322
|
-
console.log("\n=== verifyExpectedArtifact: fix-merge — SQUASH_MSG present returns false ===");
|
|
323
|
-
const base = createGitBase();
|
|
324
|
-
try {
|
|
325
|
-
writeFileSync(join(base, ".git", "SQUASH_MSG"), "squash msg\n", "utf-8");
|
|
326
|
-
const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
|
|
327
|
-
assertTrue(result === false, "SQUASH_MSG present should return false");
|
|
328
|
-
} finally {
|
|
329
|
-
cleanup(base);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
{
|
|
334
|
-
console.log("\n=== verifyExpectedArtifact: fix-merge — real UU conflict returns false ===");
|
|
335
|
-
const base = createGitBase();
|
|
336
|
-
try {
|
|
337
|
-
// Create a conflict: modify same file on two branches
|
|
338
|
-
writeFileSync(join(base, "conflict.txt"), "main content\n", "utf-8");
|
|
339
|
-
execSync('git add -A && git commit -m "main change"', { cwd: base, stdio: "ignore" });
|
|
340
|
-
execSync("git checkout -b feature", { cwd: base, stdio: "ignore" });
|
|
341
|
-
writeFileSync(join(base, "conflict.txt"), "feature content\n", "utf-8");
|
|
342
|
-
execSync('git add -A && git commit -m "feature change"', { cwd: base, stdio: "ignore" });
|
|
343
|
-
execSync("git checkout main", { cwd: base, stdio: "ignore" });
|
|
344
|
-
writeFileSync(join(base, "conflict.txt"), "different main content\n", "utf-8");
|
|
345
|
-
execSync('git add -A && git commit -m "diverge"', { cwd: base, stdio: "ignore" });
|
|
346
|
-
try { execSync("git merge feature", { cwd: base, stdio: "ignore" }); } catch { /* expected conflict */ }
|
|
347
|
-
const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
|
|
348
|
-
assertTrue(result === false, "UU conflict should return false");
|
|
349
|
-
} finally {
|
|
350
|
-
execSync("git reset --hard HEAD", { cwd: base, stdio: "ignore" });
|
|
351
|
-
cleanup(base);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
{
|
|
356
|
-
console.log("\n=== verifyExpectedArtifact: fix-merge — real DU conflict returns false ===");
|
|
357
|
-
const base = createGitBase();
|
|
358
|
-
try {
|
|
359
|
-
writeFileSync(join(base, "deleted.txt"), "content\n", "utf-8");
|
|
360
|
-
execSync('git add -A && git commit -m "add file"', { cwd: base, stdio: "ignore" });
|
|
361
|
-
execSync("git checkout -b feature2", { cwd: base, stdio: "ignore" });
|
|
362
|
-
writeFileSync(join(base, "deleted.txt"), "modified on feature\n", "utf-8");
|
|
363
|
-
execSync('git add -A && git commit -m "modify on feature"', { cwd: base, stdio: "ignore" });
|
|
364
|
-
execSync("git checkout main", { cwd: base, stdio: "ignore" });
|
|
365
|
-
execSync("git rm deleted.txt", { cwd: base, stdio: "ignore" });
|
|
366
|
-
execSync('git commit -m "delete on main"', { cwd: base, stdio: "ignore" });
|
|
367
|
-
try { execSync("git merge feature2", { cwd: base, stdio: "ignore" }); } catch { /* expected conflict */ }
|
|
368
|
-
const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
|
|
369
|
-
assertTrue(result === false, "DU conflict should return false");
|
|
370
|
-
} finally {
|
|
371
|
-
execSync("git reset --hard HEAD", { cwd: base, stdio: "ignore" });
|
|
372
|
-
cleanup(base);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
{
|
|
377
|
-
console.log("\n=== verifyExpectedArtifact: fix-merge — real AA conflict returns false ===");
|
|
378
|
-
const base = createGitBase();
|
|
379
|
-
try {
|
|
380
|
-
execSync("git checkout -b branch-a", { cwd: base, stdio: "ignore" });
|
|
381
|
-
writeFileSync(join(base, "both.txt"), "branch-a content\n", "utf-8");
|
|
382
|
-
execSync('git add -A && git commit -m "add on branch-a"', { cwd: base, stdio: "ignore" });
|
|
383
|
-
execSync("git checkout main", { cwd: base, stdio: "ignore" });
|
|
384
|
-
execSync("git checkout -b branch-b", { cwd: base, stdio: "ignore" });
|
|
385
|
-
writeFileSync(join(base, "both.txt"), "branch-b content\n", "utf-8");
|
|
386
|
-
execSync('git add -A && git commit -m "add on branch-b"', { cwd: base, stdio: "ignore" });
|
|
387
|
-
try { execSync("git merge branch-a", { cwd: base, stdio: "ignore" }); } catch { /* expected conflict */ }
|
|
388
|
-
const result = verifyExpectedArtifact("fix-merge", "M001/S01", base);
|
|
389
|
-
assertTrue(result === false, "AA conflict should return false");
|
|
390
|
-
} finally {
|
|
391
|
-
execSync("git reset --hard HEAD", { cwd: base, stdio: "ignore" });
|
|
392
|
-
cleanup(base);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
283
|
// ═══ verifyExpectedArtifact: complete-slice roadmap check ════════════════════
|
|
397
284
|
// Regression for #indefinite-hang: complete-slice must verify roadmap [x] or
|
|
398
285
|
// the idempotency skip loops forever after a crash that wrote SUMMARY+UAT but
|
|
@@ -574,4 +461,25 @@ const ROADMAP_COMPLETE = `# M001: Test Milestone
|
|
|
574
461
|
}
|
|
575
462
|
}
|
|
576
463
|
|
|
464
|
+
// ═══ verifyExpectedArtifact: hook unit types ═════════════════════════════════
|
|
465
|
+
|
|
466
|
+
console.log("\n=== verifyExpectedArtifact: hook types always return true ===");
|
|
467
|
+
|
|
468
|
+
{
|
|
469
|
+
const base = createFixtureBase();
|
|
470
|
+
try {
|
|
471
|
+
// Hook units don't have standard artifacts — they should always pass
|
|
472
|
+
const result1 = verifyExpectedArtifact("hook/code-review", "M001/S01/T01", base);
|
|
473
|
+
assertTrue(result1, "hook/code-review should always return true");
|
|
474
|
+
|
|
475
|
+
const result2 = verifyExpectedArtifact("hook/simplify", "M001/S01/T02", base);
|
|
476
|
+
assertTrue(result2, "hook/simplify should always return true");
|
|
477
|
+
|
|
478
|
+
const result3 = verifyExpectedArtifact("hook/custom-hook", "M001/S01", base);
|
|
479
|
+
assertTrue(result3, "hook/custom-hook at slice level should return true");
|
|
480
|
+
} finally {
|
|
481
|
+
rmSync(base, { recursive: true, force: true });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
577
485
|
report();
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Uses real filesystem and git fixtures — no mocking.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { mkdtempSync, mkdirSync,
|
|
9
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
@@ -16,12 +16,8 @@ import { indexWorkspace } from '../workspace-index.ts';
|
|
|
16
16
|
import { inlinePriorMilestoneSummary } from '../files.ts';
|
|
17
17
|
import { getPriorSliceCompletionBlocker } from '../dispatch-guard.ts';
|
|
18
18
|
import {
|
|
19
|
-
ensureSliceBranch,
|
|
20
|
-
getCurrentBranch,
|
|
21
19
|
getSliceBranchName,
|
|
22
|
-
mergeSliceToMain,
|
|
23
20
|
parseSliceBranch,
|
|
24
|
-
switchToMain,
|
|
25
21
|
} from '../worktree.ts';
|
|
26
22
|
import { clearPathCache } from '../paths.ts';
|
|
27
23
|
import { createTestContext } from './test-helpers.ts';
|
|
@@ -481,84 +477,22 @@ Built the legacy feature successfully.
|
|
|
481
477
|
}
|
|
482
478
|
}
|
|
483
479
|
|
|
484
|
-
// ─── Group 6: Branch
|
|
485
|
-
console.log('\n=== Group 6: Branch
|
|
480
|
+
// ─── Group 6: Branch name helpers with new-format IDs ───────────────
|
|
481
|
+
console.log('\n=== Group 6: Branch name helpers with new-format IDs ===');
|
|
486
482
|
{
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
**Goal:** Test
|
|
501
|
-
**Demo:** Branch works
|
|
502
|
-
|
|
503
|
-
## Tasks
|
|
504
|
-
- [ ] **T01: Build** \`est:10m\`
|
|
505
|
-
Build it.
|
|
506
|
-
`);
|
|
507
|
-
writeFileSync(join(base, 'README.md'), 'initial\n');
|
|
508
|
-
run('git add .', base);
|
|
509
|
-
run('git commit -m init', base);
|
|
510
|
-
|
|
511
|
-
// Test getSliceBranchName with new-format ID
|
|
512
|
-
assertEq(
|
|
513
|
-
getSliceBranchName('M001-abc123', 'S01'),
|
|
514
|
-
'gsd/M001-abc123/S01',
|
|
515
|
-
'G6: getSliceBranchName returns gsd/M001-abc123/S01',
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
// Test parseSliceBranch with new-format branch name
|
|
519
|
-
const parsed = parseSliceBranch('gsd/M001-abc123/S01');
|
|
520
|
-
assertTrue(parsed !== null, 'G6: parseSliceBranch returns non-null for new-format');
|
|
521
|
-
assertEq(parsed?.milestoneId, 'M001-abc123', 'G6: parsed milestoneId is M001-abc123');
|
|
522
|
-
assertEq(parsed?.sliceId, 'S01', 'G6: parsed sliceId is S01');
|
|
523
|
-
assertEq(parsed?.worktreeName, null, 'G6: parsed worktreeName is null (no worktree)');
|
|
524
|
-
|
|
525
|
-
// Test ensureSliceBranch creates the branch
|
|
526
|
-
const created = ensureSliceBranch(base, 'M001-abc123', 'S01');
|
|
527
|
-
assertTrue(created, 'G6: ensureSliceBranch returns true (branch created)');
|
|
528
|
-
assertEq(
|
|
529
|
-
getCurrentBranch(base),
|
|
530
|
-
'gsd/M001-abc123/S01',
|
|
531
|
-
'G6: getCurrentBranch returns gsd/M001-abc123/S01',
|
|
532
|
-
);
|
|
533
|
-
|
|
534
|
-
// Idempotent: second ensure should not create
|
|
535
|
-
const secondCreate = ensureSliceBranch(base, 'M001-abc123', 'S01');
|
|
536
|
-
assertEq(secondCreate, false, 'G6: second ensureSliceBranch returns false');
|
|
537
|
-
|
|
538
|
-
// Make a change on the slice branch, commit, then merge to main
|
|
539
|
-
writeFileSync(join(base, 'feature.txt'), 'new feature from slice\n');
|
|
540
|
-
run('git add feature.txt', base);
|
|
541
|
-
run('git commit -m "feat: slice work"', base);
|
|
542
|
-
|
|
543
|
-
// Switch to main and merge
|
|
544
|
-
switchToMain(base);
|
|
545
|
-
assertEq(getCurrentBranch(base), 'main', 'G6: back on main after switchToMain');
|
|
546
|
-
|
|
547
|
-
const merge = mergeSliceToMain(base, 'M001-abc123', 'S01', 'Slice One');
|
|
548
|
-
assertEq(merge.branch, 'gsd/M001-abc123/S01', 'G6: merge reports correct branch');
|
|
549
|
-
assertEq(getCurrentBranch(base), 'main', 'G6: still on main after merge');
|
|
550
|
-
assertTrue(merge.deletedBranch, 'G6: merge deleted the slice branch');
|
|
551
|
-
|
|
552
|
-
// Verify the merged content exists on main
|
|
553
|
-
const content = readFileSync(join(base, 'feature.txt'), 'utf-8');
|
|
554
|
-
assertTrue(content.includes('new feature from slice'), 'G6: merged content on main');
|
|
555
|
-
|
|
556
|
-
// Verify branch is gone
|
|
557
|
-
const branches = run('git branch', base);
|
|
558
|
-
assertTrue(!branches.includes('gsd/M001-abc123/S01'), 'G6: slice branch deleted after merge');
|
|
559
|
-
} finally {
|
|
560
|
-
cleanup(base);
|
|
561
|
-
}
|
|
483
|
+
// Test getSliceBranchName with new-format ID
|
|
484
|
+
assertEq(
|
|
485
|
+
getSliceBranchName('M001-abc123', 'S01'),
|
|
486
|
+
'gsd/M001-abc123/S01',
|
|
487
|
+
'G6: getSliceBranchName returns gsd/M001-abc123/S01',
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// Test parseSliceBranch with new-format branch name
|
|
491
|
+
const parsed = parseSliceBranch('gsd/M001-abc123/S01');
|
|
492
|
+
assertTrue(parsed !== null, 'G6: parseSliceBranch returns non-null for new-format');
|
|
493
|
+
assertEq(parsed?.milestoneId, 'M001-abc123', 'G6: parsed milestoneId is M001-abc123');
|
|
494
|
+
assertEq(parsed?.sliceId, 'S01', 'G6: parsed sliceId is S01');
|
|
495
|
+
assertEq(parsed?.worktreeName, null, 'G6: parsed worktreeName is null (no worktree)');
|
|
562
496
|
}
|
|
563
497
|
|
|
564
498
|
// ─── Summary ──────────────────────────────────────────────────────────
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* preferences-git.test.ts — Validates git.isolation and
|
|
2
|
+
* preferences-git.test.ts — Validates that deprecated git.isolation and
|
|
3
|
+
* git.merge_to_main preference fields produce deprecation warnings.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { createTestContext } from "./test-helpers.ts";
|
|
@@ -8,78 +9,56 @@ import { validatePreferences } from "../preferences.ts";
|
|
|
8
9
|
const { assertEq, assertTrue, report } = createTestContext();
|
|
9
10
|
|
|
10
11
|
async function main(): Promise<void> {
|
|
11
|
-
console.log("\n=== git.isolation
|
|
12
|
+
console.log("\n=== git.isolation deprecated ===");
|
|
12
13
|
|
|
13
|
-
//
|
|
14
|
+
// Any value produces a deprecation warning
|
|
14
15
|
{
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const { warnings } = validatePreferences({ git: { isolation: "worktree" } });
|
|
17
|
+
assertTrue(warnings.length > 0, "isolation: worktree — produces deprecation warning");
|
|
18
|
+
assertTrue(warnings[0].includes("deprecated"), "isolation: worktree — warning mentions deprecated");
|
|
18
19
|
}
|
|
19
20
|
{
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const { warnings } = validatePreferences({ git: { isolation: "branch" } });
|
|
22
|
+
assertTrue(warnings.length > 0, "isolation: branch — produces deprecation warning");
|
|
23
|
+
assertTrue(warnings[0].includes("deprecated"), "isolation: branch — warning mentions deprecated");
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
+
// Undefined passes through without warning
|
|
26
27
|
{
|
|
27
|
-
const {
|
|
28
|
-
|
|
29
|
-
assertTrue(errors[0].includes("isolation"), "isolation: invalid — error mentions isolation");
|
|
30
|
-
}
|
|
31
|
-
{
|
|
32
|
-
const { errors } = validatePreferences({ git: { isolation: 42 } });
|
|
33
|
-
assertTrue(errors.length > 0, "isolation: number — produces error");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Undefined passes through
|
|
37
|
-
{
|
|
38
|
-
const { preferences, errors } = validatePreferences({ git: { auto_push: true } });
|
|
39
|
-
assertEq(errors.length, 0, "isolation: undefined — no errors");
|
|
28
|
+
const { preferences, warnings } = validatePreferences({ git: { auto_push: true } });
|
|
29
|
+
assertEq(warnings.length, 0, "isolation: undefined — no warnings");
|
|
40
30
|
assertEq(preferences.git?.isolation, undefined, "isolation: undefined — not set");
|
|
41
31
|
}
|
|
42
32
|
|
|
43
|
-
console.log("\n=== git.merge_to_main
|
|
44
|
-
|
|
45
|
-
// Valid values
|
|
46
|
-
{
|
|
47
|
-
const { preferences, errors } = validatePreferences({ git: { merge_to_main: "milestone" } });
|
|
48
|
-
assertEq(errors.length, 0, "merge_to_main: milestone — no errors");
|
|
49
|
-
assertEq(preferences.git?.merge_to_main, "milestone", "merge_to_main: milestone — value preserved");
|
|
50
|
-
}
|
|
51
|
-
{
|
|
52
|
-
const { preferences, errors } = validatePreferences({ git: { merge_to_main: "slice" } });
|
|
53
|
-
assertEq(errors.length, 0, "merge_to_main: slice — no errors");
|
|
54
|
-
assertEq(preferences.git?.merge_to_main, "slice", "merge_to_main: slice — value preserved");
|
|
55
|
-
}
|
|
33
|
+
console.log("\n=== git.merge_to_main deprecated ===");
|
|
56
34
|
|
|
57
|
-
//
|
|
35
|
+
// Any value produces a deprecation warning
|
|
58
36
|
{
|
|
59
|
-
const {
|
|
60
|
-
assertTrue(
|
|
61
|
-
assertTrue(
|
|
37
|
+
const { warnings } = validatePreferences({ git: { merge_to_main: "milestone" } });
|
|
38
|
+
assertTrue(warnings.length > 0, "merge_to_main: milestone — produces deprecation warning");
|
|
39
|
+
assertTrue(warnings[0].includes("deprecated"), "merge_to_main: milestone — warning mentions deprecated");
|
|
62
40
|
}
|
|
63
41
|
{
|
|
64
|
-
const {
|
|
65
|
-
assertTrue(
|
|
42
|
+
const { warnings } = validatePreferences({ git: { merge_to_main: "slice" } });
|
|
43
|
+
assertTrue(warnings.length > 0, "merge_to_main: slice — produces deprecation warning");
|
|
44
|
+
assertTrue(warnings[0].includes("deprecated"), "merge_to_main: slice — warning mentions deprecated");
|
|
66
45
|
}
|
|
67
46
|
|
|
68
|
-
// Undefined passes through
|
|
47
|
+
// Undefined passes through without warning
|
|
69
48
|
{
|
|
70
|
-
const { preferences,
|
|
71
|
-
assertEq(
|
|
49
|
+
const { preferences, warnings } = validatePreferences({ git: { auto_push: true } });
|
|
50
|
+
assertEq(warnings.length, 0, "merge_to_main: undefined — no warnings");
|
|
72
51
|
assertEq(preferences.git?.merge_to_main, undefined, "merge_to_main: undefined — not set");
|
|
73
52
|
}
|
|
74
53
|
|
|
75
|
-
console.log("\n=== both fields together ===");
|
|
54
|
+
console.log("\n=== both deprecated fields together ===");
|
|
76
55
|
{
|
|
77
|
-
const {
|
|
56
|
+
const { warnings } = validatePreferences({
|
|
78
57
|
git: { isolation: "worktree", merge_to_main: "slice" },
|
|
79
58
|
});
|
|
80
|
-
assertEq(
|
|
81
|
-
|
|
82
|
-
|
|
59
|
+
assertEq(warnings.length, 2, "both deprecated fields — 2 warnings");
|
|
60
|
+
assertTrue(warnings.some(w => w.includes("isolation")), "one warning mentions isolation");
|
|
61
|
+
assertTrue(warnings.some(w => w.includes("merge_to_main")), "one warning mentions merge_to_main");
|
|
83
62
|
}
|
|
84
63
|
|
|
85
64
|
report();
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* worktree-e2e.test.ts -- End-to-end tests for worktree-isolated git flow.
|
|
3
3
|
*
|
|
4
|
-
* Covers
|
|
4
|
+
* Covers cross-cutting groups not tested by individual slice tests:
|
|
5
5
|
* 1. Full lifecycle chain (create -> slice commits -> merge to milestone -> merge to main)
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4. Self-heal in merge context (abortAndReset, withMergeHeal)
|
|
9
|
-
* 5. Doctor detection of orphaned worktrees
|
|
6
|
+
* 2. Self-heal: abortAndReset cleans up failed merges
|
|
7
|
+
* 3. Doctor detection of orphaned worktrees
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import {
|
|
13
11
|
mkdtempSync, mkdirSync, writeFileSync, rmSync,
|
|
14
|
-
existsSync, realpathSync,
|
|
12
|
+
existsSync, realpathSync,
|
|
15
13
|
} from "node:fs";
|
|
16
14
|
import { join } from "node:path";
|
|
17
15
|
import { tmpdir } from "node:os";
|
|
@@ -20,11 +18,9 @@ import { execSync } from "node:child_process";
|
|
|
20
18
|
import {
|
|
21
19
|
createAutoWorktree,
|
|
22
20
|
mergeMilestoneToMain,
|
|
23
|
-
mergeSliceToMilestone,
|
|
24
|
-
shouldUseWorktreeIsolation,
|
|
25
21
|
} from "../auto-worktree.ts";
|
|
26
22
|
import { getSliceBranchName } from "../worktree.ts";
|
|
27
|
-
import { abortAndReset
|
|
23
|
+
import { abortAndReset } from "../git-self-heal.ts";
|
|
28
24
|
import { runGSDDoctor } from "../doctor.ts";
|
|
29
25
|
import { createTestContext } from "./test-helpers.ts";
|
|
30
26
|
|
|
@@ -60,11 +56,11 @@ function makeRoadmap(
|
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
function addSliceToMilestone(
|
|
63
|
-
|
|
59
|
+
_repo: string,
|
|
64
60
|
wtPath: string,
|
|
65
61
|
milestoneId: string,
|
|
66
62
|
sliceId: string,
|
|
67
|
-
|
|
63
|
+
_sliceTitle: string,
|
|
68
64
|
commits: Array<{ file: string; content: string; message: string }>,
|
|
69
65
|
): void {
|
|
70
66
|
const normalizedPath = wtPath.replaceAll("\\", "/");
|
|
@@ -81,7 +77,7 @@ function addSliceToMilestone(
|
|
|
81
77
|
run(`git commit -m "${c.message}"`, wtPath);
|
|
82
78
|
}
|
|
83
79
|
run(`git checkout milestone/${milestoneId}`, wtPath);
|
|
84
|
-
|
|
80
|
+
run(`git merge --no-ff ${sliceBranch} -m "merge ${sliceId}"`, wtPath);
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
async function main(): Promise<void> {
|
|
@@ -144,59 +140,10 @@ async function main(): Promise<void> {
|
|
|
144
140
|
}
|
|
145
141
|
|
|
146
142
|
// ================================================================
|
|
147
|
-
// Group 2:
|
|
148
|
-
// ================================================================
|
|
149
|
-
console.log("\n=== Preference gating ===");
|
|
150
|
-
{
|
|
151
|
-
const repo = createTempRepo();
|
|
152
|
-
tempDirs.push(repo);
|
|
153
|
-
|
|
154
|
-
// Override to branch mode
|
|
155
|
-
const branchResult = shouldUseWorktreeIsolation(repo, { isolation: "branch" });
|
|
156
|
-
assertEq(branchResult, false, "isolation=branch returns false");
|
|
157
|
-
|
|
158
|
-
// Override to worktree mode
|
|
159
|
-
const wtResult = shouldUseWorktreeIsolation(repo, { isolation: "worktree" });
|
|
160
|
-
assertEq(wtResult, true, "isolation=worktree returns true");
|
|
161
|
-
|
|
162
|
-
// Default (no legacy branches) returns true
|
|
163
|
-
const defaultResult = shouldUseWorktreeIsolation(repo);
|
|
164
|
-
assertEq(defaultResult, true, "new project defaults to worktree (true)");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ================================================================
|
|
168
|
-
// Group 3: merge_to_main mode resolution
|
|
169
|
-
// ================================================================
|
|
170
|
-
console.log("\n=== merge_to_main mode ===");
|
|
171
|
-
{
|
|
172
|
-
// getMergeToMainMode reads from loadEffectiveGSDPreferences — test via legacy branch detection
|
|
173
|
-
// Instead, test that the function returns the default "milestone" when no prefs set
|
|
174
|
-
// (Cannot inject overridePrefs — function signature doesn't accept them)
|
|
175
|
-
// We verify the shouldUseWorktreeIsolation override path handles legacy detection
|
|
176
|
-
const repo = createTempRepo();
|
|
177
|
-
tempDirs.push(repo);
|
|
178
|
-
|
|
179
|
-
// Create a legacy gsd/*/* branch to test legacy detection
|
|
180
|
-
run("git checkout -b gsd/M001/S01", repo);
|
|
181
|
-
writeFileSync(join(repo, "legacy.txt"), "legacy\n");
|
|
182
|
-
run("git add .", repo);
|
|
183
|
-
run("git commit -m legacy", repo);
|
|
184
|
-
run("git checkout main", repo);
|
|
185
|
-
|
|
186
|
-
const legacyResult = shouldUseWorktreeIsolation(repo);
|
|
187
|
-
assertEq(legacyResult, false, "legacy gsd branches detected -> branch mode");
|
|
188
|
-
|
|
189
|
-
// Explicit worktree override wins over legacy detection
|
|
190
|
-
const overrideResult = shouldUseWorktreeIsolation(repo, { isolation: "worktree" });
|
|
191
|
-
assertEq(overrideResult, true, "explicit worktree override wins over legacy");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ================================================================
|
|
195
|
-
// Group 4: Self-heal (abortAndReset, withMergeHeal)
|
|
143
|
+
// Group 2: Self-heal (abortAndReset)
|
|
196
144
|
// ================================================================
|
|
197
145
|
console.log("\n=== Self-heal ===");
|
|
198
146
|
{
|
|
199
|
-
// 4a: abortAndReset cleans up MERGE_HEAD
|
|
200
147
|
const repo = createTempRepo();
|
|
201
148
|
tempDirs.push(repo);
|
|
202
149
|
|
|
@@ -218,37 +165,13 @@ async function main(): Promise<void> {
|
|
|
218
165
|
assertTrue(!existsSync(join(repo, ".git", "MERGE_HEAD")), "MERGE_HEAD removed after abort");
|
|
219
166
|
assertTrue(abortResult.cleaned.length > 0, "abortAndReset reports cleaned items");
|
|
220
167
|
}
|
|
221
|
-
{
|
|
222
|
-
// 4b: withMergeHeal throws MergeConflictError for real conflicts
|
|
223
|
-
const repo = createTempRepo();
|
|
224
|
-
tempDirs.push(repo);
|
|
225
|
-
|
|
226
|
-
run("git checkout -b conflict-branch", repo);
|
|
227
|
-
writeFileSync(join(repo, "file.txt"), "branch version\n");
|
|
228
|
-
run("git add .", repo);
|
|
229
|
-
run("git commit -m branch-ver", repo);
|
|
230
|
-
run("git checkout main", repo);
|
|
231
|
-
writeFileSync(join(repo, "file.txt"), "main version\n");
|
|
232
|
-
run("git add .", repo);
|
|
233
|
-
run("git commit -m main-ver", repo);
|
|
234
|
-
|
|
235
|
-
let caughtError: unknown = null;
|
|
236
|
-
try {
|
|
237
|
-
withMergeHeal(repo, () => {
|
|
238
|
-
execSync("git merge conflict-branch", { cwd: repo, stdio: "pipe" });
|
|
239
|
-
});
|
|
240
|
-
} catch (e) {
|
|
241
|
-
caughtError = e;
|
|
242
|
-
}
|
|
243
|
-
assertTrue(caughtError instanceof MergeConflictError, "withMergeHeal throws MergeConflictError");
|
|
244
|
-
if (caughtError instanceof MergeConflictError) {
|
|
245
|
-
assertTrue(caughtError.conflictedFiles.length > 0, "MergeConflictError has conflictedFiles");
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
168
|
|
|
249
169
|
// ================================================================
|
|
250
|
-
// Group
|
|
170
|
+
// Group 3: Doctor detects orphaned worktrees
|
|
171
|
+
// Skip on Windows: git worktree path resolution in temp dirs uses
|
|
172
|
+
// UNC/8.3 forms that don't match after normalization.
|
|
251
173
|
// ================================================================
|
|
174
|
+
if (process.platform !== "win32") {
|
|
252
175
|
console.log("\n=== Doctor: orphaned worktree detection ===");
|
|
253
176
|
{
|
|
254
177
|
// Build a repo with a completed milestone
|
|
@@ -279,7 +202,7 @@ Test
|
|
|
279
202
|
_None_
|
|
280
203
|
`);
|
|
281
204
|
run("git add -A", repo);
|
|
282
|
-
run("git commit -m
|
|
205
|
+
run("git commit -m \"add milestone\"", repo);
|
|
283
206
|
|
|
284
207
|
// Create orphaned worktree
|
|
285
208
|
mkdirSync(join(repo, ".gsd", "worktrees"), { recursive: true });
|
|
@@ -302,6 +225,9 @@ _None_
|
|
|
302
225
|
const wtList = run("git worktree list", repo);
|
|
303
226
|
assertTrue(!wtList.includes("milestone/M001"), "worktree gone after doctor fix");
|
|
304
227
|
}
|
|
228
|
+
} else {
|
|
229
|
+
console.log("\n=== Doctor: orphaned worktree detection (skipped on Windows) ===");
|
|
230
|
+
}
|
|
305
231
|
} finally {
|
|
306
232
|
process.chdir(savedCwd);
|
|
307
233
|
for (const d of tempDirs) {
|