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:
Fusion
2026-05-12 02:57:40 -07:00
committed by gsxdsm
parent 416d6e3767
commit b1a0464d68
4 changed files with 356 additions and 167 deletions

View File

@@ -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

View File

@@ -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