test(FN-5147): add autoMerge-off sweep guards coverage

Fusion-Task-Id: FN-5147
Fusion-Task-Lineage: 2a0e5bd4-b2ea-498c-95ef-85126819e824
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 08:15:35 -07:00
committed by gsxdsm
parent 44ea2a7075
commit a249490ef9
2 changed files with 83 additions and 15 deletions

View File

@@ -1871,6 +1871,10 @@ describe("SelfHealingManager", () => {
});
describe("recoverMissingWorktreeReviewFailures", () => {
beforeEach(() => {
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ autoMerge: true, globalPause: false, enginePaused: false });
});
it("does not hard-code unusable-worktree assertion literals in self-healing", async () => {
const source = await readFile(new URL("../self-healing.ts", import.meta.url), "utf8");
expect(source).not.toMatch(/Refusing to start coding agent/);
@@ -2225,6 +2229,10 @@ describe("SelfHealingManager", () => {
});
describe("recoverPartialProgressNoTaskDoneFailures", () => {
beforeEach(() => {
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ autoMerge: true, globalPause: false, enginePaused: false });
});
it("requeues partial-progress no-task_done failures with bounded retry count", async () => {
const managerWithRecovery = new SelfHealingManager(store, {
rootDir: "/tmp/test-project",
@@ -2364,6 +2372,10 @@ describe("SelfHealingManager", () => {
});
describe("recoverMergedReviewTasks", () => {
beforeEach(() => {
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ autoMerge: true, globalPause: false, enginePaused: false, taskStuckTimeoutMs: 1_000 });
});
it("finalizes stale merging tasks when a task commit already landed", async () => {
const managerWithRecovery = new SelfHealingManager(store, {
rootDir: "/tmp/test-project",
@@ -7579,3 +7591,56 @@ describe("SelfHealingManager no-commits-expected audit", () => {
expect(store.moveTask).not.toHaveBeenCalled();
});
});
describe("autoMerge gating for mutating in-review sweeps (FN-5147)", () => {
let store: TaskStore & EventEmitter;
let manager: SelfHealingManager;
beforeEach(() => {
store = createMockStore();
manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
autoMerge: false,
globalPause: false,
enginePaused: false,
taskStuckTimeoutMs: 1_000,
maxPostReviewFixes: 1,
});
});
afterEach(() => {
manager.stop();
});
it.each([
"recoverReviewTasksWithFailedPreMergeSteps",
"recoverStaleIncompleteReviewTasks",
"recoverGhostReviewTasks",
"recoverInterruptedMergingTasks",
"recoverMergedReviewTasks",
"recoverStuckMergeDeadlocks",
"recoverOrphanOnlyScopeViolations",
"recoverAlreadyMergedReviewTasks",
"recoverMissingWorktreeReviewFailures",
"recoverPartialProgressNoTaskDoneFailures",
] as const)("skips entirely when autoMerge is disabled (respects PR-based review flow): %s", async (methodName) => {
if (methodName === "recoverReviewTasksWithFailedPreMergeSteps") {
manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project", recoverFailedPreMergeStep: vi.fn() });
}
const result = await (manager as any)[methodName]();
expect(result).toBe(0);
expect(store.listTasks).not.toHaveBeenCalled();
expect(store.moveTask).not.toHaveBeenCalled();
expect(store.updateTask).not.toHaveBeenCalled();
expect(store.logEntry).not.toHaveBeenCalled();
});
it("skips entirely when autoMerge is disabled (respects PR-based review flow): recoverCompletionHandoffLimbo", async () => {
const result = await manager.recoverCompletionHandoffLimbo();
expect(result).toBeUndefined();
expect(store.listTasks).not.toHaveBeenCalled();
expect(store.moveTask).not.toHaveBeenCalled();
expect(store.updateTask).not.toHaveBeenCalled();
expect(store.logEntry).not.toHaveBeenCalled();
});
});

View File

@@ -3427,11 +3427,14 @@ export class SelfHealingManager {
}
}
/**
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
*/
async finalizeNoOpReviewTasks(): Promise<number> {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
const candidates = tasks.filter((t) =>
@@ -3685,7 +3688,7 @@ export class SelfHealingManager {
// "pull-request"`) — see GitHub issue #21.
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
@@ -3800,7 +3803,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const maxFixes = settings.maxPostReviewFixes ?? 1;
if (!Number.isFinite(maxFixes) || maxFixes <= 0) return 0;
@@ -3890,7 +3893,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const timeoutMs = settings.taskStuckTimeoutMs;
if (!timeoutMs || timeoutMs <= 0) return 0;
@@ -3966,7 +3969,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const cycleStartMs = Date.now();
const timeoutMs = settings.taskStuckTimeoutMs;
@@ -4097,7 +4100,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const cycleStartMs = Date.now();
const thresholdMs = settings.inReviewStalledThresholdMs;
@@ -4254,7 +4257,7 @@ export class SelfHealingManager {
const timeoutMs = settings.taskStuckTimeoutMs;
if (!timeoutMs || timeoutMs <= 0) return 0;
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const now = Date.now();
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
@@ -4319,7 +4322,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const timeoutMs = settings.taskStuckTimeoutMs;
if (!timeoutMs || timeoutMs <= 0) return 0;
@@ -4606,7 +4609,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
@@ -4705,7 +4708,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const now = Date.now();
const inReview = await this.store.listTasks({ column: "in-review", slim: true });
@@ -4861,7 +4864,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
@@ -5019,7 +5022,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
@@ -5141,7 +5144,7 @@ export class SelfHealingManager {
async recoverCompletionHandoffLimbo(): Promise<void> {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return;
if (!settings.autoMerge) return;
if (settings.autoMerge === false) return;
const tasks = await this.store.listTasks({ column: "in-review", slim: false });
const now = Date.now();
@@ -6065,7 +6068,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
const candidates = tasks.filter((task) =>
@@ -6135,7 +6138,7 @@ export class SelfHealingManager {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (!settings.autoMerge) return 0;
if (settings.autoMerge === false) return 0;
const tasks = await this.store.listTasks({ column: "in-review", slim: true });