docs(FN-4333): document postMergeAuditMode + add mode=off/warn integration tests
Adopts the documentation and integration test additions from the Fusion executor's parallel work on FN-4333 (sleek-oak worktree). The core hot-fix already landed in ccd718acd. - AGENTS.md: update the post-squash audit paragraph to describe the three modes and the verified-tree short-circuit. - docs/settings-reference.md: add postMergeAuditMode row. - packages/engine/src/__tests__/merger-merge-lifecycle.test.ts: add integration tests covering postMergeAuditMode=off (skips audit) and postMergeAuditMode=warn (continues despite dirty findings). Fusion-Task-Id: FN-4333
This commit is contained in:
@@ -225,7 +225,7 @@ Two rules, learned the hard way (FN-2370 silently reverted three commits' work):
|
||||
|
||||
2. **Prefer rebase-and-merge over squash for branches spanning multiple substantive commits.** Fusion's direct merger now defaults `directMergeCommitStrategy="auto"`, which keeps squash for branches with 0–1 substantive commits but automatically switches multi-substantive branches to a history-preserving rebase/cherry-pick path. Use the project setting `directMergeCommitStrategy` or the task-level `**Direct Merge Commit Strategy:** auto|always-squash|always-rebase` PROMPT line when you need to force a route.
|
||||
|
||||
After any squash that auto-resolved conflicts, the merger now runs the post-squash audit as a blocking gate before auto-completing the task. Flagged merges stay in `in-review` for inspection, and only a clean audit proceeds to `done`.
|
||||
After any squash that auto-resolved conflicts, the merger runs the post-squash audit before auto-completing the task. Outcome depends on `postMergeAuditMode`: `block` refuses completion on findings, `warn` logs findings and continues, and `off` skips the audit. For rebase-strategy merges, overlap-only findings are also auto-cleared when deterministic verification has already proven the merged tree.
|
||||
|
||||
Before those auto-resolved squash commits are written, the merger also runs a per-file diff-volume gate: it compares each file's staged squash delta against the branch's net delta vs its merge-base, and blocks the merge in `in-review` when a non-allowlisted file loses too much branch volume. This is the pre-commit guard against FN-3936-style silent drops where fallback resolution kept a branch's commit message but discarded the branch's main file edits.
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
||||
| `mergeDiffVolumeThreshold` | `number` | `0.2` | Minimum staged-to-branch-net ratio allowed for a non-allowlisted file during auto-resolved squash finalization. Applied at merge time and clamped to `0..1`. |
|
||||
| `mergeDiffVolumeAllowlist` | `string[]` | `[]` | Additional glob patterns skipped by the pre-commit diff-volume gate, beyond the built-in generated-file and lockfile allowlists. |
|
||||
| `mergeStrategyOverlapBehavior` | `"flip-to-prefer-branch" \| "warn-only" \| "ignore"` | `"flip-to-prefer-branch"` | Safety control for `mergeConflictStrategy="smart-prefer-main"`. Before the Attempt 3 `-X ours` fallback, Fusion checks whether the task branch and recent `main` history overlap on the same files (30-commit lookback, matching the squash audit heuristics). `flip-to-prefer-branch` makes overlapping files prefer the task branch so hardening is not silently discarded (the FN-3936 class of regression). `warn-only` logs the overlap but keeps the legacy main-wins fallback. `ignore` disables the overlap guard and preserves legacy behavior exactly. |
|
||||
| `postMergeAuditMode` | `"block" \| "warn" \| "off"` | `"block"` | Controls the post-merge audit gate. **Block** refuses to auto-complete merges that show duplicate-subject or touched-file overlap risks (most conservative). **Warn** logs findings but auto-completes the merge. **Off** skips the audit entirely. Default: Block. Switching to Warn or Off is recommended only if you trust your branches don't silently drop edits. Regardless of mode, rebase-strategy overlap-only findings are auto-cleared when deterministic merge verification has already proven the tree (FN-4333). |
|
||||
|
||||
### Per-task direct-merge override
|
||||
|
||||
|
||||
@@ -2806,6 +2806,69 @@ describe("aiMergeTask post-squash audit gate", () => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-050", { status: null });
|
||||
});
|
||||
|
||||
it("skips the post-merge audit entirely when postMergeAuditMode=off (FN-4333)", async () => {
|
||||
setupAutoResolvedMergeExecSync();
|
||||
mockedAuditSquashMerge.mockResolvedValue({
|
||||
strategy: "squash",
|
||||
squashSha: "mergedcommit123",
|
||||
parentSha: "parent123",
|
||||
auditTargetLabel: "mergedcommit123",
|
||||
squashSubject: "feat: squash merge",
|
||||
lookback: 30,
|
||||
branchSubjects: ["feat: duplicate subject"],
|
||||
recentMainSubjects: ["feat: duplicate subject"],
|
||||
duplicateSubjects: [{ type: "duplicate-subject", subject: "feat: duplicate subject" }],
|
||||
touchedFiles: [],
|
||||
touchedFileOverlaps: [],
|
||||
findings: [{ type: "duplicate-subject", subject: "feat: duplicate subject" }],
|
||||
issueCount: 1,
|
||||
clean: false,
|
||||
});
|
||||
const store = createAuditStore({ postMergeAuditMode: "off" });
|
||||
|
||||
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
expect(result.merged).toBe(true);
|
||||
expect(mockedAuditSquashMerge).not.toHaveBeenCalled();
|
||||
expect(
|
||||
vi.mocked(store.appendAgentLog).mock.calls.some(([, message]) => String(message).includes("audit blocked auto-completion")),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("continues when postMergeAuditMode=warn even with dirty audit findings (FN-4333)", async () => {
|
||||
setupAutoResolvedMergeExecSync();
|
||||
mockedAuditSquashMerge.mockResolvedValue({
|
||||
strategy: "squash",
|
||||
squashSha: "mergedcommit123",
|
||||
parentSha: "parent123",
|
||||
auditTargetLabel: "mergedcommit123",
|
||||
squashSubject: "feat: squash merge",
|
||||
lookback: 30,
|
||||
branchSubjects: ["feat: duplicate subject"],
|
||||
recentMainSubjects: ["feat: duplicate subject"],
|
||||
duplicateSubjects: [{ type: "duplicate-subject", subject: "feat: duplicate subject" }],
|
||||
touchedFiles: [],
|
||||
touchedFileOverlaps: [],
|
||||
findings: [{ type: "duplicate-subject", subject: "feat: duplicate subject" }],
|
||||
issueCount: 1,
|
||||
clean: false,
|
||||
});
|
||||
const store = createAuditStore({ postMergeAuditMode: "warn" });
|
||||
|
||||
const result = await aiMergeTask(store, "/tmp/root", "FN-050");
|
||||
|
||||
expect(result.merged).toBe(true);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-050", "done");
|
||||
expect(
|
||||
vi.mocked(store.appendAgentLog).mock.calls.some(([, message, type]) => type === "tool_error" && String(message).includes("audit blocked auto-completion")),
|
||||
).toBe(false);
|
||||
expect(
|
||||
vi.mocked(store.appendAgentLog).mock.calls.some(
|
||||
([, message]) => String(message).includes("post-squash audit found 1 risk(s) — continuing"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("skips the post-squash audit when the squash merge is empty", async () => {
|
||||
setupEmptySquashMergeExecSync();
|
||||
const store = createAuditStore();
|
||||
|
||||
Reference in New Issue
Block a user