test(FN-perf): convert research-dispatcher polling test to fake timers

Replace ~200ms of real-time sleep() waits with deterministic fake-timer
advances. The dispatcher clamps tickIntervalMs to a 100ms floor, so the
old 30-40ms real sleeps never fired a second interval tick — the
double-dispatch and stop-cancels-timer cases now genuinely exercise a
follow-up tick, making them both faster and stronger.

Fusion-Task-Id: FN-perf
This commit is contained in:
gsxdsm
2026-08-03 17:05:40 -07:00
parent b7d9cbf186
commit 4f9f5e4276

View File

@@ -1,11 +1,27 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ResearchRun, ResearchStore } from "@fusion/core";
import type { ResearchOrchestrator } from "../research/research-orchestrator.js";
import { ResearchRunDispatcher } from "../research/research-dispatcher.js";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/*
FNXC:ResearchDispatcher 2026-08-04-00:03:
This is a pure timer-driven polling unit test (no subprocess, no network), so it uses
FAKE TIMERS instead of real `sleep()` waits per AGENTS.md "Do Not Add Slow Tests" /
"Prefer fake timers over real polling/time waits". The previous real-sleep version was
both slow (~200ms of wall-clock waits) AND weaker: the dispatcher clamps tickIntervalMs
to a 100ms floor, so 30-40ms real sleeps never let the interval re-fire — only the
immediate `void this.tick()` in start() ran. The double-dispatch and stop-cancels-timer
cases therefore never exercised a SECOND tick. Advancing fake timers past the 100ms
interval now deterministically fires follow-up ticks, so those invariants are genuinely
asserted. stop()'s drain loop only spins when inFlight is non-empty, so every test drains
inFlight (immediate/resolved runs) before stop, keeping the fake-timer clock from stalling
in that real-`setTimeout` loop.
*/
describe("ResearchRunDispatcher", () => {
afterEach(() => {
vi.useRealTimers();
});
function createStore(runs: ResearchRun[]): ResearchStore {
return {
listRuns: vi.fn(() => runs),
@@ -13,6 +29,7 @@ describe("ResearchRunDispatcher", () => {
}
it("dispatches queued runs", async () => {
vi.useFakeTimers();
const runs = [{ id: "RR-1", query: "hello", status: "queued" } as ResearchRun];
const store = createStore(runs);
const startRun = vi.fn(async () => ({ id: "RR-1" } as ResearchRun));
@@ -20,13 +37,15 @@ describe("ResearchRunDispatcher", () => {
const dispatcher = new ResearchRunDispatcher({ store, orchestrator, tickIntervalMs: 10 });
dispatcher.start();
await sleep(30);
// Flush the immediate `void this.tick()` chain (await listRuns -> startRun).
await vi.advanceTimersByTimeAsync(0);
expect(startRun).toHaveBeenCalledWith("RR-1", "hello", expect.objectContaining({ abortSignal: expect.any(AbortSignal) }));
await dispatcher.stop();
});
it("does not double-dispatch in-flight runs", async () => {
vi.useFakeTimers();
const runs = [{ id: "RR-1", query: "hello", status: "queued" } as ResearchRun];
const store = createStore(runs);
let resolveRun: (() => void) | undefined;
@@ -37,15 +56,21 @@ describe("ResearchRunDispatcher", () => {
const dispatcher = new ResearchRunDispatcher({ store, orchestrator, tickIntervalMs: 10 });
dispatcher.start();
await sleep(40);
// Immediate tick dispatches RR-1 and marks it in-flight (startRun stays pending).
await vi.advanceTimersByTimeAsync(0);
expect(startRun).toHaveBeenCalledTimes(1);
// Fire a follow-up interval tick (100ms floor): the in-flight guard must skip RR-1.
await vi.advanceTimersByTimeAsync(100);
expect(startRun).toHaveBeenCalledTimes(1);
resolveRun?.();
await sleep(10);
await vi.advanceTimersByTimeAsync(0);
await dispatcher.stop();
});
it("survives startRun rejection", async () => {
vi.useFakeTimers();
const runs = [{ id: "RR-1", query: "hello", status: "queued" } as ResearchRun];
const store = createStore(runs);
const startRun = vi.fn(async () => {
@@ -55,13 +80,14 @@ describe("ResearchRunDispatcher", () => {
const dispatcher = new ResearchRunDispatcher({ store, orchestrator, tickIntervalMs: 10 });
dispatcher.start();
await sleep(40);
await vi.advanceTimersByTimeAsync(0);
expect(startRun).toHaveBeenCalled();
await dispatcher.stop();
});
it("stop cancels timer", async () => {
vi.useFakeTimers();
const runs = [{ id: "RR-1", query: "hello", status: "queued" } as ResearchRun];
const store = createStore(runs);
const startRun = vi.fn(async () => ({ id: "RR-1" } as ResearchRun));
@@ -69,11 +95,12 @@ describe("ResearchRunDispatcher", () => {
const dispatcher = new ResearchRunDispatcher({ store, orchestrator, tickIntervalMs: 10 });
dispatcher.start();
await sleep(30);
await vi.advanceTimersByTimeAsync(0);
await dispatcher.stop();
const callsAfterStop = startRun.mock.calls.length;
await sleep(40);
// Advancing well past several intervals must produce no further ticks once stopped.
await vi.advanceTimersByTimeAsync(300);
expect(startRun).toHaveBeenCalledTimes(callsAfterStop);
});
});