Fixes #1678
This commit is contained in:
parent
57b92dee43
commit
24af556942
2 changed files with 215 additions and 9 deletions
|
|
@ -302,19 +302,19 @@ export function syncWorktreeStateBack(
|
|||
/* non-fatal */
|
||||
}
|
||||
} else if (fileEntry.isDirectory() && fileEntry.name === "tasks") {
|
||||
// Recurse into tasks/ to sync task-level summaries (#1678)
|
||||
// Recurse into tasks/ subdirectory to sync task summaries (#1678).
|
||||
// Without this, T01-SUMMARY.md etc. are silently dropped on
|
||||
// worktree teardown because the loop only processes isFile() entries.
|
||||
const wtTasksDir = join(wtSliceDir, "tasks");
|
||||
const mainTasksDir = join(mainSliceDir, "tasks");
|
||||
mkdirSync(mainTasksDir, { recursive: true });
|
||||
try {
|
||||
mkdirSync(mainTasksDir, { recursive: true });
|
||||
for (const taskEntry of readdirSync(wtTasksDir, {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
for (const taskEntry of readdirSync(wtTasksDir, { withFileTypes: true })) {
|
||||
if (taskEntry.isFile() && taskEntry.name.endsWith(".md")) {
|
||||
const src = join(wtTasksDir, taskEntry.name);
|
||||
const dst = join(mainTasksDir, taskEntry.name);
|
||||
const taskSrc = join(wtTasksDir, taskEntry.name);
|
||||
const taskDst = join(mainTasksDir, taskEntry.name);
|
||||
try {
|
||||
cpSync(src, dst, { force: true });
|
||||
cpSync(taskSrc, taskDst, { force: true });
|
||||
synced.push(
|
||||
`milestones/${milestoneId}/slices/${sid}/tasks/${taskEntry.name}`,
|
||||
);
|
||||
|
|
@ -324,7 +324,7 @@ export function syncWorktreeStateBack(
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
/* non-fatal: tasks dir read failure */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
206
src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts
Normal file
206
src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
* worktree-sync-tasks.test.ts — Regression test for #1678.
|
||||
*
|
||||
* Verifies that syncWorktreeStateBack() correctly syncs task summaries
|
||||
* from the tasks/ subdirectory within each slice, not just slice-level files.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import { syncWorktreeStateBack } from "../auto-worktree.ts";
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function makeTempDir(prefix: string): string {
|
||||
return mkdtempSync(join(tmpdir(), `gsd-sync-test-${prefix}-`));
|
||||
}
|
||||
|
||||
function cleanup(...dirs: string[]): void {
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeFile(dir: string, relativePath: string, content: string): void {
|
||||
const fullPath = join(dir, relativePath);
|
||||
mkdirSync(join(fullPath, ".."), { recursive: true });
|
||||
writeFileSync(fullPath, content, "utf-8");
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
test("syncWorktreeStateBack copies task summaries from tasks/ subdirectory (#1678)", () => {
|
||||
const mainBase = makeTempDir("main");
|
||||
const wtBase = makeTempDir("wt");
|
||||
const mid = "M001";
|
||||
|
||||
try {
|
||||
// Set up worktree with milestone, slice, and task files
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/${mid}-ROADMAP.md`, "# Roadmap\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/${mid}-SUMMARY.md`, "# Summary\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/S01-PLAN.md`, "# Plan\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/S01-SUMMARY.md`, "# Slice Summary\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/S01-UAT.md`, "# UAT\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-PLAN.md`, "# Task 1 Plan\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`, "# Task 1 Summary\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/tasks/T02-PLAN.md`, "# Task 2 Plan\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/tasks/T02-SUMMARY.md`, "# Task 2 Summary\n");
|
||||
|
||||
// Set up main with empty .gsd
|
||||
mkdirSync(join(mainBase, ".gsd"), { recursive: true });
|
||||
|
||||
// Run sync
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, mid);
|
||||
|
||||
// Verify milestone-level files synced
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, `.gsd/milestones/${mid}/${mid}-ROADMAP.md`)),
|
||||
"ROADMAP should be synced",
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, `.gsd/milestones/${mid}/${mid}-SUMMARY.md`)),
|
||||
"SUMMARY should be synced",
|
||||
);
|
||||
|
||||
// Verify slice-level files synced
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/S01-PLAN.md`)),
|
||||
"S01-PLAN should be synced",
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/S01-SUMMARY.md`)),
|
||||
"S01-SUMMARY should be synced",
|
||||
);
|
||||
|
||||
// Verify task-level files synced (THE BUG FIX)
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-PLAN.md`)),
|
||||
"T01-PLAN should be synced (was dropped before fix)",
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`)),
|
||||
"T01-SUMMARY should be synced (was dropped before fix)",
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T02-PLAN.md`)),
|
||||
"T02-PLAN should be synced (was dropped before fix)",
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T02-SUMMARY.md`)),
|
||||
"T02-SUMMARY should be synced (was dropped before fix)",
|
||||
);
|
||||
|
||||
// Verify task files appear in synced list
|
||||
const taskSynced = result.synced.filter(p => p.includes("/tasks/"));
|
||||
assert.ok(
|
||||
taskSynced.length >= 4,
|
||||
`Expected at least 4 task files in synced list, got ${taskSynced.length}: ${taskSynced.join(", ")}`,
|
||||
);
|
||||
|
||||
// Verify content integrity
|
||||
const t1Summary = readFileSync(
|
||||
join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`),
|
||||
"utf-8",
|
||||
);
|
||||
assert.equal(t1Summary, "# Task 1 Summary\n");
|
||||
} finally {
|
||||
cleanup(mainBase, wtBase);
|
||||
}
|
||||
});
|
||||
|
||||
test("syncWorktreeStateBack handles multiple slices with tasks (#1678)", () => {
|
||||
const mainBase = makeTempDir("main");
|
||||
const wtBase = makeTempDir("wt");
|
||||
const mid = "M002";
|
||||
|
||||
try {
|
||||
// Set up two slices with tasks
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/S01-SUMMARY.md`, "# S01\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`, "# S01-T01\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S02/S02-SUMMARY.md`, "# S02\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S02/tasks/T01-SUMMARY.md`, "# S02-T01\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S02/tasks/T02-SUMMARY.md`, "# S02-T02\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S02/tasks/T03-SUMMARY.md`, "# S02-T03\n");
|
||||
|
||||
mkdirSync(join(mainBase, ".gsd"), { recursive: true });
|
||||
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, mid);
|
||||
|
||||
// All task summaries from both slices should be synced
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`)));
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S02/tasks/T01-SUMMARY.md`)));
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S02/tasks/T02-SUMMARY.md`)));
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S02/tasks/T03-SUMMARY.md`)));
|
||||
|
||||
// Verify content integrity across slices
|
||||
assert.equal(
|
||||
readFileSync(join(mainBase, `.gsd/milestones/${mid}/slices/S02/tasks/T03-SUMMARY.md`), "utf-8"),
|
||||
"# S02-T03\n",
|
||||
);
|
||||
} finally {
|
||||
cleanup(mainBase, wtBase);
|
||||
}
|
||||
});
|
||||
|
||||
test("syncWorktreeStateBack handles slices without tasks/ directory", () => {
|
||||
const mainBase = makeTempDir("main");
|
||||
const wtBase = makeTempDir("wt");
|
||||
const mid = "M003";
|
||||
|
||||
try {
|
||||
// Slice with no tasks/ subdirectory (legitimate case: pre-planning)
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/S01-RESEARCH.md`, "# Research\n");
|
||||
|
||||
mkdirSync(join(mainBase, ".gsd"), { recursive: true });
|
||||
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, mid);
|
||||
|
||||
// Should sync the slice file without errors
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/S01-RESEARCH.md`)));
|
||||
// Should not have any task entries
|
||||
const taskSynced = result.synced.filter(p => p.includes("/tasks/"));
|
||||
assert.equal(taskSynced.length, 0);
|
||||
} finally {
|
||||
cleanup(mainBase, wtBase);
|
||||
}
|
||||
});
|
||||
|
||||
test("syncWorktreeStateBack ignores non-md files in tasks/", () => {
|
||||
const mainBase = makeTempDir("main");
|
||||
const wtBase = makeTempDir("wt");
|
||||
const mid = "M004";
|
||||
|
||||
try {
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/S01-PLAN.md`, "# Plan\n");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`, "# T01\n");
|
||||
// Non-md file should be ignored
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/tasks/.DS_Store`, "junk");
|
||||
writeFile(wtBase, `.gsd/milestones/${mid}/slices/S01/tasks/notes.txt`, "notes");
|
||||
|
||||
mkdirSync(join(mainBase, ".gsd"), { recursive: true });
|
||||
|
||||
const result = syncWorktreeStateBack(mainBase, wtBase, mid);
|
||||
|
||||
// Only .md files should be synced
|
||||
assert.ok(existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/T01-SUMMARY.md`)));
|
||||
assert.ok(!existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/.DS_Store`)));
|
||||
assert.ok(!existsSync(join(mainBase, `.gsd/milestones/${mid}/slices/S01/tasks/notes.txt`)));
|
||||
} finally {
|
||||
cleanup(mainBase, wtBase);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue