fix(FN-5627): default auto-prerebase to fire when branch >=1 commit behind
decideAutoPrerebase() previously defaulted prerebaseDivergenceThreshold to 0, which meant the threshold path NEVER fired unless the user explicitly set a positive value. Only hot-file matches could trigger prerebase. The result: tasks whose branch was started against an older main tip (because other tasks landed concurrently) skipped prerebase, built their squash commit against the stale base, and then failed at git update-ref because the squash didn't descend from current main. The merger correctly detected this as non-fast-forward and threw IntegrationBranchConcurrentAdvanceError, but with both 'expected' and 'observed' SHAs set to current main tip \u2014 because observedCurrentSha was captured from the pre-update rev-parse, not post-failure. This produced the misleading 'expected X, observed X' same-SHA error signature that stranded FN-5632 stuck at mergeRetries=3 after the FN-5627 merger fix and engine restart. New default: prerebaseDivergenceThreshold = 1. Any branch behind by at least 1 commit auto-rebases before squash. Users who want the legacy never-fire behavior can explicitly set prerebaseDivergenceThreshold = 0. Threshold comparison also changed from > to >= so an explicit threshold of N rebases at N+ commits behind instead of N+1+. The self-healing classifier comment for spurious-concurrent-advance-same-sha is updated to note the signature can come from either pre-FN-5627 misclassification OR the legitimate post-FN-5627 non-FF path; the auto-recovery sweep is unchanged because both cases self-heal cleanly once prerebase fires on the retry. Tests (3 new): - Default threshold (undefined) fires at 1 commit behind - Explicit threshold = 0 stays as opt-out (never fire on commit-count) - Default threshold doesn't fire when branch is up-to-date Engine suite: 6160 tests pass. In-flight: FN-5632 manually SQL-reset to mergeRetries=0 / status=null once more so the next merger tick (after engine restart picks up this code) auto-prerebases and lands the work. Future occurrences self-recover. Fusion-Task-Id: FN-5627
This commit is contained in:
@@ -49,6 +49,63 @@ describe("decideAutoPrerebase", () => {
|
||||
expect(decision.fire).toBe(false);
|
||||
expect(decision.reason).toBe("no-divergence");
|
||||
});
|
||||
|
||||
it("FN-5627: fires on default threshold (1 commit) when prerebaseDivergenceThreshold is undefined", () => {
|
||||
// Pre-FN-5627 default behavior: undefined threshold meant 'never fire on
|
||||
// commit-count'. This left tasks that branched off older main tips with
|
||||
// no recourse — the merger would build the squash against the stale
|
||||
// base, update-ref would refuse non-FF, and IntegrationBranchConcurrentAdvanceError
|
||||
// surfaced with a misleading same-SHA pair (the exact signature that
|
||||
// stranded FN-5632). New default: threshold=1, fire on any divergence.
|
||||
const settingsNoThreshold = {
|
||||
prerebaseAutoEnabled: true,
|
||||
prerebaseHotFiles: ["AGENTS.md"],
|
||||
// prerebaseDivergenceThreshold intentionally undefined
|
||||
};
|
||||
const decision = decideAutoPrerebase({
|
||||
settings: settingsNoThreshold,
|
||||
baseCommitSha: "abc",
|
||||
commitsBehind: 1,
|
||||
changedFiles: ["x.ts"],
|
||||
worktrunkEnabled: false,
|
||||
});
|
||||
expect(decision.fire).toBe(true);
|
||||
expect(decision.reason).toBe("divergence-threshold");
|
||||
expect(decision.commitsBehind).toBe(1);
|
||||
});
|
||||
|
||||
it("FN-5627: respects explicit prerebaseDivergenceThreshold = 0 as opt-out (never fire on commit-count)", () => {
|
||||
const settingsOptOut = {
|
||||
prerebaseAutoEnabled: true,
|
||||
prerebaseHotFiles: ["AGENTS.md"],
|
||||
prerebaseDivergenceThreshold: 0,
|
||||
};
|
||||
const decision = decideAutoPrerebase({
|
||||
settings: settingsOptOut,
|
||||
baseCommitSha: "abc",
|
||||
commitsBehind: 100,
|
||||
changedFiles: ["x.ts"],
|
||||
worktrunkEnabled: false,
|
||||
});
|
||||
expect(decision.fire).toBe(false);
|
||||
expect(decision.reason).toBe("no-divergence");
|
||||
});
|
||||
|
||||
it("FN-5627: default threshold doesn't fire when branch is up-to-date", () => {
|
||||
const settingsNoThreshold = {
|
||||
prerebaseAutoEnabled: true,
|
||||
prerebaseHotFiles: ["AGENTS.md"],
|
||||
};
|
||||
const decision = decideAutoPrerebase({
|
||||
settings: settingsNoThreshold,
|
||||
baseCommitSha: "abc",
|
||||
commitsBehind: 0,
|
||||
changedFiles: [],
|
||||
worktrunkEnabled: false,
|
||||
});
|
||||
expect(decision.fire).toBe(false);
|
||||
expect(decision.reason).toBe("no-divergence");
|
||||
});
|
||||
});
|
||||
|
||||
describeIfGit("merger-auto-prerebase git integration", () => {
|
||||
|
||||
@@ -71,8 +71,16 @@ export function decideAutoPrerebase(input: {
|
||||
return { fire: true, reason: "hot-file", commitsBehind, hotMatches };
|
||||
}
|
||||
|
||||
const threshold = input.settings.prerebaseDivergenceThreshold ?? 0;
|
||||
if (threshold > 0 && commitsBehind > threshold) {
|
||||
// FN-5627 follow-up: default divergence threshold flipped from 0 ("never
|
||||
// fire on commit-count") to 1 ("fire when branch is behind by at least 1
|
||||
// commit"). The legacy default left tasks that branched off an older main
|
||||
// tip with no recourse — the squash would build against the stale base,
|
||||
// `git update-ref` would correctly refuse the non-fast-forward advance,
|
||||
// and the merger would surface `IntegrationBranchConcurrentAdvanceError`
|
||||
// with a misleading same-SHA pair. Users who want the legacy never-fire
|
||||
// behavior can explicitly set `prerebaseDivergenceThreshold = 0`.
|
||||
const threshold = input.settings.prerebaseDivergenceThreshold ?? 1;
|
||||
if (threshold > 0 && commitsBehind >= threshold) {
|
||||
return { fire: true, reason: "divergence-threshold", commitsBehind, hotMatches: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -331,12 +331,20 @@ export const MAX_TRANSIENT_MERGE_RECOVERIES = 2;
|
||||
* self-healing sweeps that clean stale `mergeQueue` rows (FN-5353/FN-5363).
|
||||
* - `spurious-concurrent-advance-same-sha`: the merger reported
|
||||
* `Integration branch X advanced concurrently (expected SHA, observed SHA)`
|
||||
* with identical SHA on both sides — the integration ref didn't actually
|
||||
* move. Pre-FN-5627 misclassification in `merger-ref-update-advance.ts`
|
||||
* routed real ref-update-refusal failures (lock contention, hook rejection)
|
||||
* through `IntegrationBranchConcurrentAdvanceError`. The current code
|
||||
* classifies these as `ref-update-refused`, so this class is here to
|
||||
* recover legacy stuck rows from before the fix landed.
|
||||
* with identical SHA on both sides. This signature shows up in two cases:
|
||||
* (1) Pre-FN-5627 misclassification in `merger-ref-update-advance.ts`
|
||||
* routed real ref-update-refusal failures (lock contention, hook
|
||||
* rejection) through `IntegrationBranchConcurrentAdvanceError`.
|
||||
* (2) Post-FN-5627: the merger's `advanceIntegrationBranchRef` correctly
|
||||
* detects `non-fast-forward-advance` when the freshly built squash
|
||||
* commit does not descend from the current integration ref (typically
|
||||
* because the task branch was started against an older main tip and
|
||||
* auto-prerebase was skipped). The error carries the same SHA in both
|
||||
* the "expected" and "observed" slots because the pre-advance rev-parse
|
||||
* captured the ref state and update-ref refused without moving it. On
|
||||
* the next merge attempt, the lifted auto-prerebase default (threshold
|
||||
* = 1 commit, FN-5627 follow-up to `merger-auto-prerebase.ts`) rebases
|
||||
* the task branch onto current main, so the retry succeeds.
|
||||
*/
|
||||
export function classifyTransientMergeError(error: string | null | undefined): string | null {
|
||||
if (!error) return null;
|
||||
|
||||
Reference in New Issue
Block a user