Merge pull request #1539 from Runfusion/feature/workflow-mapping
fix(FN-6035): align workflow dispatch and broad suite
This commit is contained in:
@@ -6,11 +6,13 @@ const { execMock, readFileMock, pingMock, versionMock, inspectMock, dockerCtor,
|
||||
const pingMock = vi.fn();
|
||||
const versionMock = vi.fn();
|
||||
const inspectMock = vi.fn();
|
||||
const dockerCtor = vi.fn().mockImplementation(() => ({
|
||||
const dockerCtor = vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
ping: pingMock,
|
||||
version: versionMock,
|
||||
getContainer: vi.fn(() => ({ inspect: inspectMock, logs: vi.fn().mockResolvedValue(Buffer.from("logs")) })),
|
||||
}));
|
||||
};
|
||||
});
|
||||
const dockerodeModuleFactoryMock = vi.fn(() => ({ default: dockerCtor }));
|
||||
return { execMock, readFileMock, pingMock, versionMock, inspectMock, dockerCtor, dockerodeModuleFactoryMock };
|
||||
});
|
||||
|
||||
@@ -51,10 +51,12 @@ const {
|
||||
});
|
||||
|
||||
vi.mock("../docker-client.js", () => ({
|
||||
DockerClientService: vi.fn().mockImplementation(() => ({
|
||||
getDockerInstance: getDockerInstanceMock,
|
||||
getContainerInfo: vi.fn(),
|
||||
})),
|
||||
DockerClientService: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
getDockerInstance: getDockerInstanceMock,
|
||||
getContainerInfo: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
import { DockerProvisioningService } from "../docker-provisioning";
|
||||
|
||||
@@ -85,11 +85,13 @@ describe("NodeDiscovery", () => {
|
||||
publishMock.mockReturnValue(publishService);
|
||||
findMock.mockReturnValue(browser);
|
||||
destroyMock.mockReturnValue(undefined);
|
||||
BonjourMock.mockImplementation(() => ({
|
||||
publish: publishMock,
|
||||
find: findMock,
|
||||
destroy: destroyMock,
|
||||
}));
|
||||
BonjourMock.mockImplementation(function () {
|
||||
return {
|
||||
publish: publishMock,
|
||||
find: findMock,
|
||||
destroy: destroyMock,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -7,10 +7,12 @@ import { TaskStore, setTaskCreatedHook } from "@fusion/core";
|
||||
import { HeartbeatMonitor } from "../agent-heartbeat.js";
|
||||
import { createDelegateTaskTool, createTaskCreateTool } from "../agent-tools.js";
|
||||
|
||||
const githubTrackingHookEntry = "../../../dashboard/src/github-tracking-hook.js";
|
||||
const githubTrackingEntry = "../../../dashboard/src/github-tracking.js";
|
||||
const githubTrackingHookModulePromise: Promise<any> = import(/* @vite-ignore */ githubTrackingHookEntry);
|
||||
const githubTrackingModulePromise: Promise<any> = import(/* @vite-ignore */ githubTrackingEntry);
|
||||
const githubTrackingHookModulePromise: Promise<any> = import(
|
||||
new URL("../../../dashboard/src/github-tracking-hook.js", import.meta.url).href
|
||||
);
|
||||
const githubTrackingModulePromise: Promise<any> = import(
|
||||
new URL("../../../dashboard/src/github-tracking.js", import.meta.url).href
|
||||
);
|
||||
|
||||
function makeTmpDir(prefix: string): string {
|
||||
return mkdtempSync(join(tmpdir(), prefix));
|
||||
|
||||
@@ -6,10 +6,12 @@ import { tmpdir } from "node:os";
|
||||
import { TaskStore, resolveTaskGithubTracking, setTaskCreatedHook } from "@fusion/core";
|
||||
import { createDelegateTaskTool, createTaskCreateTool } from "../agent-tools.js";
|
||||
|
||||
const githubTrackingHookEntry = "../../../dashboard/src/github-tracking-hook.js";
|
||||
const githubTrackingEntry = "../../../dashboard/src/github-tracking.js";
|
||||
const githubTrackingHookModulePromise: Promise<any> = import(/* @vite-ignore */ githubTrackingHookEntry);
|
||||
const githubTrackingModulePromise: Promise<any> = import(/* @vite-ignore */ githubTrackingEntry);
|
||||
const githubTrackingHookModulePromise: Promise<any> = import(
|
||||
new URL("../../../dashboard/src/github-tracking-hook.js", import.meta.url).href
|
||||
);
|
||||
const githubTrackingModulePromise: Promise<any> = import(
|
||||
new URL("../../../dashboard/src/github-tracking.js", import.meta.url).href
|
||||
);
|
||||
|
||||
function makeTmpDir(prefix: string): string {
|
||||
return mkdtempSync(join(tmpdir(), prefix));
|
||||
|
||||
@@ -2065,42 +2065,44 @@ describe("StepSessionExecutor integration", () => {
|
||||
it("persists tokenUsage incrementally during step execution before in-review transition", async () => {
|
||||
const { store } = createTokenUsageStepSessionStore();
|
||||
|
||||
mockedStepSessionExecutor.mockImplementationOnce(((options: any) => ({
|
||||
executeAll: vi.fn(async () => {
|
||||
options.onStepComplete(0, {
|
||||
stepIndex: 0,
|
||||
success: true,
|
||||
retries: 0,
|
||||
tokenUsage: { inputTokens: 20, outputTokens: 10, cachedTokens: 2, totalTokens: 32 },
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
options.onStepComplete(1, {
|
||||
stepIndex: 1,
|
||||
success: true,
|
||||
retries: 0,
|
||||
tokenUsage: { inputTokens: 30, outputTokens: 5, cachedTokens: 1, totalTokens: 36 },
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
return [
|
||||
{
|
||||
mockedStepSessionExecutor.mockImplementationOnce(function (options: any) {
|
||||
return {
|
||||
executeAll: vi.fn(async () => {
|
||||
options.onStepComplete(0, {
|
||||
stepIndex: 0,
|
||||
success: true,
|
||||
retries: 0,
|
||||
tokenUsage: { inputTokens: 20, outputTokens: 10, cachedTokens: 2, totalTokens: 32 },
|
||||
},
|
||||
{
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
options.onStepComplete(1, {
|
||||
stepIndex: 1,
|
||||
success: true,
|
||||
retries: 0,
|
||||
tokenUsage: { inputTokens: 30, outputTokens: 5, cachedTokens: 1, totalTokens: 36 },
|
||||
},
|
||||
];
|
||||
}),
|
||||
terminateAllSessions: mockTerminateAllSessions,
|
||||
cleanup: mockCleanup,
|
||||
})) as any);
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
return [
|
||||
{
|
||||
stepIndex: 0,
|
||||
success: true,
|
||||
retries: 0,
|
||||
tokenUsage: { inputTokens: 20, outputTokens: 10, cachedTokens: 2, totalTokens: 32 },
|
||||
},
|
||||
{
|
||||
stepIndex: 1,
|
||||
success: true,
|
||||
retries: 0,
|
||||
tokenUsage: { inputTokens: 30, outputTokens: 5, cachedTokens: 1, totalTokens: 36 },
|
||||
},
|
||||
];
|
||||
}),
|
||||
terminateAllSessions: mockTerminateAllSessions,
|
||||
cleanup: mockCleanup,
|
||||
};
|
||||
} as any);
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test", {});
|
||||
await executor.execute(createTaskWithSteps());
|
||||
@@ -2831,4 +2833,3 @@ describe("FN-5256 awaitAbortInFlightTaskWork pause synchronization", () => {
|
||||
expect((executor as any).activeSessions.has("FN-PAUSE-3")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ describe("FN-5241 executor handoff auditing", () => {
|
||||
const handoff = store.getRunAuditEvents({ taskId: task.id, mutationType: "task:handoff", limit: 10 })[0];
|
||||
expect(handoff?.metadata).toMatchObject({
|
||||
taskId: task.id,
|
||||
reason: "fn_task_done",
|
||||
reason: "workflow-graph-review",
|
||||
alreadyEnqueued: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -280,7 +280,7 @@ describe("per-agent heartbeat config", () => {
|
||||
const config = await monitor.getAgentHeartbeatConfig("agent-001");
|
||||
expect(config.pollIntervalMs).toBe(60_000);
|
||||
expect(config.heartbeatTimeoutMs).toBe(30_000);
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -571,4 +571,3 @@ describe("per-agent heartbeat config", () => {
|
||||
});
|
||||
|
||||
// ── Heartbeat Execution Tests ──────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ const nodeHealthState = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("../project-manager.js", () => ({
|
||||
ProjectManager: vi.fn().mockImplementation(() => ({
|
||||
ProjectManager: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
on: vi.fn(),
|
||||
addProject: vi.fn().mockImplementation(async ({ projectId }: { projectId: string }) => {
|
||||
projectManagerState.projectIds.push(projectId);
|
||||
@@ -39,11 +40,13 @@ vi.mock("../project-manager.js", () => ({
|
||||
releaseGlobalSlot: vi.fn().mockResolvedValue(undefined),
|
||||
removeProject: vi.fn().mockResolvedValue(undefined),
|
||||
stopAll: projectManagerState.stopAll,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../node-health-monitor.js", () => ({
|
||||
NodeHealthMonitor: vi.fn().mockImplementation((centralCore: CentralCore) => ({
|
||||
NodeHealthMonitor: vi.fn().mockImplementation(function (centralCore: CentralCore) {
|
||||
return {
|
||||
start: vi.fn().mockImplementation(async () => {
|
||||
const nodes = await centralCore.listNodes();
|
||||
nodeHealthState.nodes.clear();
|
||||
@@ -53,7 +56,8 @@ vi.mock("../node-health-monitor.js", () => ({
|
||||
}),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getNodeHealth: vi.fn().mockImplementation((nodeId: string) => nodeHealthState.nodes.get(nodeId)),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("HybridExecutor multi-node routing", () => {
|
||||
|
||||
@@ -9,21 +9,25 @@ const projectManagerState = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("../project-manager.js", () => ({
|
||||
ProjectManager: vi.fn().mockImplementation(() => ({
|
||||
ProjectManager: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
on: vi.fn(),
|
||||
addProject: vi.fn().mockImplementation(async (config: { projectId: string }) => {
|
||||
projectManagerState.projectIds.push(config.projectId);
|
||||
}),
|
||||
getProjectIds: vi.fn().mockImplementation(() => [...projectManagerState.projectIds]),
|
||||
stopAll: projectManagerState.stopAll,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../node-health-monitor.js", () => ({
|
||||
NodeHealthMonitor: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
NodeHealthMonitor: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
function createCentralCore(overrides?: {
|
||||
|
||||
@@ -22,7 +22,7 @@ const mockProjectManagerInstances: Array<{
|
||||
}> = [];
|
||||
|
||||
vi.mock("../project-manager.js", () => ({
|
||||
ProjectManager: vi.fn().mockImplementation(() => {
|
||||
ProjectManager: vi.fn().mockImplementation(function () {
|
||||
const instance = {
|
||||
addProject: vi.fn().mockImplementation((config: ProjectRuntimeConfig) => {
|
||||
const runtime = {
|
||||
@@ -93,7 +93,7 @@ const mockNodeHealthMonitorInstances: Array<{
|
||||
}> = [];
|
||||
|
||||
vi.mock("../node-health-monitor.js", () => ({
|
||||
NodeHealthMonitor: vi.fn().mockImplementation(() => {
|
||||
NodeHealthMonitor: vi.fn().mockImplementation(function () {
|
||||
const instance = {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("interpreter merge seam", () => {
|
||||
const fakeEngine = fakeEngineWith({ autoEligible: true, onMerge });
|
||||
const result = await (ProjectEngine.prototype as any).requestInterpreterMerge.call(fakeEngine, "FN-3");
|
||||
|
||||
expect(onMerge).toHaveBeenCalledWith("FN-3");
|
||||
expect(onMerge).toHaveBeenCalledWith("FN-3", {});
|
||||
expect(result.merged).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -25,17 +25,19 @@ vi.mock("../merger.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../runtimes/in-process-runtime.js", () => ({
|
||||
InProcessRuntime: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(async () => undefined),
|
||||
stop: vi.fn(async () => undefined),
|
||||
getTaskStore: () => testState.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
})),
|
||||
InProcessRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(async () => undefined),
|
||||
stop: vi.fn(async () => undefined),
|
||||
getTaskStore: () => testState.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
import { ProjectEngine } from "../project-engine.js";
|
||||
|
||||
@@ -43,10 +43,12 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
|
||||
createFindTool: vi.fn(() => ({ name: "find" })),
|
||||
createLsTool: vi.fn(() => ({ name: "ls" })),
|
||||
createExtensionRuntime: vi.fn(),
|
||||
DefaultResourceLoader: vi.fn().mockImplementation(() => ({
|
||||
reload: vi.fn().mockResolvedValue(undefined),
|
||||
skillsOverride: undefined,
|
||||
})),
|
||||
DefaultResourceLoader: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
reload: vi.fn().mockResolvedValue(undefined),
|
||||
skillsOverride: undefined,
|
||||
};
|
||||
}),
|
||||
DefaultPackageManager: vi.fn(),
|
||||
discoverAndLoadExtensions: vi.fn().mockResolvedValue({ errors: [], runtime: { pendingProviderRegistrations: [] } }),
|
||||
getAgentDir: vi.fn(() => "/test/agent-dir"),
|
||||
|
||||
@@ -15,21 +15,23 @@ vi.mock("../engine-singleton-lock.js", () => ({
|
||||
vi.mock("../project-engine.js", () => {
|
||||
return {
|
||||
|
||||
ProjectEngine: vi.fn().mockImplementation((config: any) => ({
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
getHeartbeatMonitor: vi.fn().mockReturnValue(undefined),
|
||||
getHeartbeatTriggerScheduler: vi.fn().mockReturnValue(undefined),
|
||||
getAutomationStore: vi.fn().mockReturnValue(undefined),
|
||||
getRuntime: vi.fn().mockReturnValue({
|
||||
getMissionAutopilot: vi.fn().mockReturnValue(undefined),
|
||||
getMissionExecutionLoop: vi.fn().mockReturnValue(undefined),
|
||||
}),
|
||||
getWorkingDirectory: vi.fn().mockReturnValue(config.workingDirectory),
|
||||
onMerge: vi.fn().mockResolvedValue(undefined),
|
||||
_config: config,
|
||||
})),
|
||||
ProjectEngine: vi.fn().mockImplementation(function (config: any) {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
getHeartbeatMonitor: vi.fn().mockReturnValue(undefined),
|
||||
getHeartbeatTriggerScheduler: vi.fn().mockReturnValue(undefined),
|
||||
getAutomationStore: vi.fn().mockReturnValue(undefined),
|
||||
getRuntime: vi.fn().mockReturnValue({
|
||||
getMissionAutopilot: vi.fn().mockReturnValue(undefined),
|
||||
getMissionExecutionLoop: vi.fn().mockReturnValue(undefined),
|
||||
}),
|
||||
getWorkingDirectory: vi.fn().mockReturnValue(config.workingDirectory),
|
||||
onMerge: vi.fn().mockResolvedValue(undefined),
|
||||
_config: config,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -148,15 +150,17 @@ describe("ProjectEngineManager", () => {
|
||||
let callCount = 0;
|
||||
(ProjectEngine as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
|
||||
(config: any) => ({
|
||||
start: vi.fn().mockImplementation(async () => {
|
||||
function (config: any) {
|
||||
return {
|
||||
start: vi.fn().mockImplementation(async () => {
|
||||
callCount++;
|
||||
if (callCount === 1) throw new Error("transient failure");
|
||||
}),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
_config: config,
|
||||
}),
|
||||
}),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
_config: config,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await expect(manager.ensureEngine("proj_aaa")).rejects.toThrow("transient failure");
|
||||
@@ -554,18 +558,20 @@ describe("ProjectEngineManager", () => {
|
||||
// Make starts fail on the first 3 calls (one per project in the first tick)
|
||||
(ProjectEngine as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(config: any) => ({
|
||||
start: vi.fn().mockImplementation(async () => {
|
||||
function (config: any) {
|
||||
return {
|
||||
start: vi.fn().mockImplementation(async () => {
|
||||
startCallCount++;
|
||||
// Fail only the first 3 calls (one per project in first reconciliation tick)
|
||||
if (startCallCount <= 3) {
|
||||
throw new Error("transient failure");
|
||||
}
|
||||
}),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
_config: config,
|
||||
}),
|
||||
}),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
_config: config,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Start reconciliation (runs immediate tick which fails all 3)
|
||||
@@ -593,12 +599,14 @@ describe("ProjectEngineManager", () => {
|
||||
// Reset mock for other tests
|
||||
(ProjectEngine as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(config: any) => ({
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
_config: config,
|
||||
}),
|
||||
function (config: any) {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
_config: config,
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -639,12 +647,14 @@ describe("ProjectEngineManager", () => {
|
||||
vi.clearAllMocks();
|
||||
(ProjectEngine as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(config: any) => ({
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
_config: config,
|
||||
}),
|
||||
function (config: any) {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getTaskStore: vi.fn().mockReturnValue({ projectId: config.projectId }),
|
||||
_config: config,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Advance more time - no new engines should be started
|
||||
|
||||
@@ -22,40 +22,42 @@ vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
return { ...actual, execFile: mocks.execFile };
|
||||
});
|
||||
vi.mock("../pr-monitor.js", () => ({ PrMonitor: vi.fn().mockImplementation(() => ({ onNewComments: vi.fn() })) }));
|
||||
vi.mock("../pr-comment-handler.js", () => ({ PrCommentHandler: vi.fn().mockImplementation(() => ({ handleNewComments: vi.fn() })) }));
|
||||
vi.mock("../pr-monitor.js", () => ({ PrMonitor: vi.fn().mockImplementation(function () { return { onNewComments: vi.fn() }; }) }));
|
||||
vi.mock("../pr-comment-handler.js", () => ({ PrCommentHandler: vi.fn().mockImplementation(function () { return { handleNewComments: vi.fn() }; }) }));
|
||||
vi.mock("../auth-storage.js", () => ({
|
||||
createFusionAuthStorage: vi.fn(() => ({ reload: vi.fn(), getOAuthProviders: vi.fn(() => []), get: vi.fn(() => undefined) })),
|
||||
getFusionOAuthAlertStatePath: vi.fn(() => "/tmp/oauth-alert-state.json"),
|
||||
}));
|
||||
vi.mock("../notifier.js", () => ({ NtfyNotifier: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })) }));
|
||||
vi.mock("../notifier.js", () => ({ NtfyNotifier: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }) }));
|
||||
vi.mock("../notification/index.js", () => ({
|
||||
NotificationService: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
OAuthAlertStateStore: vi.fn().mockImplementation(() => ({})),
|
||||
OAuthExpiryMonitor: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
OAuthValidityLogger: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
NotificationService: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }),
|
||||
OAuthAlertStateStore: vi.fn().mockImplementation(function () { return {}; }),
|
||||
OAuthExpiryMonitor: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }),
|
||||
OAuthValidityLogger: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }),
|
||||
}));
|
||||
vi.mock("../cron-runner.js", () => ({
|
||||
CronRunner: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
CronRunner: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }),
|
||||
createAiPromptExecutor: vi.fn(async () => vi.fn()),
|
||||
}));
|
||||
vi.mock("../runtimes/in-process-runtime.js", () => ({
|
||||
InProcessRuntime: vi.fn().mockImplementation(() => ({
|
||||
start: mocks.runtimeStart,
|
||||
stop: mocks.runtimeStop,
|
||||
resumeAfterUnpause: mocks.runtimeResumeAfterUnpause,
|
||||
getTaskStore: () => mocks.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
configurePrMonitoring: mocks.runtimeConfigurePrMonitoring,
|
||||
setActiveMergeTaskIdProvider: vi.fn(),
|
||||
setMergeEnqueuer: vi.fn(),
|
||||
setMergeActiveClearer: vi.fn(),
|
||||
})),
|
||||
InProcessRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: mocks.runtimeStart,
|
||||
stop: mocks.runtimeStop,
|
||||
resumeAfterUnpause: mocks.runtimeResumeAfterUnpause,
|
||||
getTaskStore: () => mocks.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
configurePrMonitoring: mocks.runtimeConfigurePrMonitoring,
|
||||
setActiveMergeTaskIdProvider: vi.fn(),
|
||||
setMergeEnqueuer: vi.fn(),
|
||||
setMergeActiveClearer: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
type Listener = (...args: any[]) => void | Promise<void>;
|
||||
|
||||
@@ -51,10 +51,12 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
|
||||
vi.mock("../cron-runner.js", () => {
|
||||
return {
|
||||
CronRunner: vi.fn().mockImplementation(() => ({
|
||||
start: mocks.cronRunnerStart,
|
||||
stop: mocks.cronRunnerStop,
|
||||
})),
|
||||
CronRunner: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: mocks.cronRunnerStart,
|
||||
stop: mocks.cronRunnerStop,
|
||||
};
|
||||
}),
|
||||
createAiPromptExecutor: mocks.createAiPromptExecutor,
|
||||
};
|
||||
});
|
||||
@@ -72,40 +74,54 @@ vi.mock("node:child_process", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("../pr-monitor.js", () => ({
|
||||
PrMonitor: vi.fn().mockImplementation(() => ({
|
||||
onNewComments: vi.fn(),
|
||||
})),
|
||||
PrMonitor: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
onNewComments: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../pr-comment-handler.js", () => ({
|
||||
PrCommentHandler: vi.fn().mockImplementation(() => ({
|
||||
handleNewComments: vi.fn(),
|
||||
createFollowUpTask: mocks.prHandlerCreateFollowUpTask,
|
||||
})),
|
||||
PrCommentHandler: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
handleNewComments: vi.fn(),
|
||||
createFollowUpTask: mocks.prHandlerCreateFollowUpTask,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../notifier.js", () => ({
|
||||
NtfyNotifier: vi.fn().mockImplementation(() => ({
|
||||
start: mocks.notifierStart,
|
||||
stop: mocks.notifierStop,
|
||||
notifyGridlock: mocks.notifierNotifyGridlock,
|
||||
})),
|
||||
NtfyNotifier: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: mocks.notifierStart,
|
||||
stop: mocks.notifierStop,
|
||||
notifyGridlock: mocks.notifierNotifyGridlock,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../notification/index.js", () => ({
|
||||
NotificationService: vi.fn().mockImplementation(() => ({
|
||||
start: mocks.notificationServiceStart,
|
||||
stop: mocks.notificationServiceStop,
|
||||
})),
|
||||
OAuthAlertStateStore: vi.fn().mockImplementation(() => ({})),
|
||||
OAuthExpiryMonitor: vi.fn().mockImplementation(() => ({
|
||||
start: mocks.oauthExpiryMonitorStart,
|
||||
stop: mocks.oauthExpiryMonitorStop,
|
||||
})),
|
||||
OAuthValidityLogger: vi.fn().mockImplementation(() => ({
|
||||
start: mocks.oauthValidityLoggerStart,
|
||||
stop: mocks.oauthValidityLoggerStop,
|
||||
})),
|
||||
NotificationService: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: mocks.notificationServiceStart,
|
||||
stop: mocks.notificationServiceStop,
|
||||
};
|
||||
}),
|
||||
OAuthAlertStateStore: vi.fn().mockImplementation(function () {
|
||||
return {};
|
||||
}),
|
||||
OAuthExpiryMonitor: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: mocks.oauthExpiryMonitorStart,
|
||||
stop: mocks.oauthExpiryMonitorStop,
|
||||
};
|
||||
}),
|
||||
OAuthValidityLogger: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: mocks.oauthValidityLoggerStart,
|
||||
stop: mocks.oauthValidityLoggerStop,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../auth-storage.js", () => ({
|
||||
@@ -118,19 +134,21 @@ vi.mock("../auth-storage.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../runtimes/in-process-runtime.js", () => ({
|
||||
InProcessRuntime: vi.fn().mockImplementation(() => ({
|
||||
start: mocks.runtimeStart,
|
||||
stop: mocks.runtimeStop,
|
||||
resumeAfterUnpause: mocks.runtimeResumeAfterUnpause,
|
||||
getTaskStore: () => mocks.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
configurePrMonitoring: mocks.runtimeConfigurePrMonitoring,
|
||||
})),
|
||||
InProcessRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: mocks.runtimeStart,
|
||||
stop: mocks.runtimeStop,
|
||||
resumeAfterUnpause: mocks.runtimeResumeAfterUnpause,
|
||||
getTaskStore: () => mocks.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
configurePrMonitoring: mocks.runtimeConfigurePrMonitoring,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
type SettingsHandlerPayload = {
|
||||
|
||||
@@ -8,7 +8,8 @@ import type { ProjectRuntimeConfig } from "../project-runtime.js";
|
||||
|
||||
// Mock the runtimes
|
||||
vi.mock("../runtimes/in-process-runtime.js", () => ({
|
||||
InProcessRuntime: vi.fn().mockImplementation(() => ({
|
||||
InProcessRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getStatus: vi.fn().mockReturnValue("active"),
|
||||
@@ -20,11 +21,13 @@ vi.mock("../runtimes/in-process-runtime.js", () => ({
|
||||
lastActivityAt: new Date().toISOString(),
|
||||
}),
|
||||
on: vi.fn().mockReturnThis(),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../runtimes/child-process-runtime.js", () => ({
|
||||
ChildProcessRuntime: vi.fn().mockImplementation(() => ({
|
||||
ChildProcessRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getStatus: vi.fn().mockReturnValue("active"),
|
||||
@@ -40,11 +43,13 @@ vi.mock("../runtimes/child-process-runtime.js", () => ({
|
||||
lastActivityAt: new Date().toISOString(),
|
||||
}),
|
||||
on: vi.fn().mockReturnThis(),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../runtimes/remote-node-runtime.js", () => ({
|
||||
RemoteNodeRuntime: vi.fn().mockImplementation(() => ({
|
||||
RemoteNodeRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getStatus: vi.fn().mockReturnValue("active"),
|
||||
@@ -60,7 +65,8 @@ vi.mock("../runtimes/remote-node-runtime.js", () => ({
|
||||
lastActivityAt: new Date().toISOString(),
|
||||
}),
|
||||
on: vi.fn().mockReturnThis(),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("ProjectManager", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
const execMock = vi.fn();
|
||||
@@ -43,6 +43,10 @@ describe("reliability interactions: live-zero reclaim", () => {
|
||||
execMock.mockResolvedValue("");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("restart recovery and reclaim sweep converge to todo + null worktree for live-zero case", async () => {
|
||||
const taskState: any = {
|
||||
id: "FN-9100",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
const execMock = vi.fn();
|
||||
@@ -42,6 +42,10 @@ describe("reliability interactions: stale cached-base branch reclaim", () => {
|
||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("restart recovery + reclaim sweep ends with todo and nulled cached branch metadata", async () => {
|
||||
const task: any = { id: "FN-9001", column: "in-review", checkedOutBy: null, branch: "fusion/fn-9001", worktree: "/tmp/ghost", baseCommitSha: "stale-base", paused: true, pausedReason: "branch-conflict-unrecoverable", error: "Agent exited without calling fn_task_done", status: "failed", steps: [{ status: "pending" }] };
|
||||
const statefulStore: any = createStore();
|
||||
|
||||
@@ -9,12 +9,14 @@ const mockStreamEvents = vi.hoisted(() => vi.fn());
|
||||
const mockPollPendingAssignments = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../runtimes/remote-node-client.js", () => ({
|
||||
RemoteNodeClient: vi.fn().mockImplementation(() => ({
|
||||
health: mockHealth,
|
||||
getMetrics: mockGetMetrics,
|
||||
streamEvents: mockStreamEvents,
|
||||
pollPendingAssignments: mockPollPendingAssignments,
|
||||
})),
|
||||
RemoteNodeClient: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
health: mockHealth,
|
||||
getMetrics: mockGetMetrics,
|
||||
streamEvents: mockStreamEvents,
|
||||
pollPendingAssignments: mockPollPendingAssignments,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
const NOW = "2026-05-16T00:00:00.000Z";
|
||||
|
||||
@@ -3,13 +3,15 @@ import { describe, expect, it } from "vitest";
|
||||
// This contract test fails loudly if dashboard denial-reason enum values change without updating cross-node consumers.
|
||||
describe("reliability: node settings sync auth denial-reason contract", () => {
|
||||
it("pins the SyncStatusDenialReason enum values consumed by cross-node sync-status callers", async () => {
|
||||
const helpersModulePath = "../../../../dashboard/src/routes/register-settings-sync-helpers.js";
|
||||
const apiErrorModulePath = "../../../../dashboard/src/api-error.js";
|
||||
const { SYNC_STATUS_DENIAL_REASONS, classifySyncStatusDenialReason } = await import(helpersModulePath) as {
|
||||
const { SYNC_STATUS_DENIAL_REASONS, classifySyncStatusDenialReason } = await import(
|
||||
new URL("../../../../dashboard/src/routes/register-settings-sync-helpers.js", import.meta.url).href
|
||||
) as {
|
||||
SYNC_STATUS_DENIAL_REASONS: readonly string[];
|
||||
classifySyncStatusDenialReason: (err: unknown) => string;
|
||||
};
|
||||
const { ApiError } = await import(apiErrorModulePath) as {
|
||||
const { ApiError } = await import(
|
||||
new URL("../../../../dashboard/src/api-error.js", import.meta.url).href
|
||||
) as {
|
||||
ApiError: new (status: number, message: string) => Error;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,17 +16,19 @@ vi.mock("../../merger.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("../../runtimes/in-process-runtime.js", () => ({
|
||||
InProcessRuntime: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(async () => undefined),
|
||||
stop: vi.fn(async () => undefined),
|
||||
getTaskStore: () => testState.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
})),
|
||||
InProcessRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(async () => undefined),
|
||||
stop: vi.fn(async () => undefined),
|
||||
getTaskStore: () => testState.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
import { ProjectEngine } from "../../project-engine.js";
|
||||
|
||||
@@ -22,17 +22,19 @@ vi.mock("../../merger.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("../../runtimes/in-process-runtime.js", () => ({
|
||||
InProcessRuntime: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(async () => undefined),
|
||||
stop: vi.fn(async () => undefined),
|
||||
getTaskStore: () => testState.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
})),
|
||||
InProcessRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(async () => undefined),
|
||||
stop: vi.fn(async () => undefined),
|
||||
getTaskStore: () => testState.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
import { ProjectEngine } from "../../project-engine.js";
|
||||
|
||||
@@ -25,40 +25,42 @@ vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
return { ...actual, execFile: projectEngineMocks.execFile };
|
||||
});
|
||||
vi.mock("../../pr-monitor.js", () => ({ PrMonitor: vi.fn().mockImplementation(() => ({ onNewComments: vi.fn() })) }));
|
||||
vi.mock("../../pr-comment-handler.js", () => ({ PrCommentHandler: vi.fn().mockImplementation(() => ({ handleNewComments: vi.fn() })) }));
|
||||
vi.mock("../../pr-monitor.js", () => ({ PrMonitor: vi.fn().mockImplementation(function () { return { onNewComments: vi.fn() }; }) }));
|
||||
vi.mock("../../pr-comment-handler.js", () => ({ PrCommentHandler: vi.fn().mockImplementation(function () { return { handleNewComments: vi.fn() }; }) }));
|
||||
vi.mock("../../auth-storage.js", () => ({
|
||||
createFusionAuthStorage: vi.fn(() => ({ reload: vi.fn(), getOAuthProviders: vi.fn(() => []), get: vi.fn(() => undefined) })),
|
||||
getFusionOAuthAlertStatePath: vi.fn(() => "/tmp/oauth-alert-state.json"),
|
||||
}));
|
||||
vi.mock("../../notifier.js", () => ({ NtfyNotifier: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })) }));
|
||||
vi.mock("../../notifier.js", () => ({ NtfyNotifier: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }) }));
|
||||
vi.mock("../../notification/index.js", () => ({
|
||||
NotificationService: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
OAuthAlertStateStore: vi.fn().mockImplementation(() => ({})),
|
||||
OAuthExpiryMonitor: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
OAuthValidityLogger: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
NotificationService: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }),
|
||||
OAuthAlertStateStore: vi.fn().mockImplementation(function () { return {}; }),
|
||||
OAuthExpiryMonitor: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }),
|
||||
OAuthValidityLogger: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }),
|
||||
}));
|
||||
vi.mock("../../cron-runner.js", () => ({
|
||||
CronRunner: vi.fn().mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() })),
|
||||
CronRunner: vi.fn().mockImplementation(function () { return { start: vi.fn(), stop: vi.fn() }; }),
|
||||
createAiPromptExecutor: vi.fn(async () => vi.fn()),
|
||||
}));
|
||||
vi.mock("../../runtimes/in-process-runtime.js", () => ({
|
||||
InProcessRuntime: vi.fn().mockImplementation(() => ({
|
||||
start: projectEngineMocks.runtimeStart,
|
||||
stop: projectEngineMocks.runtimeStop,
|
||||
resumeAfterUnpause: projectEngineMocks.runtimeResumeAfterUnpause,
|
||||
getTaskStore: () => projectEngineMocks.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
configurePrMonitoring: projectEngineMocks.runtimeConfigurePrMonitoring,
|
||||
setActiveMergeTaskIdProvider: vi.fn(),
|
||||
setMergeEnqueuer: vi.fn(),
|
||||
setMergeActiveClearer: vi.fn(),
|
||||
})),
|
||||
InProcessRuntime: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: projectEngineMocks.runtimeStart,
|
||||
stop: projectEngineMocks.runtimeStop,
|
||||
resumeAfterUnpause: projectEngineMocks.runtimeResumeAfterUnpause,
|
||||
getTaskStore: () => projectEngineMocks.currentStore,
|
||||
getAgentStore: vi.fn(),
|
||||
getMessageStore: vi.fn(),
|
||||
getRoutineStore: vi.fn(),
|
||||
getRoutineRunner: vi.fn(),
|
||||
getHeartbeatMonitor: vi.fn(),
|
||||
getTriggerScheduler: vi.fn(),
|
||||
configurePrMonitoring: projectEngineMocks.runtimeConfigurePrMonitoring,
|
||||
setActiveMergeTaskIdProvider: vi.fn(),
|
||||
setMergeEnqueuer: vi.fn(),
|
||||
setMergeActiveClearer: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
type Listener = (...args: any[]) => void | Promise<void>;
|
||||
|
||||
@@ -1411,6 +1411,34 @@ describe("Scheduler", () => {
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("caps in-progress dispatch by the global semaphore limit even before executors acquire slots", async () => {
|
||||
vi.mocked(existsSync).mockReturnValue(true);
|
||||
vi.mocked(readFile).mockResolvedValue("# Task\nDo something");
|
||||
|
||||
const semaphore = new AgentSemaphore(3);
|
||||
const tasks = [
|
||||
createMockTask({ id: "FN-001", column: "in-progress" }),
|
||||
createMockTask({ id: "FN-002", column: "in-progress" }),
|
||||
createMockTask({ id: "FN-003", column: "in-progress" }),
|
||||
createMockTask({ id: "FN-004", column: "todo", dependencies: [] }),
|
||||
createMockTask({ id: "FN-005", column: "todo", dependencies: [] }),
|
||||
];
|
||||
|
||||
const store = createMockStore({
|
||||
listTasks: vi.fn().mockResolvedValue(tasks),
|
||||
getTask: vi.fn(async (taskId: string) => tasks.find((task) => task.id === taskId) ?? null),
|
||||
getSettings: vi.fn().mockResolvedValue({ maxConcurrent: 5, maxWorktrees: 10 }),
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
moveTask: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { semaphore });
|
||||
scheduler.start();
|
||||
await scheduler.schedule();
|
||||
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("respects maxWorktrees limit", async () => {
|
||||
const tasks = [
|
||||
createMockTask({ id: "FN-001", column: "in-progress" }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { SelfHealingManager } from "../self-healing.js";
|
||||
import * as branchConflicts from "../branch-conflicts.js";
|
||||
@@ -29,6 +29,10 @@ describe("self-healing reclaim paused review", () => {
|
||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("reclaims paused in-review branch conflict, clears paused state, and requeues to todo with audit metadata", async () => {
|
||||
(store.listTasks as any)
|
||||
.mockResolvedValueOnce([])
|
||||
|
||||
@@ -137,7 +137,7 @@ vi.mock("../../worktree-pool.js", async () => {
|
||||
// Mock the scheduler
|
||||
vi.mock("../../scheduler.js", async () => {
|
||||
return {
|
||||
Scheduler: vi.fn().mockImplementation(() => {
|
||||
Scheduler: vi.fn().mockImplementation(function () {
|
||||
const self = {} as Record<string, unknown>;
|
||||
self.start = vi.fn();
|
||||
self.stop = vi.fn();
|
||||
@@ -150,7 +150,7 @@ vi.mock("../../scheduler.js", async () => {
|
||||
|
||||
vi.mock("../../self-healing.js", async () => {
|
||||
return {
|
||||
SelfHealingManager: vi.fn().mockImplementation((_store, opts) => {
|
||||
SelfHealingManager: vi.fn().mockImplementation(function (_store, opts) {
|
||||
mockSelfHealingCtor(opts);
|
||||
return {
|
||||
start: mockSelfHealingStart,
|
||||
@@ -164,28 +164,32 @@ vi.mock("../../self-healing.js", async () => {
|
||||
|
||||
vi.mock("../../restart-recovery-coordinator.js", async () => {
|
||||
return {
|
||||
RestartRecoveryCoordinator: vi.fn().mockImplementation(() => ({
|
||||
recoverInterruptedRuns: mockRecoverInterruptedRuns,
|
||||
})),
|
||||
RestartRecoveryCoordinator: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
recoverInterruptedRuns: mockRecoverInterruptedRuns,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the plugin runner
|
||||
vi.mock("../../plugin-runner.js", async () => {
|
||||
return {
|
||||
PluginRunner: vi.fn().mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
shutdown: vi.fn().mockResolvedValue(undefined),
|
||||
getPluginTools: vi.fn().mockReturnValue([]),
|
||||
getPluginRoutes: vi.fn().mockReturnValue([]),
|
||||
})),
|
||||
PluginRunner: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
shutdown: vi.fn().mockResolvedValue(undefined),
|
||||
getPluginTools: vi.fn().mockReturnValue([]),
|
||||
getPluginRoutes: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the executor
|
||||
vi.mock("../../executor.js", async () => {
|
||||
return {
|
||||
TaskExecutor: vi.fn().mockImplementation((_store, _rootDir, options) => {
|
||||
TaskExecutor: vi.fn().mockImplementation(function (_store, _rootDir, options) {
|
||||
mockExecutorCtor(options);
|
||||
const self = {} as Record<string, unknown>;
|
||||
self.resumeOrphaned = mockResumeOrphaned;
|
||||
|
||||
@@ -10,7 +10,7 @@ const mockStreamEvents = vi.hoisted(() => vi.fn());
|
||||
const mockPollPendingAssignments = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../remote-node-client.js", () => ({
|
||||
RemoteNodeClient: vi.fn().mockImplementation((options: unknown) => {
|
||||
RemoteNodeClient: vi.fn().mockImplementation(function (options: unknown) {
|
||||
mockClientConstructor(options);
|
||||
return {
|
||||
health: mockHealth,
|
||||
|
||||
@@ -1304,7 +1304,10 @@ export class Scheduler {
|
||||
// When a semaphore is provided, factor in its available slots so we
|
||||
// don't schedule more tasks than the global limit allows.
|
||||
const semaphoreAvailable = this.options.semaphore
|
||||
? this.options.semaphore.availableCount
|
||||
? Math.min(
|
||||
this.options.semaphore.availableCount,
|
||||
this.options.semaphore.limit - agentSlots,
|
||||
)
|
||||
: Infinity;
|
||||
|
||||
const available = Math.min(
|
||||
|
||||
Reference in New Issue
Block a user