Merge pull request #3682 from Tibsfox/fix/sync-worktree-skip-current-milestone
fix(gsd): skip current milestone in syncWorktreeStateBack to prevent merge conflicts
This commit is contained in:
commit
764d8ff466
4 changed files with 97 additions and 26 deletions
|
|
@ -770,6 +770,9 @@ export function syncWorktreeStateBack(
|
|||
.map((d) => d.name);
|
||||
|
||||
for (const mid of wtMilestones) {
|
||||
// Skip the current milestone being merged — its files are already in the
|
||||
// milestone branch and would conflict with the squash merge (#3641).
|
||||
if (mid === milestoneId) continue;
|
||||
syncMilestoneDir(wtGsd, mainGsd, mid, synced);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* Regression test for #3641 — syncWorktreeStateBack skips current milestone
|
||||
*
|
||||
* When syncing worktree state back to main, the current milestone being
|
||||
* merged should be skipped. Its files are already in the milestone branch
|
||||
* and copying them back would conflict with the squash merge.
|
||||
*
|
||||
* The fix adds a `mid === milestoneId` skip guard inside the milestone
|
||||
* iteration loop in syncWorktreeStateBack.
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const src = readFileSync(
|
||||
resolve(process.cwd(), 'src', 'resources', 'extensions', 'gsd', 'auto-worktree.ts'),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
describe('syncWorktreeStateBack skips current milestone (#3641)', () => {
|
||||
it('syncWorktreeStateBack function exists', () => {
|
||||
assert.ok(
|
||||
src.includes('function syncWorktreeStateBack('),
|
||||
'syncWorktreeStateBack function must be defined',
|
||||
)
|
||||
})
|
||||
|
||||
it('mid === milestoneId skip guard exists in the milestone loop', () => {
|
||||
// Find syncWorktreeStateBack
|
||||
const fnStart = src.indexOf('function syncWorktreeStateBack(')
|
||||
assert.ok(fnStart !== -1)
|
||||
|
||||
// Get a reasonable portion of the function
|
||||
const fnBlock = src.slice(fnStart, fnStart + 3000)
|
||||
|
||||
// Find the for loop iterating milestones
|
||||
const loopIdx = fnBlock.indexOf('for (const mid of wtMilestones)')
|
||||
assert.ok(loopIdx !== -1, 'milestone iteration loop must exist')
|
||||
|
||||
// After the loop, there should be the skip guard
|
||||
const loopBody = fnBlock.slice(loopIdx, loopIdx + 300)
|
||||
assert.ok(
|
||||
loopBody.includes('mid === milestoneId'),
|
||||
'mid === milestoneId skip guard must exist inside the milestone loop',
|
||||
)
|
||||
assert.ok(
|
||||
loopBody.includes('continue'),
|
||||
'skip guard must use continue to skip the current milestone',
|
||||
)
|
||||
})
|
||||
|
||||
it('syncMilestoneDir is still called for non-current milestones', () => {
|
||||
const fnStart = src.indexOf('function syncWorktreeStateBack(')
|
||||
assert.ok(fnStart !== -1)
|
||||
|
||||
const fnBlock = src.slice(fnStart, fnStart + 3000)
|
||||
|
||||
assert.ok(
|
||||
fnBlock.includes('syncMilestoneDir('),
|
||||
'syncMilestoneDir must still be called for other milestones',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -221,7 +221,8 @@ describe('worktree-sync-milestones', async () => {
|
|||
|
||||
try {
|
||||
// Build worktree milestone structure with slice-level and task-level files
|
||||
const wtSliceDir = join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S01');
|
||||
// Use M002 as the milestone to sync, M001 as the "current" being merged (skipped)
|
||||
const wtSliceDir = join(wtBase, '.gsd', 'milestones', 'M002', 'slices', 'S01');
|
||||
const wtTasksDir = join(wtSliceDir, 'tasks');
|
||||
mkdirSync(wtTasksDir, { recursive: true });
|
||||
writeFileSync(join(wtSliceDir, 'S01-SUMMARY.md'), '# S01 Summary');
|
||||
|
|
@ -229,11 +230,12 @@ describe('worktree-sync-milestones', async () => {
|
|||
writeFileSync(join(wtTasksDir, 'T02-SUMMARY.md'), '# T02 Summary');
|
||||
|
||||
// Main project root starts with only the milestone directory (no slices yet)
|
||||
mkdirSync(join(mainBase, '.gsd', 'milestones', 'M001'), { recursive: true });
|
||||
mkdirSync(join(mainBase, '.gsd', 'milestones', 'M002'), { recursive: true });
|
||||
|
||||
// Pass M001 as milestoneId (the one being merged/skipped), M002 should still sync
|
||||
const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M001');
|
||||
|
||||
const mainSliceDir = join(mainBase, '.gsd', 'milestones', 'M001', 'slices', 'S01');
|
||||
const mainSliceDir = join(mainBase, '.gsd', 'milestones', 'M002', 'slices', 'S01');
|
||||
const mainTasksDir = join(mainSliceDir, 'tasks');
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -341,16 +343,16 @@ describe('worktree-sync-milestones', async () => {
|
|||
'M002 missing in main before sync',
|
||||
);
|
||||
|
||||
// Sync with milestoneId = M001 (the current milestone)
|
||||
// Sync with milestoneId = M001 (the current milestone being merged — skipped)
|
||||
const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M001');
|
||||
|
||||
// M001 should be synced (current milestone — always synced)
|
||||
// M001 should be SKIPPED (current milestone being merged — #3641)
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, '.gsd', 'milestones', 'M001', 'M001-SUMMARY.md')),
|
||||
'M001 SUMMARY synced to main',
|
||||
!existsSync(join(mainBase, '.gsd', 'milestones', 'M001', 'M001-SUMMARY.md')),
|
||||
'M001 SUMMARY NOT synced (current milestone skipped to prevent merge conflicts)',
|
||||
);
|
||||
|
||||
// M002 should ALSO be synced (next milestone — the fix)
|
||||
// M002 should be synced (other milestone — not skipped)
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, '.gsd', 'milestones', 'M002-abc123', 'M002-abc123-CONTEXT.md')),
|
||||
'M002 CONTEXT synced to main (next-milestone fix)',
|
||||
|
|
@ -407,20 +409,17 @@ describe('worktree-sync-milestones', async () => {
|
|||
writeFileSync(join(wtBase, '.gsd', 'REQUIREMENTS.md'), '# Requirements\n## R001-R089\n## R090 — SCIM\n## R091 — WebAuthn');
|
||||
writeFileSync(join(wtBase, '.gsd', 'PROJECT.md'), '# Project\nMilestones: M001-M007');
|
||||
|
||||
// Sync with milestoneId = M006 (the completing milestone)
|
||||
// Sync with milestoneId = M006 (the completing milestone — skipped by sync)
|
||||
const { synced } = syncWorktreeStateBack(mainBase, wtBase, 'M006-589wvh');
|
||||
|
||||
// Verify M006 artifacts synced
|
||||
// M006 is the current milestone being merged — it should be SKIPPED (#3641)
|
||||
// Its files are already in the milestone branch and would conflict with squash merge.
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, '.gsd', 'milestones', 'M006-589wvh', 'M006-589wvh-SUMMARY.md')),
|
||||
'M006 SUMMARY synced',
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, '.gsd', 'milestones', 'M006-589wvh', 'slices', 'S01', 'S01-SUMMARY.md')),
|
||||
'M006 S01 SUMMARY synced',
|
||||
!existsSync(join(mainBase, '.gsd', 'milestones', 'M006-589wvh', 'M006-589wvh-SUMMARY.md')),
|
||||
'M006 SUMMARY NOT synced (current milestone skipped)',
|
||||
);
|
||||
|
||||
// Verify M007 artifacts synced (the critical fix)
|
||||
// Verify M007 artifacts synced (the critical fix — other milestones still sync)
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, '.gsd', 'milestones', 'M007-wortc8', 'M007-wortc8-CONTEXT.md')),
|
||||
'M007 CONTEXT synced to main (next-milestone)',
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ function writeFile(dir: string, relativePath: string, content: string): void {
|
|||
test("syncWorktreeStateBack copies task summaries from tasks/ subdirectory (#1678)", () => {
|
||||
const mainBase = makeTempDir("main");
|
||||
const wtBase = makeTempDir("wt");
|
||||
const mid = "M001";
|
||||
const currentMid = "M000"; // milestone being merged (skipped by sync)
|
||||
const mid = "M001"; // other milestone that should be synced
|
||||
|
||||
try {
|
||||
// Set up worktree with milestone, slice, and task files
|
||||
|
|
@ -64,8 +65,8 @@ test("syncWorktreeStateBack copies task summaries from tasks/ subdirectory (#167
|
|||
// Set up main with empty .gsd
|
||||
mkdirSync(join(mainBase, ".gsd"), { recursive: true });
|
||||
|
||||
// Run sync
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, mid);
|
||||
// Run sync — currentMid is skipped, mid (M001) should be synced
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, currentMid);
|
||||
|
||||
// Verify milestone-level files synced
|
||||
assert.ok(
|
||||
|
|
@ -126,7 +127,8 @@ test("syncWorktreeStateBack copies task summaries from tasks/ subdirectory (#167
|
|||
test("syncWorktreeStateBack handles multiple slices with tasks (#1678)", () => {
|
||||
const mainBase = makeTempDir("main");
|
||||
const wtBase = makeTempDir("wt");
|
||||
const mid = "M002";
|
||||
const currentMid = "M000"; // milestone being merged (skipped)
|
||||
const mid = "M002"; // other milestone that should be synced
|
||||
|
||||
try {
|
||||
// Set up two slices with tasks
|
||||
|
|
@ -139,7 +141,7 @@ test("syncWorktreeStateBack handles multiple slices with tasks (#1678)", () => {
|
|||
|
||||
mkdirSync(join(mainBase, ".gsd"), { recursive: true });
|
||||
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, mid);
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, currentMid);
|
||||
|
||||
// All task summaries from both slices should be synced
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`)));
|
||||
|
|
@ -160,7 +162,8 @@ test("syncWorktreeStateBack handles multiple slices with tasks (#1678)", () => {
|
|||
test("syncWorktreeStateBack handles slices without tasks/ directory", () => {
|
||||
const mainBase = makeTempDir("main");
|
||||
const wtBase = makeTempDir("wt");
|
||||
const mid = "M003";
|
||||
const currentMid = "M000"; // milestone being merged (skipped)
|
||||
const mid = "M003"; // other milestone that should be synced
|
||||
|
||||
try {
|
||||
// Slice with no tasks/ subdirectory (legitimate case: pre-planning)
|
||||
|
|
@ -168,7 +171,7 @@ test("syncWorktreeStateBack handles slices without tasks/ directory", () => {
|
|||
|
||||
mkdirSync(join(mainBase, ".gsd"), { recursive: true });
|
||||
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, mid);
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, currentMid);
|
||||
|
||||
// Should sync the slice file without errors
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/S01-RESEARCH.md`)));
|
||||
|
|
@ -183,7 +186,8 @@ test("syncWorktreeStateBack handles slices without tasks/ directory", () => {
|
|||
test("syncWorktreeStateBack ignores non-md files in tasks/", () => {
|
||||
const mainBase = makeTempDir("main");
|
||||
const wtBase = makeTempDir("wt");
|
||||
const mid = "M004";
|
||||
const currentMid = "M000"; // milestone being merged (skipped)
|
||||
const mid = "M004"; // other milestone that should be synced
|
||||
|
||||
try {
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/S01-PLAN.md`, "# Plan\n");
|
||||
|
|
@ -194,7 +198,7 @@ test("syncWorktreeStateBack ignores non-md files in tasks/", () => {
|
|||
|
||||
mkdirSync(join(mainBase, ".gsd"), { recursive: true });
|
||||
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, mid);
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, currentMid);
|
||||
|
||||
// Only .md files should be synced
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`)));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue