fix(FN-4409): add merge-base fallback for contamination audit

Fusion-Task-Id: FN-4409
Fusion-Task-Lineage: c608c942-fec7-4489-9970-b8dc314f8be2
This commit is contained in:
Fusion
2026-05-13 17:24:44 -07:00
committed by gsxdsm
parent 6301015550
commit 8d9e426353
2 changed files with 38 additions and 17 deletions

View File

@@ -21,8 +21,6 @@ test("flags branch as tainted when foreign task commits are present", () => {
fs.writeFileSync(path.join(dir, "README.md"), "base\n");
run(dir, "git", ["add", "README.md"]);
run(dir, "git", ["commit", "-m", "chore: base"]);
const base = run(dir, "git", ["rev-parse", "HEAD"]);
run(dir, "git", ["checkout", "-b", "fusion/fn-0001"]);
fs.writeFileSync(path.join(dir, "feature.txt"), "foreign\n");
run(dir, "git", ["add", "feature.txt"]);
@@ -32,7 +30,7 @@ test("flags branch as tainted when foreign task commits are present", () => {
fs.mkdirSync(fusionDir, { recursive: true });
const dbPath = path.join(fusionDir, "fusion.db");
run(dir, "sqlite3", [dbPath, `CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT, branch TEXT, baseCommitSha TEXT, "column" TEXT);`]);
run(dir, "sqlite3", [dbPath, `INSERT INTO tasks (id, title, branch, baseCommitSha, "column") VALUES ('FN-0001', 'Task 1', 'fusion/fn-0001', '${base}', 'in-progress');`]);
run(dir, "sqlite3", [dbPath, `INSERT INTO tasks (id, title, branch, baseCommitSha, "column") VALUES ('FN-0001', 'Task 1', 'fusion/fn-0001', NULL, 'in-progress');`]);
const report = auditBranchCrossContamination({ projectRoot: dir });
assert.equal(report.taintedTaskCount, 1);
@@ -40,6 +38,7 @@ test("flags branch as tainted when foreign task commits are present", () => {
assert.ok(task);
assert.equal(task.tainted, true);
assert.equal(task.recommendation, "force-reset");
assert.equal(task.baseResolutionSource.startsWith("merge-base("), true);
assert.equal(task.taintedCommits[0].foreignTaskId, "FN-0002");
} finally {
fs.rmSync(dir, { recursive: true, force: true });

View File

@@ -60,6 +60,25 @@ function branchExists(projectRoot, branchName) {
return runGit(projectRoot, ["rev-parse", "--verify", `refs/heads/${branchName}`], { allowFailure: true }) !== null;
}
function resolveMainRef(projectRoot) {
if (runGit(projectRoot, ["rev-parse", "--verify", "origin/main"], { allowFailure: true })) {
return "origin/main";
}
return "main";
}
function resolveBaseCommit(projectRoot, branchName, taskBaseCommitSha) {
if (taskBaseCommitSha && String(taskBaseCommitSha).trim()) {
return { baseCommitSha: String(taskBaseCommitSha).trim(), source: "task.baseCommitSha" };
}
const mainRef = resolveMainRef(projectRoot);
const fallback = runGit(projectRoot, ["merge-base", mainRef, branchName], { allowFailure: true });
if (fallback) {
return { baseCommitSha: fallback.trim(), source: `merge-base(${mainRef},${branchName})` };
}
return { baseCommitSha: null, source: "unresolved" };
}
function parseCommits(raw) {
if (!raw) return [];
const lines = raw.split("\n").map((line) => line.trim()).filter(Boolean);
@@ -97,20 +116,8 @@ export function auditBranchCrossContamination({ projectRoot = process.cwd() } =
for (const task of taskRows) {
const taskId = String(task.id).toUpperCase();
const branchName = expectedBranch(taskId, task.branch);
const baseCommitSha = task.baseCommitSha ? String(task.baseCommitSha).trim() : null;
if (!baseCommitSha) {
report.tasks.push({
taskId,
title: task.title,
branchName,
baseCommitSha: null,
column: task.columnName,
skipped: true,
reason: "missing-baseCommitSha",
});
continue;
}
const baseResolution = resolveBaseCommit(projectRoot, branchName, task.baseCommitSha);
const baseCommitSha = baseResolution.baseCommitSha;
if (!branchExists(projectRoot, branchName)) {
report.missingBranchCount += 1;
@@ -127,6 +134,20 @@ export function auditBranchCrossContamination({ projectRoot = process.cwd() } =
continue;
}
if (!baseCommitSha) {
report.tasks.push({
taskId,
title: task.title,
branchName,
baseCommitSha: null,
baseResolutionSource: baseResolution.source,
column: task.columnName,
skipped: true,
reason: "missing-baseCommitSha",
});
continue;
}
const rawLog = runGit(projectRoot, ["log", `${baseCommitSha}..${branchName}`, "--format=%H%x1f%s%x1f%b"]);
const commits = parseCommits(rawLog);
const taintedCommits = commits.filter((commit) => commit.attributedTaskId && commit.attributedTaskId !== taskId);
@@ -140,6 +161,7 @@ export function auditBranchCrossContamination({ projectRoot = process.cwd() } =
title: task.title,
branchName,
baseCommitSha,
baseResolutionSource: baseResolution.source,
column: task.columnName,
totalCommits: commits.length,
taskAttributedCommitCount: ownCommits.length,