feat(FN-3201): restore engine unpause merge sweep and document soft-pause b

The merge restores the engine's unpause merge sweep logic in `project-engine.ts` and documents the soft-pause merge resume behavior across architecture and settings reference docs, with associated test coverage added.

Fusion-Task-Id: FN-3201
This commit is contained in:
Fusion
2026-05-02 09:45:22 -07:00
committed by gsxdsm
parent 222e11c2f8
commit 62de4b1be3
4 changed files with 45 additions and 47 deletions

View File

@@ -1479,10 +1479,12 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
vi.useRealTimers();
});
it("engine unpause sweep does not enqueue paused in-review tasks", async () => {
it("engine unpause sweep re-enqueues only merge-eligible in-review tasks", async () => {
const mockStore = createMockStore({ ...baseSettings, autoMerge: true });
mocks.currentStore = mockStore.store;
const engine = createEngine();
const engine = createEngine({
getTaskMergeBlocker: (task) => (task.id === "FN-blocked" ? "blocked" : null),
});
const privateEngine = engine as unknown as { internalEnqueueMerge: (taskId: string) => void };
const enqueueSpy = vi.spyOn(privateEngine, "internalEnqueueMerge");
@@ -1490,6 +1492,8 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
enqueueSpy.mockClear();
mockStore.store.listTasks.mockResolvedValueOnce([
{ id: "FN-paused", column: "in-review", paused: true, mergeRetries: 0, status: null },
{ id: "FN-failed", column: "in-review", paused: false, mergeRetries: 0, status: "failed" },
{ id: "FN-blocked", column: "in-review", paused: false, mergeRetries: 0, status: null },
{ id: "FN-ready", column: "in-review", paused: false, mergeRetries: 0, status: null },
]);
@@ -1500,6 +1504,8 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
expect(enqueueSpy).toHaveBeenCalledWith("FN-ready");
expect(enqueueSpy).not.toHaveBeenCalledWith("FN-paused");
expect(enqueueSpy).not.toHaveBeenCalledWith("FN-failed");
expect(enqueueSpy).not.toHaveBeenCalledWith("FN-blocked");
await engine.stop();
});

View File

@@ -1790,6 +1790,39 @@ export class ProjectEngine {
// ── Settings event listeners ──
private async resumeAfterUnpauseAndSweepInReview(
store: TaskStore,
settings: Settings,
source: "Global unpause" | "Engine unpause",
): Promise<void> {
try {
const runtime = this.runtime as any;
runtime.resumeAfterUnpause?.().catch((err: Error) =>
runtimeLog.error(
`Failed to resume agentic activity on ${source.toLowerCase()}:`,
err,
),
);
} catch (err: unknown) {
runtimeLog.warn(
`${source}: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`,
);
}
if (settings.globalPause || settings.enginePaused || !settings.autoMerge) {
return;
}
try {
const tasks = await store.listTasks({ column: "in-review" });
this.enqueueEligibleInReviewTasks(tasks as Task[]);
} catch (err: unknown) {
runtimeLog.warn(
`${source}: failed to scan in-review tasks for auto-merge: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
private wireSettingsListeners(store: TaskStore): void {
// 1. Global pause — terminate active merge session AND abort any running
// deterministic verification (pnpm test/build). The abort controller gates
@@ -1822,28 +1855,7 @@ export class ProjectEngine {
}) => {
if (prev.globalPause && !s.globalPause) {
runtimeLog.log("Global unpause — resuming agentic activity");
try {
const runtime = this.runtime as any;
runtime.resumeAfterUnpause?.().catch((err: Error) =>
runtimeLog.error("Failed to resume agentic activity on unpause:", err),
);
} catch (err: unknown) {
runtimeLog.warn(
`Global unpause: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`,
);
}
if (s.autoMerge) {
try {
const tasks = await store.listTasks({ column: "in-review" });
this.enqueueEligibleInReviewTasks(tasks as Task[]);
} catch (err: unknown) {
runtimeLog.warn(
`Global unpause: failed to scan in-review tasks for auto-merge: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
await this.resumeAfterUnpauseAndSweepInReview(store, s, "Global unpause");
}
};
store.on("settings:updated", onGlobalUnpause);
@@ -1859,28 +1871,7 @@ export class ProjectEngine {
}) => {
if (prev.enginePaused && !s.enginePaused) {
runtimeLog.log("Engine unpaused — resuming agentic activity");
try {
const runtime = this.runtime as any;
runtime.resumeAfterUnpause?.().catch((err: Error) =>
runtimeLog.error("Failed to resume agentic activity on engine unpause:", err),
);
} catch (err: unknown) {
runtimeLog.warn(
`Engine unpause: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`,
);
}
if (s.autoMerge) {
try {
const tasks = await store.listTasks({ column: "in-review" });
this.enqueueEligibleInReviewTasks(tasks as Task[]);
} catch (err: unknown) {
runtimeLog.warn(
`Engine unpause: failed to scan in-review tasks for auto-merge: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
await this.resumeAfterUnpauseAndSweepInReview(store, s, "Engine unpause");
}
};
store.on("settings:updated", onEngineUnpause);