test(FN-4153): complete Step 5 — add scheduler ephemeral toggle integration coverage
Fusion-Task-Id: FN-4153 Fusion-Task-Lineage: 6ec09467-c7a0-4a9d-9a09-16d5ef6f7a86
This commit is contained in:
142
packages/engine/src/__tests__/scheduler-ephemeral-toggle.test.ts
Normal file
142
packages/engine/src/__tests__/scheduler-ephemeral-toggle.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import type { Agent, Task, TaskStore } from "@fusion/core";
|
||||
import { Scheduler } from "../scheduler.js";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
return { ...actual, existsSync: vi.fn() };
|
||||
});
|
||||
|
||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
||||
return { ...actual, readFile: vi.fn() };
|
||||
});
|
||||
|
||||
function makeTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: "FN-100",
|
||||
description: "test",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
...overrides,
|
||||
} as Task;
|
||||
}
|
||||
|
||||
function makeAgent(overrides: Partial<Agent> & Pick<Agent, "id">): Agent {
|
||||
return {
|
||||
id: overrides.id,
|
||||
name: overrides.name ?? overrides.id,
|
||||
role: overrides.role ?? "executor",
|
||||
state: overrides.state ?? "idle",
|
||||
createdAt: overrides.createdAt ?? "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: overrides.updatedAt ?? "2026-01-01T00:00:00.000Z",
|
||||
metadata: overrides.metadata ?? {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createStore(task: Task, settings: Record<string, unknown>, tasksForList?: Task[]): TaskStore {
|
||||
return {
|
||||
listTasks: vi.fn().mockImplementation(async () => tasksForList ?? [task]),
|
||||
getSettings: vi.fn().mockResolvedValue(settings),
|
||||
getTask: vi.fn().mockResolvedValue(task),
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
moveTask: vi.fn().mockResolvedValue(undefined),
|
||||
parseFileScopeFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/tmp/project"),
|
||||
getTasksDir: vi.fn().mockReturnValue("/tmp/project/.fusion/tasks"),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
} as unknown as TaskStore;
|
||||
}
|
||||
|
||||
async function runSchedulerOnce(scheduler: Scheduler): Promise<void> {
|
||||
await scheduler.start();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
scheduler.stop();
|
||||
}
|
||||
|
||||
describe("Scheduler ephemeralAgentsEnabled toggle", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.mocked(existsSync).mockReturnValue(true);
|
||||
vi.mocked(readFile).mockResolvedValue("# Prompt\n");
|
||||
});
|
||||
|
||||
it("default on: dispatches without auto-assigned agent", async () => {
|
||||
const task = makeTask({ id: "FN-101" });
|
||||
const store = createStore(task, { maxConcurrent: 2, maxWorktrees: 4, ephemeralAgentsEnabled: true });
|
||||
const scheduler = new Scheduler(store);
|
||||
|
||||
await runSchedulerOnce(scheduler);
|
||||
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("FN-101", expect.objectContaining({ assignedAgentId: expect.any(String) }));
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-101", "in-progress", expect.any(Object));
|
||||
});
|
||||
|
||||
it("off + no permanent executor: keeps task queued in todo", async () => {
|
||||
const task = makeTask({ id: "FN-102" });
|
||||
const store = createStore(task, { maxConcurrent: 2, maxWorktrees: 4, ephemeralAgentsEnabled: false });
|
||||
const scheduler = new Scheduler(store, {
|
||||
agentStore: {
|
||||
listAgents: vi.fn().mockResolvedValue([]),
|
||||
getChainOfCommand: vi.fn().mockResolvedValue([]),
|
||||
} as never,
|
||||
});
|
||||
|
||||
await runSchedulerOnce(scheduler);
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-102", { status: "queued" });
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-102", "queued — no permanent executor available (ephemeral agents disabled)");
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("off + permanent executor: assigns then dispatches", async () => {
|
||||
const task = makeTask({ id: "FN-103" });
|
||||
const store = createStore(task, { maxConcurrent: 2, maxWorktrees: 4, ephemeralAgentsEnabled: false });
|
||||
const scheduler = new Scheduler(store, {
|
||||
agentStore: {
|
||||
listAgents: vi.fn().mockResolvedValue([makeAgent({ id: "agent-1" })]),
|
||||
getChainOfCommand: vi.fn().mockResolvedValue([]),
|
||||
} as never,
|
||||
});
|
||||
|
||||
await runSchedulerOnce(scheduler);
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-103", { assignedAgentId: "agent-1" });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-103", "in-progress", expect.any(Object));
|
||||
});
|
||||
|
||||
it("off + multiple executors: picks least-loaded", async () => {
|
||||
const task = makeTask({ id: "FN-104" });
|
||||
const tasks = [
|
||||
task,
|
||||
makeTask({ id: "FN-A", column: "in-progress", assignedAgentId: "agent-heavy" }),
|
||||
makeTask({ id: "FN-B", column: "todo", assignedAgentId: "agent-heavy" }),
|
||||
makeTask({ id: "FN-C", column: "in-review", assignedAgentId: "agent-light" }),
|
||||
];
|
||||
const store = createStore(task, { maxConcurrent: 2, maxWorktrees: 4, ephemeralAgentsEnabled: false }, tasks);
|
||||
const scheduler = new Scheduler(store, {
|
||||
agentStore: {
|
||||
listAgents: vi.fn().mockResolvedValue([
|
||||
makeAgent({ id: "agent-heavy", createdAt: "2026-01-01T00:00:00.000Z" }),
|
||||
makeAgent({ id: "agent-light", createdAt: "2026-01-01T00:00:01.000Z" }),
|
||||
]),
|
||||
getChainOfCommand: vi.fn().mockResolvedValue([]),
|
||||
} as never,
|
||||
});
|
||||
|
||||
await runSchedulerOnce(scheduler);
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-104", { assignedAgentId: "agent-light" });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-104", "in-progress", expect.any(Object));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user