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:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user