fix(merger): prevent tasks landing in Done with no commit on main

Two root-cause fixes for the "fake done" patterns surfaced while debugging
FN-5475's stuck preflight (it depended on FN-5233, which the board reported
as Done but whose squash had stranded on a sibling fusion/fn-* branch).

1. resolveTaskMergeTarget rejects fusion/fn-* sibling branches as a merge
   destination — when a task's baseBranch was inherited from a sibling/dependent
   dispatch, the merger detached onto and squashed against that branch instead
   of advancing main. New audit event surfaces the steering miss so the
   underlying baseBranch-propagation bug stays observable.

2. self-healing findLandedTaskCommit verifies ownership against each grep
   candidate's body before attribution. The previous code blindly accepted the
   first hit of `git log --grep=FN-XXXX` (which matches the entire commit
   message); FN-5441 and FN-5446 were both marked done against an unrelated
   FN-5483 commit whose body merely mentioned them in prose. commitOwnedByTask
   is also tightened: trailers must be line-anchored and the subject fallback
   must match conventional-commit form, not a bare substring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-23 17:16:45 -07:00
parent ef12df4363
commit 408e20bdc6
6 changed files with 248 additions and 10 deletions

View File

@@ -79,6 +79,55 @@ describe("resolveTaskMergeTarget", () => {
source: "legacy-main",
});
});
// Regression for FN-5233/FN-5530: when a sibling-dispatched task inherits
// `baseBranch = fusion/fn-<id>`, the merger must NOT use that as the squash
// destination — otherwise the commit lands on the sibling branch and is
// lost from main. Falls through to projectDefault, and reports the rejection.
it("rejects task baseBranch when it points at a sibling fusion/fn-* branch", () => {
const result = resolveTaskMergeTarget(
{ baseBranch: "fusion/fn-5339", branchContext: undefined },
{ projectDefaultBranch: "main" },
);
expect(result.branch).toBe("main");
expect(result.source).toBe("project-default");
expect(result.rejected).toEqual({
branch: "fusion/fn-5339",
source: "task-base-branch",
reason: "fusion-sibling-branch",
});
});
it("rejects inherited branch context that points at a sibling fusion/fn-* branch", () => {
const result = resolveTaskMergeTarget(
{
baseBranch: undefined,
branchContext: {
groupId: "G-1",
source: "planning",
assignmentMode: "shared",
inheritedBaseBranch: "FUSION/FN-1234",
},
},
{ projectDefaultBranch: "main" },
);
expect(result.branch).toBe("main");
expect(result.source).toBe("project-default");
expect(result.rejected).toEqual({
branch: "FUSION/FN-1234",
source: "task-branch-context",
reason: "fusion-sibling-branch",
});
});
it("does not reject non-fusion branches that happen to share a prefix", () => {
// `fusion/release-1.0` is a legitimate human-chosen base; only the
// canonical `fusion/fn-<id>` pattern is a sibling-task marker.
expect(resolveTaskMergeTarget({ baseBranch: "fusion/release-1.0", branchContext: undefined })).toEqual({
branch: "fusion/release-1.0",
source: "task-base-branch",
});
});
});
describe("getTaskMergeBlocker", () => {