feat(FN-5091): complete Step 4 — wire self-healing reporter pass

Fusion-Task-Id: FN-5091
Fusion-Task-Lineage: f59dc7dd-abc2-4d1d-a755-1fe446314131
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 03:07:43 -07:00
committed by gsxdsm
parent 2bc1c023c8
commit e698d44914
2 changed files with 76 additions and 0 deletions

View File

@@ -4870,6 +4870,45 @@ describe("SelfHealingManager", () => {
});
});
describe("surfaceDependencyBlockedTodos", () => {
it("returns 0 when globalPause is enabled", async () => {
const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project", getProjectId: () => "/tmp/test-project" });
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ globalPause: true });
expect(await managerWithRecovery.surfaceDependencyBlockedTodos()).toBe(0);
managerWithRecovery.stop();
});
it("returns 0 when dependency-blocked todo reporting is disabled", async () => {
const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project", getProjectId: () => "/tmp/test-project" });
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ dependencyBlockedTodoReportEnabled: false });
expect(await managerWithRecovery.surfaceDependencyBlockedTodos()).toBe(0);
managerWithRecovery.stop();
});
it("returns groupCount from reporter", async () => {
const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project", getProjectId: () => "/tmp/test-project" });
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ dependencyBlockedTodoReportEnabled: true });
const reportSpy = vi.fn().mockResolvedValue({ alerted: true, groupCount: 1 });
(managerWithRecovery as unknown as { dependencyBlockedTodoReporter: { report: typeof reportSpy } }).dependencyBlockedTodoReporter = { report: reportSpy };
expect(await managerWithRecovery.surfaceDependencyBlockedTodos()).toBe(1);
expect(reportSpy).toHaveBeenCalledWith();
managerWithRecovery.stop();
});
it("returns 0 and logs error when reporter fails", async () => {
const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project", getProjectId: () => "/tmp/test-project" });
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ dependencyBlockedTodoReportEnabled: true });
const reportSpy = vi.fn().mockRejectedValue(new Error("boom"));
(managerWithRecovery as unknown as { dependencyBlockedTodoReporter: { report: typeof reportSpy } }).dependencyBlockedTodoReporter = { report: reportSpy };
expect(await managerWithRecovery.surfaceDependencyBlockedTodos()).toBe(0);
managerWithRecovery.stop();
});
});
describe("surfaceStalePausedReviews", () => {
function pausedReviewTask(overrides: Record<string, unknown> = {}) {
return {

View File

@@ -57,6 +57,7 @@ import {
type NtfyNotifier,
} from "./notifier.js";
import type { GhostBugDecision } from "./triage-preflight.js";
import { DependencyBlockedTodoReporter } from "./dependency-blocked-todo-reporter.js";
const log = createLogger("self-healing");
const worktreeMetadataReconcileLog = createLogger("worktree-metadata-reconcile");
@@ -250,6 +251,7 @@ export interface SelfHealingOptions {
messageStore?: MessageStore;
/** Optional notifier for board-stall unrecovered alerts. */
ntfyNotifier?: Pick<NtfyNotifier, "notifyBoardStallUnrecovered">;
getProjectId?: () => string;
}
const APPROVED_TRIAGE_RECOVERY_GRACE_MS = 60_000;
@@ -529,6 +531,8 @@ export class SelfHealingManager {
private orphanArchivedAcknowledged = new Set<string>();
private finalizeUnprovenWarned = new Set<string>();
private maintenanceTickCounter = 0;
private dependencyBlockedTodoReporter: DependencyBlockedTodoReporter | null = null;
private boardStallWindow: {
windowStartMs: number;
windowStartBlockedDepth: number;
@@ -4041,6 +4045,39 @@ export class SelfHealingManager {
}
}
private getDependencyBlockedTodoReporter(): DependencyBlockedTodoReporter | null {
if (this.dependencyBlockedTodoReporter) {
return this.dependencyBlockedTodoReporter;
}
const projectId = this.options.getProjectId?.();
if (!projectId) {
return null;
}
this.dependencyBlockedTodoReporter = new DependencyBlockedTodoReporter({
store: this.store,
projectId,
now: () => Date.now(),
});
return this.dependencyBlockedTodoReporter;
}
async surfaceDependencyBlockedTodos(): Promise<number> {
try {
const settings = await this.store.getSettings();
if (settings.globalPause || settings.enginePaused) return 0;
if (settings.dependencyBlockedTodoReportEnabled === false) return 0;
const reporter = this.getDependencyBlockedTodoReporter();
if (!reporter) return 0;
const result = await reporter.report();
return result.groupCount ?? 0;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
log.error(`Dependency-blocked todo surfacing failed: ${errorMessage}`);
return 0;
}
}
async surfaceStalePausedReviews(): Promise<number> {
try {
const settings = await this.store.getSettings();