feat(FN-4465): complete Step 3 — scheduler stale task reporter

Fusion-Task-Id: FN-4465
Fusion-Task-Lineage: 4365d4cb-ab93-4ed5-add9-3e14b26237ad
This commit is contained in:
Fusion
2026-05-14 05:34:53 -07:00
committed by gsxdsm
parent f6951e7f4a
commit 3577a8babd
4 changed files with 299 additions and 0 deletions

View File

@@ -7,6 +7,8 @@ import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { schedulerLog } from "../logger.js";
const staleReporterReportMock = vi.fn();
// Mock fs modules
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
@@ -32,6 +34,12 @@ vi.mock("../logger.js", () => ({
},
}));
vi.mock("../stale-task-reporter.js", () => ({
StaleTaskReporter: vi.fn().mockImplementation(() => ({
report: staleReporterReportMock,
})),
}));
// Helper to create mock tasks
function createMockTask(overrides: Partial<Task> = {}): Task {
return {
@@ -146,6 +154,9 @@ describe("filterPathsByIgnoreList", () => {
});
describe("Scheduler", () => {
beforeEach(() => {
staleReporterReportMock.mockReset().mockResolvedValue({ surfaced: 0 });
});
// Helper to create mock MissionStore (shared across mission-related test suites)
function createMockMissionStore(overrides = {}) {
return {
@@ -170,6 +181,70 @@ describe("Scheduler", () => {
};
}
describe("stale task reporter integration", () => {
it("invokes reporter from schedule", async () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(readFile).mockResolvedValue("# Task\nDo something");
const store = createMockStore({
listTasks: vi.fn().mockResolvedValue([createMockTask({ id: "FN-STALE", column: "todo", dependencies: [] })]),
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
staleInProgressWarningMs: 1000,
staleInReviewWarningMs: 2000,
}),
});
const scheduler = new Scheduler(store);
(scheduler as unknown as { running: boolean }).running = true;
await scheduler.schedule();
expect(staleReporterReportMock).toHaveBeenCalledTimes(1);
});
it("does not throw when reporter errors", async () => {
staleReporterReportMock.mockRejectedValueOnce(new Error("boom"));
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(readFile).mockResolvedValue("# Task\nDo something");
const store = createMockStore({
listTasks: vi.fn().mockResolvedValue([createMockTask({ id: "FN-STALE", column: "todo", dependencies: [] })]),
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
staleInProgressWarningMs: 1000,
staleInReviewWarningMs: 2000,
}),
});
const scheduler = new Scheduler(store);
(scheduler as unknown as { running: boolean }).running = true;
await expect(scheduler.schedule()).resolves.toBeUndefined();
expect(schedulerLog.warn).toHaveBeenCalledWith("Stale task reporter failed", expect.any(Error));
});
it("rate-limits back-to-back reporter runs", async () => {
vi.useFakeTimers();
try {
vi.setSystemTime(new Date("2026-05-14T12:00:00.000Z"));
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(readFile).mockResolvedValue("# Task\nDo something");
const store = createMockStore({
listTasks: vi.fn().mockResolvedValue([createMockTask({ id: "FN-STALE", column: "todo", dependencies: [] })]),
getSettings: vi.fn().mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
staleInProgressWarningMs: 5000,
staleInReviewWarningMs: 10000,
}),
});
const scheduler = new Scheduler(store);
(scheduler as unknown as { running: boolean }).running = true;
await scheduler.schedule();
await scheduler.schedule();
expect(staleReporterReportMock).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
}
});
});
describe("constructor", () => {
it("initializes with default options", () => {
const store = createMockStore();