feat(FN-4087): replace sleep() with deterministic waits in tests
The merge replaces all real `setTimeout`/`sleep` polling with deterministic event-based waits across the executor pause integration tests, restart integration tests, dashboard tests, and chat-store tests, making the suite faster and more reliable. Fusion-Task-Id: FN-4087
This commit is contained in:
@@ -35,6 +35,22 @@ import {
|
||||
|
||||
const mockedReviewStep = vi.mocked(mockedReviewStepFn);
|
||||
|
||||
const WAIT_FOR_ASYNC_OPTIONS = { timeout: 2000, interval: 5 };
|
||||
|
||||
async function waitForAsyncExpectation(assertion: () => void | Promise<void>) {
|
||||
await vi.waitFor(assertion, WAIT_FOR_ASYNC_OPTIONS);
|
||||
}
|
||||
|
||||
async function waitForStepExecutorRegistration(executor: TaskExecutor, taskId: string) {
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect((executor as any).activeStepExecutors.has(taskId)).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("TaskExecutor context limit error recovery", () => {
|
||||
beforeEach(() => {
|
||||
resetExecutorMocks();
|
||||
@@ -784,8 +800,9 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
// Trigger the task:moved event manually
|
||||
store._trigger("task:moved", { task, from: "todo", to: "in-progress" });
|
||||
|
||||
// Wait for async execution to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(session.prompt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Verify the agent was created and prompt was called
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledWith(
|
||||
@@ -823,8 +840,7 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
// Trigger the task:moved event with to='done' (should not execute)
|
||||
store._trigger("task:moved", { task, from: "in-progress", to: "done" });
|
||||
|
||||
// Wait for async
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await Promise.resolve();
|
||||
|
||||
// Verify no agent was created
|
||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||
@@ -860,7 +876,14 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
|
||||
store.getTask.mockResolvedValue(movedTask);
|
||||
store._trigger("task:moved", { task: movedTask, from: "in-review", to: "in-progress" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-2883-A", expect.objectContaining({
|
||||
mergeDetails: null,
|
||||
mergeRetries: 0,
|
||||
verificationFailureCount: 0,
|
||||
workflowStepResults: [],
|
||||
}));
|
||||
});
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-2883-A", expect.objectContaining({
|
||||
mergeDetails: null,
|
||||
@@ -869,12 +892,14 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
workflowStepResults: [],
|
||||
}));
|
||||
expect(store.updateStep).toHaveBeenCalledWith("FN-2883-A", 3, "pending");
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-2883-A",
|
||||
expect.stringContaining("Task returned to in-progress from in-review column"),
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-2883-A",
|
||||
expect.stringContaining("Task returned to in-progress from in-review column"),
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
expect(executeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -905,7 +930,14 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
|
||||
store.getTask.mockResolvedValue(movedTask);
|
||||
store._trigger("task:moved", { task: movedTask, from: "done", to: "in-progress" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-2883-B", expect.objectContaining({
|
||||
mergeDetails: null,
|
||||
mergeRetries: 0,
|
||||
verificationFailureCount: 0,
|
||||
workflowStepResults: [],
|
||||
}));
|
||||
});
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-2883-B", expect.objectContaining({
|
||||
mergeDetails: null,
|
||||
@@ -914,12 +946,14 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
workflowStepResults: [],
|
||||
}));
|
||||
expect(store.updateStep).toHaveBeenCalledWith("FN-2883-B", 2, "pending");
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-2883-B",
|
||||
expect.stringContaining("Task returned to in-progress from done column"),
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-2883-B",
|
||||
expect.stringContaining("Task returned to in-progress from done column"),
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves verificationFailureCount for merge remediation cycles even if status was cleared", async () => {
|
||||
@@ -946,7 +980,14 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
|
||||
store.getTask.mockResolvedValue(movedTask);
|
||||
store._trigger("task:moved", { task: movedTask, from: "in-review", to: "in-progress" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-2883-D", expect.objectContaining({
|
||||
mergeDetails: null,
|
||||
mergeRetries: 0,
|
||||
verificationFailureCount: 2,
|
||||
workflowStepResults: [],
|
||||
}));
|
||||
});
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-2883-D", expect.objectContaining({
|
||||
mergeDetails: null,
|
||||
@@ -978,7 +1019,7 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
};
|
||||
|
||||
store._trigger("task:moved", { task: movedTask, from: "todo", to: "in-progress" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
await Promise.resolve();
|
||||
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("FN-2883-C", expect.objectContaining({ mergeDetails: null }));
|
||||
expect(store.updateStep).not.toHaveBeenCalled();
|
||||
@@ -1019,8 +1060,9 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
// Trigger task:moved away from in-progress
|
||||
store._trigger("task:moved", { task, from: "in-progress", to: "todo" });
|
||||
|
||||
// Allow async handlers to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(disposeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Verify session was disposed and removed from map
|
||||
expect(disposeSpy).toHaveBeenCalled();
|
||||
@@ -1059,8 +1101,9 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
// Trigger task:moved away from in-progress
|
||||
store._trigger("task:moved", { task, from: "in-progress", to: "triage" });
|
||||
|
||||
// Allow async handlers to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(mockTerminateAllSessions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Verify terminateAllSessions was called
|
||||
expect(mockTerminateAllSessions).toHaveBeenCalled();
|
||||
@@ -1134,9 +1177,9 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
|
||||
store._trigger("task:moved", { task, from: "in-progress", to: "todo" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(untrackSpy).toHaveBeenCalledWith("FN-004");
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(untrackSpy).toHaveBeenCalledWith("FN-004");
|
||||
});
|
||||
});
|
||||
|
||||
it("adds task to pausedAborted set to prevent re-execution", async () => {
|
||||
@@ -1170,9 +1213,9 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
|
||||
store._trigger("task:moved", { task, from: "in-progress", to: "triage" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect((executor as any).pausedAborted.has("FN-005")).toBe(true);
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect((executor as any).pausedAborted.has("FN-005")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1331,8 +1374,13 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
|
||||
it("prevents duplicate execution when task:moved fires twice for same task", async () => {
|
||||
const store = createMockStore();
|
||||
let resolvePrompt: (() => void) | undefined;
|
||||
const session = {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
prompt: vi.fn().mockImplementation(
|
||||
() => new Promise<void>((resolve) => {
|
||||
resolvePrompt = resolve;
|
||||
}),
|
||||
),
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -1353,21 +1401,23 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Trigger the event twice quickly
|
||||
// Trigger the event twice quickly while the first execution is still in flight.
|
||||
store._trigger("task:moved", { task, from: "todo", to: "in-progress" });
|
||||
store._trigger("task:moved", { task, from: "todo", to: "in-progress" });
|
||||
|
||||
// Wait for completion
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
expect(session.prompt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
resolvePrompt?.();
|
||||
await Promise.resolve();
|
||||
|
||||
// The executing guard prevents duplicate execution from the event handler.
|
||||
// Note: createFnAgent may be called a second time if the agent finishes
|
||||
// without calling fn_task_done (retry path), but the initial trigger should
|
||||
// only cause one execution, not two.
|
||||
// Verify that store.on was called with task:moved (listener registered)
|
||||
expect(store.on).toHaveBeenCalledWith("task:moved", expect.any(Function));
|
||||
// Verify the event handler initiated execute() (not twice from events)
|
||||
// The executing set guard works — both triggers don't cause double execution
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
expect(session.prompt).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("logs error when execute() fails in task:moved handler", async () => {
|
||||
@@ -1393,8 +1443,12 @@ describe("TaskExecutor agent execution flow (FN-978)", () => {
|
||||
// Trigger the event
|
||||
store._trigger("task:moved", { task, from: "todo", to: "in-progress" });
|
||||
|
||||
// Wait for async
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: "FN-978" }),
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
// Verify the error handler was called
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
@@ -1944,7 +1998,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
retries: 0,
|
||||
tokenUsage: { inputTokens: 20, outputTokens: 10, cachedTokens: 2, totalTokens: 32 },
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await Promise.resolve();
|
||||
|
||||
options.onStepComplete(1, {
|
||||
stepIndex: 1,
|
||||
@@ -1952,7 +2006,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
retries: 0,
|
||||
tokenUsage: { inputTokens: 30, outputTokens: 5, cachedTokens: 1, totalTokens: 36 },
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await Promise.resolve();
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -2228,8 +2282,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
// Start execution (don't await — it will hang)
|
||||
const executePromise = executor.execute(task);
|
||||
|
||||
// Give it time to set up the step executor
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForStepExecutorRegistration(executor, "FN-200");
|
||||
|
||||
// Trigger pause
|
||||
store._trigger("task:updated", { ...task, paused: true });
|
||||
@@ -2256,8 +2309,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
const task = createTaskWithSteps();
|
||||
const executePromise = executor.execute(task);
|
||||
|
||||
// Give it time to set up the step executor
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForStepExecutorRegistration(executor, "FN-200");
|
||||
|
||||
// Trigger stuck kill
|
||||
executor.markStuckAborted("FN-200");
|
||||
@@ -2285,8 +2337,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
const task = createTaskWithSteps();
|
||||
const executePromise = executor.execute(task);
|
||||
|
||||
// Give it time to set up the step executor
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForStepExecutorRegistration(executor, "FN-200");
|
||||
|
||||
// Verify step executor is registered
|
||||
expect((executor as any).activeStepExecutors.has("FN-200")).toBe(true);
|
||||
@@ -2320,7 +2371,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
const task = createTaskWithSteps();
|
||||
const executePromise = executor.execute(task);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForStepExecutorRegistration(executor, "FN-200");
|
||||
|
||||
// Budget exhausted — should NOT requeue
|
||||
executor.markStuckAborted("FN-200", false);
|
||||
@@ -2355,7 +2406,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
const task = createTaskWithSteps();
|
||||
const executePromise = executor.execute(task);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForStepExecutorRegistration(executor, "FN-200");
|
||||
|
||||
// Trigger pause
|
||||
store._trigger("task:updated", { ...task, paused: true });
|
||||
@@ -2448,8 +2499,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
const task = createTaskWithSteps();
|
||||
const executePromise = executor.execute(task);
|
||||
|
||||
// Give it time to set up the step executor
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForStepExecutorRegistration(executor, "FN-200");
|
||||
|
||||
// Simulate dep-abort by directly triggering the fn_task_add_dep cleanup logic
|
||||
// The dep-abort flag should cause the step-session path to handle cleanup
|
||||
@@ -2524,9 +2574,7 @@ describe("StepSessionExecutor integration", () => {
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timers to trigger the setTimeout that moves task to todo then in-progress
|
||||
vi.advanceTimersByTime(0);
|
||||
// Run any pending microtasks (the async code in setTimeout)
|
||||
await vi.runAllTimersAsync();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
// Task should move to todo then in-progress (not in-review). The
|
||||
// workflow-rerun bounce flags preserveResumeState so the worktree and
|
||||
|
||||
@@ -9,9 +9,15 @@
|
||||
* - Triage re-picks unspecified tasks
|
||||
* - Crash scenarios are handled gracefully (semaphore release, status cleanup)
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { AgentSemaphore } from "../concurrency.js";
|
||||
|
||||
const WAIT_FOR_ASYNC_OPTIONS = { timeout: 2000, interval: 5 };
|
||||
|
||||
async function waitForAsyncExpectation(assertion: () => void | Promise<void>) {
|
||||
await vi.waitFor(assertion, WAIT_FOR_ASYNC_OPTIONS);
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type, @typescript-eslint/no-explicit-any -- Test mocks use Function/any type for simplicity */
|
||||
|
||||
// ── Module-level mocks (matching existing test patterns) ──────────────────
|
||||
@@ -404,6 +410,10 @@ beforeEach(() => {
|
||||
}) as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// ── Step 2: In-progress task resume tests ─────────────────────────────────
|
||||
|
||||
describe("In-progress task resume after restart", () => {
|
||||
@@ -420,8 +430,9 @@ describe("In-progress task resume after restart", () => {
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
|
||||
// Wait for async execute calls to complete
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// Exactly one agent session per in-progress task (no retry inflation)
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(2);
|
||||
@@ -447,7 +458,9 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-010", "Resumed after engine restart");
|
||||
});
|
||||
|
||||
// No git worktree add commands should have been called
|
||||
const gitWorktreeAddCalls = mockedExecSync.mock.calls.filter(
|
||||
@@ -478,7 +491,9 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(capturedPrompt).toContain("⚠️ RESUMING");
|
||||
});
|
||||
|
||||
expect(capturedPrompt).toContain("⚠️ RESUMING");
|
||||
expect(capturedPrompt).toContain("Step 0 (Step 0): **done**");
|
||||
@@ -501,7 +516,9 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.getSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// getSettings is called (for project commands in execution prompt) but init command should not run
|
||||
expect(store.getSettings).toHaveBeenCalled();
|
||||
@@ -540,7 +557,9 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.updateStep).toHaveBeenCalledWith("FN-1701", 1, "done");
|
||||
});
|
||||
|
||||
// The step should have been flipped to done *before* execute ran.
|
||||
expect(store.updateStep).toHaveBeenCalledWith("FN-1701", 1, "done");
|
||||
@@ -572,7 +591,9 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Must NOT mark the step done — the reset invalidated the prior approval.
|
||||
const updateStepDoneCalls = store.updateStep.mock.calls.filter(
|
||||
@@ -599,7 +620,9 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const updateStepDoneCalls = store.updateStep.mock.calls.filter(
|
||||
(c: any[]) => c[0] === "FN-1703" && c[2] === "done",
|
||||
@@ -630,7 +653,9 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.updateStep).toHaveBeenCalledWith("FN-1704", 0, "done");
|
||||
});
|
||||
|
||||
expect(store.updateStep).toHaveBeenCalledWith("FN-1704", 0, "done");
|
||||
expect(store.updateStep).toHaveBeenCalledWith("FN-1704", 1, "done");
|
||||
@@ -649,7 +674,9 @@ describe("In-progress task resume after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-040", "Resumed after engine restart");
|
||||
});
|
||||
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-040", "Resumed after engine restart");
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-041", "Resumed after engine restart");
|
||||
@@ -784,9 +811,7 @@ describe("In-progress task resume after restart", () => {
|
||||
expect(store.updateStep).toHaveBeenCalledWith("FN-963", 0, "pending");
|
||||
|
||||
// Advance timers to trigger the setTimeout that moves task to todo then in-progress
|
||||
vi.advanceTimersByTime(0);
|
||||
// Run any pending microtasks (the async code in setTimeout)
|
||||
await vi.runAllTimersAsync();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
// Task should move to todo then in-progress (not in-review). The
|
||||
// workflow-rerun bounce passes `preserveWorktree: true` so the
|
||||
@@ -955,8 +980,9 @@ describe("Triage re-pick after restart", () => {
|
||||
});
|
||||
triage.start();
|
||||
|
||||
// Wait for the immediate poll() to fire
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-060", { status: "planning" });
|
||||
});
|
||||
triage.stop();
|
||||
|
||||
// Both triage tasks should have been picked up for specification
|
||||
@@ -993,8 +1019,9 @@ describe("Triage re-pick after restart", () => {
|
||||
// Start first specification (will block on prompt)
|
||||
const first = triage.specifyTask(task);
|
||||
|
||||
// Give it time to enter processing set
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Second call should be a no-op (already processing)
|
||||
await triage.specifyTask(task);
|
||||
@@ -1029,7 +1056,9 @@ describe("Scheduler after restart", () => {
|
||||
|
||||
// Use start/stop to trigger schedule() then clean up
|
||||
scheduler.start();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-070", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||
});
|
||||
scheduler.stop();
|
||||
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-070", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||
@@ -1054,7 +1083,9 @@ describe("Scheduler after restart", () => {
|
||||
});
|
||||
|
||||
scheduler.start();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(onBlocked).toHaveBeenCalledWith(blockedTask, ["FN-071"]);
|
||||
});
|
||||
scheduler.stop();
|
||||
|
||||
// Task should NOT have been moved
|
||||
@@ -1087,7 +1118,9 @@ describe("Scheduler after restart", () => {
|
||||
pollIntervalMs: 100000,
|
||||
});
|
||||
triage.start();
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-080", { status: "planning" });
|
||||
});
|
||||
triage.stop();
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-080", { status: "planning" });
|
||||
@@ -1099,7 +1132,9 @@ describe("Scheduler after restart", () => {
|
||||
|
||||
const scheduler = new Scheduler(store, { maxConcurrent: 2, maxWorktrees: 4 });
|
||||
scheduler.start();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-081", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||
});
|
||||
scheduler.stop();
|
||||
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-081", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||
@@ -1115,7 +1150,9 @@ describe("Scheduler after restart", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/root");
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-082", "Resumed after engine restart");
|
||||
});
|
||||
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-082", "Resumed after engine restart");
|
||||
|
||||
@@ -1153,7 +1190,9 @@ describe("Crash scenario edge cases", () => {
|
||||
});
|
||||
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(onError).toHaveBeenCalledWith(task, expect.any(Error));
|
||||
});
|
||||
|
||||
// onError should have been called
|
||||
expect(onError).toHaveBeenCalledWith(task, expect.any(Error));
|
||||
@@ -1170,7 +1209,9 @@ describe("Crash scenario edge cases", () => {
|
||||
createAgentWithTaskDone();
|
||||
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Exactly one agent created for the re-resume, proving the task was eligible
|
||||
// and completed without retry inflation.
|
||||
@@ -1245,11 +1286,13 @@ describe("Crash scenario edge cases", () => {
|
||||
|
||||
// First call starts execution
|
||||
const first = executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Second call while first is still executing
|
||||
const second = executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await Promise.resolve();
|
||||
|
||||
// Only one agent should have been created (the executing set guards against double-exec)
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||
@@ -1282,7 +1325,9 @@ describe("Crash scenario edge cases", () => {
|
||||
});
|
||||
|
||||
await executor.resumeOrphaned();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Semaphore should return to pre-execution count (1 from our manual acquire)
|
||||
expect(sem.activeCount).toBe(1);
|
||||
@@ -1380,7 +1425,6 @@ describe("Worktree pool restart with recycleWorktrees=true", () => {
|
||||
|
||||
const executor = new TaskExecutor(store, "/root", { pool });
|
||||
await executor.execute(makeTask("FN-110", "in-progress"));
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
// Pool should be empty (worktree acquired)
|
||||
expect(pool.size).toBe(0);
|
||||
@@ -1615,7 +1659,9 @@ describe("Engine pause/unpause cycle", () => {
|
||||
});
|
||||
|
||||
scheduler.start();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-EP3", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||
});
|
||||
|
||||
// Scheduler should have moved todo task to in-progress
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-EP3", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||
@@ -1638,7 +1684,9 @@ describe("Engine pause/unpause cycle", () => {
|
||||
previous: { ...DEFAULT_SETTINGS, enginePaused: true },
|
||||
});
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
await waitForAsyncExpectation(() => {
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-EP4", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||
});
|
||||
scheduler.stop();
|
||||
|
||||
// The new task should have been scheduled after unpause
|
||||
|
||||
Reference in New Issue
Block a user