From f96a26b3c92cf36d9064f70540dacd07772a70b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Thu, 26 Mar 2026 20:03:37 -0600 Subject: [PATCH] fix: auto-resolve build artifact conflicts in milestone merge (#2777) * fix: auto-resolve build artifact conflicts in milestone merge The binary conflict classification in mergeMilestoneToMain only auto-resolved .gsd/ prefixed files. Machine-generated build artifacts like .tsbuildinfo, .pyc, __pycache__/, .DS_Store, and .map files were treated as real code conflicts, blocking auto-merge unnecessarily. Extract an isSafeToAutoResolve helper that checks both the .gsd/ prefix and a SAFE_AUTO_RESOLVE_PATTERNS regex list. Matched files are resolved with --theirs, same as .gsd/ state files. Closes #2761 Co-Authored-By: Claude Opus 4.6 (1M context) * test: add tests for build artifact auto-resolve patterns Extract isSafeToAutoResolve and SAFE_AUTO_RESOLVE_PATTERNS to module-level exports for testability. Add unit tests covering .gsd/ state files, build artifacts (.tsbuildinfo, .pyc, __pycache__, .DS_Store, .map), and rejection of real source files (.ts, .js, .py, .json, .md). Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto-worktree.ts | 45 ++++++++--- .../tests/auto-worktree-auto-resolve.test.ts | 80 +++++++++++++++++++ 2 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts 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); + } + }); +});