diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index 037bb516f..4727517dc 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -157,6 +157,25 @@ function clearProjectRootStateFiles(basePath: string, milestoneId: string): void } } +// ─── Build Artifact Auto-Resolve ───────────────────────────────────────────── + +/** Patterns for machine-generated build artifacts that can be safely + * auto-resolved by accepting --theirs during merge. These files are + * regenerable and never contain meaningful manual edits. */ +export const SAFE_AUTO_RESOLVE_PATTERNS: RegExp[] = [ + /\.tsbuildinfo$/, + /\.pyc$/, + /\/__pycache__\//, + /\.DS_Store$/, + /\.map$/, +]; + +/** Returns true if the file path is safe to auto-resolve during merge. + * Covers `.gsd/` state files and common build artifacts. */ +export const isSafeToAutoResolve = (filePath: string): boolean => + filePath.startsWith(".gsd/") || + SAFE_AUTO_RESOLVE_PATTERNS.some((re) => re.test(filePath)); + // ─── Dispatch-Level Sync (project root ↔ worktree) ────────────────────────── /** @@ -1408,30 +1427,30 @@ export function mergeMilestoneToMain( : nativeConflictFiles(originalBasePath_); if (conflictedFiles.length > 0) { - // Separate .gsd/ state file conflicts from real code conflicts. - // GSD state files (STATE.md, auto.lock, etc.) - // diverge between branches during normal operation — always prefer the - // milestone branch version since it has the latest execution state. - const gsdConflicts = conflictedFiles.filter((f) => f.startsWith(".gsd/")); + // Separate auto-resolvable conflicts (GSD state files + build artifacts) + // from real code conflicts. GSD state files diverge between branches + // during normal operation. Build artifacts are machine-generated and + // regenerable. Both are safe to accept from the milestone branch. + const autoResolvable = conflictedFiles.filter(isSafeToAutoResolve); const codeConflicts = conflictedFiles.filter( - (f) => !f.startsWith(".gsd/"), + (f) => !isSafeToAutoResolve(f), ); - // Auto-resolve .gsd/ conflicts by accepting the milestone branch version - if (gsdConflicts.length > 0) { - for (const gsdFile of gsdConflicts) { + // Auto-resolve safe conflicts by accepting the milestone branch version + if (autoResolvable.length > 0) { + for (const safeFile of autoResolvable) { try { - nativeCheckoutTheirs(originalBasePath_, [gsdFile]); - nativeAddPaths(originalBasePath_, [gsdFile]); + nativeCheckoutTheirs(originalBasePath_, [safeFile]); + nativeAddPaths(originalBasePath_, [safeFile]); } catch { // If checkout --theirs fails, try removing the file from the merge // (it's a runtime file that shouldn't be committed anyway) - nativeRmForce(originalBasePath_, [gsdFile]); + nativeRmForce(originalBasePath_, [safeFile]); } } } - // If there are still non-.gsd conflicts, escalate + // If there are still real code conflicts, escalate if (codeConflicts.length > 0) { // Pop stash before throwing so local work is not lost (#2151). if (stashed) { diff --git a/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts b/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts new file mode 100644 index 000000000..5dfaf4812 --- /dev/null +++ b/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts @@ -0,0 +1,80 @@ +/** + * auto-worktree-auto-resolve.test.ts — Unit tests for isSafeToAutoResolve. + * + * Covers: .gsd/ state files, build artifacts (.tsbuildinfo, .pyc, __pycache__, + * .DS_Store, .map), and rejection of real source files. + */ + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; + +import { + isSafeToAutoResolve, + SAFE_AUTO_RESOLVE_PATTERNS, +} from "../auto-worktree.ts"; + +describe("isSafeToAutoResolve", () => { + // ─── .gsd/ state files ─────────────────────────────────────────────────── + test("returns true for .gsd/ prefixed paths", () => { + assert.ok(isSafeToAutoResolve(".gsd/STATE.md")); + assert.ok(isSafeToAutoResolve(".gsd/milestones/M001/CONTEXT.md")); + assert.ok(isSafeToAutoResolve(".gsd/gsd.db")); + }); + + // ─── Build artifact patterns ───────────────────────────────────────────── + test("returns true for .tsbuildinfo files", () => { + assert.ok(isSafeToAutoResolve("tsconfig.tsbuildinfo")); + assert.ok(isSafeToAutoResolve("dist/tsconfig.tsbuildinfo")); + }); + + test("returns true for .pyc files", () => { + assert.ok(isSafeToAutoResolve("module.pyc")); + assert.ok(isSafeToAutoResolve("src/utils/helpers.pyc")); + }); + + test("returns true for __pycache__/ paths", () => { + assert.ok(isSafeToAutoResolve("src/__pycache__/module.cpython-311.pyc")); + assert.ok(isSafeToAutoResolve("lib/__pycache__/foo.py")); + }); + + test("returns true for .DS_Store files", () => { + assert.ok(isSafeToAutoResolve(".DS_Store")); + assert.ok(isSafeToAutoResolve("src/.DS_Store")); + }); + + test("returns true for .map source map files", () => { + assert.ok(isSafeToAutoResolve("dist/index.js.map")); + assert.ok(isSafeToAutoResolve("out/bundle.css.map")); + }); + + // ─── Real source files (should NOT be auto-resolved) ───────────────────── + test("returns false for .ts source files", () => { + assert.ok(!isSafeToAutoResolve("src/index.ts")); + assert.ok(!isSafeToAutoResolve("lib/utils.ts")); + }); + + test("returns false for .js source files", () => { + assert.ok(!isSafeToAutoResolve("src/index.js")); + assert.ok(!isSafeToAutoResolve("lib/helpers.js")); + }); + + test("returns false for .py source files", () => { + assert.ok(!isSafeToAutoResolve("src/main.py")); + assert.ok(!isSafeToAutoResolve("scripts/deploy.py")); + }); + + test("returns false for config and data files", () => { + assert.ok(!isSafeToAutoResolve("package.json")); + assert.ok(!isSafeToAutoResolve("tsconfig.json")); + assert.ok(!isSafeToAutoResolve("README.md")); + }); + + // ─── SAFE_AUTO_RESOLVE_PATTERNS export ──────────────────────────────────── + test("SAFE_AUTO_RESOLVE_PATTERNS is a non-empty array of RegExp", () => { + assert.ok(Array.isArray(SAFE_AUTO_RESOLVE_PATTERNS)); + assert.ok(SAFE_AUTO_RESOLVE_PATTERNS.length > 0); + for (const pattern of SAFE_AUTO_RESOLVE_PATTERNS) { + assert.ok(pattern instanceof RegExp); + } + }); +});