feat(FN-4887): complete Step 2-3 — add foreign-only recovery sweep and shared handler
Fusion-Task-Id: FN-4887 Fusion-Task-Lineage: 559690d8-cea6-4323-a522-9ebb6aa25731
This commit is contained in:
committed by
gsxdsm
parent
91df093502
commit
b5ed3d0c9c
@@ -57,6 +57,29 @@ describe("reliability interaction: contamination auto-recovery precedence", () =
|
||||
expect(issueRetry).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("foreign-only no-own-work routes to retry, not pause", async () => {
|
||||
const issueRetry = vi.fn(async () => {});
|
||||
const dispatcher = new AutoRecoveryDispatcher({
|
||||
taskStore: {} as never,
|
||||
auditEmitter: { database: vi.fn(async () => {}), git: vi.fn(), filesystem: vi.fn(), sandbox: vi.fn() },
|
||||
handlers: { issueRetry },
|
||||
});
|
||||
|
||||
const decision = await dispatcher.dispatch({
|
||||
class: "branch-cross-contamination",
|
||||
taskId: "FN-1",
|
||||
pausedReason: "branch-cross-contamination",
|
||||
evidence: { ownCommits: 0, foreignAttributedCommits: 3, recoveryKind: "foreign-only" },
|
||||
}, {
|
||||
task: baseTask,
|
||||
retryCount: 0,
|
||||
settings: { mode: "programmatic", maxRetries: 2 },
|
||||
});
|
||||
|
||||
expect(decision.action).toBe("retry");
|
||||
expect(issueRetry).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("mode off and destructive ambiguity preserve pause", () => {
|
||||
const dispatcher = new AutoRecoveryDispatcher({
|
||||
taskStore: {} as never,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import type { Task } from "@fusion/core";
|
||||
|
||||
const mocked = vi.hoisted(() => ({
|
||||
classifyForeignOnlyContamination: vi.fn(),
|
||||
recoverForeignOnlyContamination: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../branch-conflicts.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../branch-conflicts.js")>("../branch-conflicts.js");
|
||||
return {
|
||||
...actual,
|
||||
classifyForeignOnlyContamination: mocked.classifyForeignOnlyContamination,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../recovery/foreign-only-contamination.js", () => ({
|
||||
recoverForeignOnlyContamination: mocked.recoverForeignOnlyContamination,
|
||||
}));
|
||||
|
||||
import { SelfHealingManager } from "../self-healing.js";
|
||||
|
||||
function mkTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: "FN-1",
|
||||
column: "in-review",
|
||||
branch: "fusion/fn-1",
|
||||
worktree: "/tmp/wt",
|
||||
baseCommitSha: "main",
|
||||
paused: false,
|
||||
userPaused: false,
|
||||
mergeDetails: null,
|
||||
steps: [],
|
||||
...overrides,
|
||||
} as Task;
|
||||
}
|
||||
|
||||
describe("SelfHealingManager.recoverForeignOnlyContaminatedInReviewTasks", () => {
|
||||
const store = {
|
||||
getSettings: vi.fn(async () => ({ globalPause: false, enginePaused: false })),
|
||||
listTasks: vi.fn(),
|
||||
logEntry: vi.fn(async () => {}),
|
||||
on: vi.fn(),
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("recovers foreign-only in-review candidates", async () => {
|
||||
store.listTasks.mockImplementation(async ({ column }: { column: string }) => column === "in-review" ? [mkTask()] : []);
|
||||
mocked.classifyForeignOnlyContamination.mockResolvedValue({ kind: "foreign-only-no-own-work" });
|
||||
mocked.recoverForeignOnlyContamination.mockResolvedValue({ recovered: true, subtype: "reanchor" });
|
||||
|
||||
const manager = new SelfHealingManager(store, { rootDir: process.cwd() });
|
||||
const recovered = await manager.recoverForeignOnlyContaminatedInReviewTasks();
|
||||
|
||||
expect(recovered).toBe(1);
|
||||
expect(mocked.recoverForeignOnlyContamination).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("skips ambiguous and user-paused tasks", async () => {
|
||||
store.listTasks.mockImplementation(async ({ column }: { column: string }) => {
|
||||
if (column === "in-review") return [mkTask({ id: "FN-2", userPaused: true }), mkTask({ id: "FN-3" })];
|
||||
return [];
|
||||
});
|
||||
mocked.classifyForeignOnlyContamination.mockResolvedValue({ kind: "ambiguous" });
|
||||
|
||||
const manager = new SelfHealingManager(store, { rootDir: process.cwd() });
|
||||
const recovered = await manager.recoverForeignOnlyContaminatedInReviewTasks();
|
||||
|
||||
expect(recovered).toBe(0);
|
||||
expect(mocked.recoverForeignOnlyContamination).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user