test: simplify mocks

This commit is contained in:
Nicolas Meienberger 2026-04-12 16:09:05 +02:00
parent 9a242eb445
commit faf5c7f26d
No known key found for this signature in database
4 changed files with 81 additions and 82 deletions

View file

@ -1,22 +1,13 @@
import { afterEach, expect, test, vi } from "vitest"; import { afterEach, expect, test, vi } from "vitest";
import waitForExpect from "wait-for-expect"; import waitForExpect from "wait-for-expect";
import { fromPartial } from "@total-typescript/shoehorn"; import { fromAny, fromPartial } from "@total-typescript/shoehorn";
import { agentManager } from "../agents-manager"; import { agentManager, type ProcessWithAgentRuntime } from "../agents-manager";
import type { AgentManagerRuntime } from "../controller/server"; import type { AgentManagerRuntime } from "../controller/server";
import type { BackupRunPayload } from "@zerobyte/contracts/agent-protocol"; import type { BackupRunPayload } from "@zerobyte/contracts/agent-protocol";
type ProcessWithAgentRuntime = NodeJS.Process & {
__zerobyteAgentRuntime?: {
agentManager: AgentManagerRuntime | null;
localAgent: null;
isStoppingLocalAgent: boolean;
localAgentRestartTimeout: null;
};
};
const setAgentRuntime = (agentManagerRuntime: Partial<AgentManagerRuntime> | null) => { const setAgentRuntime = (agentManagerRuntime: Partial<AgentManagerRuntime> | null) => {
(process as ProcessWithAgentRuntime).__zerobyteAgentRuntime = { (process as ProcessWithAgentRuntime).__zerobyteAgentRuntime = {
agentManager: agentManagerRuntime ? fromPartial<AgentManagerRuntime>(agentManagerRuntime) : null, agentManager: fromAny(agentManagerRuntime),
localAgent: null, localAgent: null,
isStoppingLocalAgent: false, isStoppingLocalAgent: false,
localAgentRestartTimeout: null, localAgentRestartTimeout: null,

View file

@ -1,16 +1,11 @@
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import type * as childProcess from "node:child_process";
import { PassThrough } from "node:stream"; import { PassThrough } from "node:stream";
import { afterEach, expect, test, vi } from "vitest"; import { afterEach, expect, test, vi } from "vitest";
const spawnMock = vi.fn(); const spawnMock = vi.fn();
vi.mock("node:child_process", async () => { vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process"); return { spawn: spawnMock };
return {
...actual,
spawn: spawnMock,
};
}); });
const { spawnLocalAgent, stopLocalAgent } = await import("../agents-manager"); const { spawnLocalAgent, stopLocalAgent } = await import("../agents-manager");
@ -51,9 +46,7 @@ test("respawns the local agent after an unexpected exit", async () => {
const firstChild = createFakeChild(); const firstChild = createFakeChild();
const secondChild = createFakeChild(); const secondChild = createFakeChild();
spawnMock spawnMock.mockReturnValueOnce(firstChild).mockReturnValueOnce(secondChild);
.mockReturnValueOnce(firstChild as unknown as ReturnType<typeof childProcess.spawn>)
.mockReturnValueOnce(secondChild as unknown as ReturnType<typeof childProcess.spawn>);
await spawnLocalAgent(); await spawnLocalAgent();
@ -69,7 +62,7 @@ test("does not respawn the local agent after an intentional stop", async () => {
vi.useFakeTimers(); vi.useFakeTimers();
const child = createFakeChild(); const child = createFakeChild();
spawnMock.mockReturnValue(child as unknown as ReturnType<typeof childProcess.spawn>); spawnMock.mockReturnValue(child);
await spawnLocalAgent(); await spawnLocalAgent();
await stopLocalAgent(); await stopLocalAgent();

View file

@ -6,19 +6,19 @@ import { createAgentMessage } from "@zerobyte/contracts/agent-protocol";
import { createControllerAgentSession } from "../controller/session"; import { createControllerAgentSession } from "../controller/session";
const createSocket = (overrides: Partial<Parameters<typeof createControllerAgentSession>[0]> = {}) => { const createSocket = (overrides: Partial<Parameters<typeof createControllerAgentSession>[0]> = {}) => {
return fromPartial<Parameters<typeof createControllerAgentSession>[0]>({ return {
data: { id: "connection-1", agentId: "local", organizationId: null, agentName: "Local Agent" }, data: { id: "connection-1", agentId: "local", organizationId: null, agentName: "Local Agent" },
send: vi.fn(() => 1), send: vi.fn(() => 1),
close: vi.fn(), close: vi.fn(),
...overrides, ...overrides,
}); };
}; };
const createSession = (handlers: Parameters<typeof createControllerAgentSession>[1] = {}, socket = createSocket()) => { const createSession = (handlers: Parameters<typeof createControllerAgentSession>[1] = {}, socket = createSocket()) => {
const scope = Effect.runSync(Scope.make()); const scope = Effect.runSync(Scope.make());
try { try {
const session = Effect.runSync(Scope.extend(createControllerAgentSession(socket, handlers), scope)); const session = Effect.runSync(Scope.extend(createControllerAgentSession(fromPartial(socket), handlers), scope));
return { return {
session, session,
@ -57,12 +57,12 @@ test("close emits a synthetic backup.cancelled for a started backup", () => {
close(); close();
expect(onBackupCancelled).toHaveBeenCalledTimes(1); expect(onBackupCancelled).toHaveBeenCalledTimes(1);
expect(onBackupCancelled).toHaveBeenCalledWith({ expect(onBackupCancelled).toHaveBeenCalledWith(
jobId: "job-1", expect.objectContaining({
scheduleId: "schedule-1", jobId: "job-1",
message: scheduleId: "schedule-1",
"The connection to the backup agent was lost while this backup was running. Restart the backup to ensure it completes.", }),
}); );
}); });
test.each([ test.each([
@ -150,12 +150,12 @@ test("close emits a synthetic backup.cancelled for a queued backup", () => {
close(); close();
expect(onBackupCancelled).toHaveBeenCalledTimes(1); expect(onBackupCancelled).toHaveBeenCalledTimes(1);
expect(onBackupCancelled).toHaveBeenCalledWith({ expect(onBackupCancelled).toHaveBeenLastCalledWith(
jobId: "job-queued", expect.objectContaining({
scheduleId: "schedule-queued", jobId: "job-queued",
message: scheduleId: "schedule-queued",
"The connection to the backup agent was lost before this backup started. Restart the backup to ensure it completes.", }),
}); );
}); });
test("a dropped backup.cancel closes the session and emits a synthetic backup.cancelled", async () => { test("a dropped backup.cancel closes the session and emits a synthetic backup.cancelled", async () => {
@ -185,12 +185,12 @@ test("a dropped backup.cancel closes the session and emits a synthetic backup.ca
expect(send).toHaveBeenCalledTimes(1); expect(send).toHaveBeenCalledTimes(1);
expect(socket.close).toHaveBeenCalledTimes(1); expect(socket.close).toHaveBeenCalledTimes(1);
expect(onBackupCancelled).toHaveBeenCalledTimes(1); expect(onBackupCancelled).toHaveBeenCalledTimes(1);
expect(onBackupCancelled).toHaveBeenCalledWith({ expect(onBackupCancelled).toHaveBeenCalledWith(
jobId: "job-1", expect.objectContaining({
scheduleId: "schedule-1", jobId: "job-1",
message: scheduleId: "schedule-1",
"The connection to the backup agent was lost while this backup was running. Restart the backup to ensure it completes.", }),
}); );
}); });
} finally { } finally {
await closeAsync(); await closeAsync();

View file

@ -19,38 +19,6 @@ export type AgentRunBackupRequest = {
onProgress: (progress: BackupExecutionProgress) => void; onProgress: (progress: BackupExecutionProgress) => void;
}; };
type AgentRuntimeState = {
agentManager: AgentManagerRuntime | null;
localAgent: ChildProcess | null;
isStoppingLocalAgent: boolean;
localAgentRestartTimeout: ReturnType<typeof setTimeout> | null;
};
type ProcessWithAgentRuntime = NodeJS.Process & {
__zerobyteAgentRuntime?: AgentRuntimeState;
};
const getAgentRuntimeState = () => {
const runtimeProcess = process as ProcessWithAgentRuntime;
const existingRuntime = runtimeProcess.__zerobyteAgentRuntime;
if (existingRuntime) {
return existingRuntime;
}
const runtime = {
agentManager: null,
localAgent: null,
isStoppingLocalAgent: false,
localAgentRestartTimeout: null,
};
runtimeProcess.__zerobyteAgentRuntime = runtime;
return runtime;
};
const getAgentManagerRuntime = () => getAgentRuntimeState().agentManager;
type ActiveBackupRun = { type ActiveBackupRun = {
scheduleId: number; scheduleId: number;
jobId: string; jobId: string;
@ -60,8 +28,53 @@ type ActiveBackupRun = {
cancellationRequested: boolean; cancellationRequested: boolean;
}; };
const activeBackupsByScheduleId = new Map<number, ActiveBackupRun>(); type AgentRuntimeState = {
const activeBackupScheduleIdsByJobId = new Map<string, number>(); agentManager: AgentManagerRuntime | null;
localAgent: ChildProcess | null;
isStoppingLocalAgent: boolean;
localAgentRestartTimeout: ReturnType<typeof setTimeout> | null;
activeBackupsByScheduleId: Map<number, ActiveBackupRun>;
activeBackupScheduleIdsByJobId: Map<string, number>;
};
type LegacyAgentRuntimeState = Omit<AgentRuntimeState, "activeBackupsByScheduleId" | "activeBackupScheduleIdsByJobId"> &
Partial<Pick<AgentRuntimeState, "activeBackupsByScheduleId" | "activeBackupScheduleIdsByJobId">>;
export type ProcessWithAgentRuntime = NodeJS.Process & {
__zerobyteAgentRuntime?: LegacyAgentRuntimeState;
};
const getAgentRuntimeState = () => {
const runtimeProcess = process as ProcessWithAgentRuntime;
const existingRuntime = runtimeProcess.__zerobyteAgentRuntime;
if (existingRuntime) {
const runtime: AgentRuntimeState = {
...existingRuntime,
activeBackupsByScheduleId: existingRuntime.activeBackupsByScheduleId ?? new Map<number, ActiveBackupRun>(),
activeBackupScheduleIdsByJobId: existingRuntime.activeBackupScheduleIdsByJobId ?? new Map<string, number>(),
};
runtimeProcess.__zerobyteAgentRuntime = runtime;
return runtime;
}
const runtime: AgentRuntimeState = {
agentManager: null,
localAgent: null,
isStoppingLocalAgent: false,
localAgentRestartTimeout: null,
activeBackupsByScheduleId: new Map(),
activeBackupScheduleIdsByJobId: new Map(),
};
runtimeProcess.__zerobyteAgentRuntime = runtime;
return runtime;
};
const getAgentManagerRuntime = () => getAgentRuntimeState().agentManager;
const getActiveBackupsByScheduleId = () => getAgentRuntimeState().activeBackupsByScheduleId;
const getActiveBackupScheduleIdsByJobId = () => getAgentRuntimeState().activeBackupScheduleIdsByJobId;
const getUnavailableError = (agentId: string) => { const getUnavailableError = (agentId: string) => {
if (agentId === "local") { if (agentId === "local") {
@ -72,6 +85,8 @@ const getUnavailableError = (agentId: string) => {
}; };
const clearActiveBackupRun = (scheduleId: number) => { const clearActiveBackupRun = (scheduleId: number) => {
const activeBackupsByScheduleId = getActiveBackupsByScheduleId();
const activeBackupScheduleIdsByJobId = getActiveBackupScheduleIdsByJobId();
const activeBackupRun = activeBackupsByScheduleId.get(scheduleId); const activeBackupRun = activeBackupsByScheduleId.get(scheduleId);
if (!activeBackupRun) { if (!activeBackupRun) {
return null; return null;
@ -93,13 +108,13 @@ const resolveActiveBackupRun = (scheduleId: number, result: BackupExecutionResul
}; };
const getActiveBackupRun = (jobId: string, scheduleId: string, eventName: string, agentId: string) => { const getActiveBackupRun = (jobId: string, scheduleId: string, eventName: string, agentId: string) => {
const trackedScheduleId = activeBackupScheduleIdsByJobId.get(jobId); const trackedScheduleId = getActiveBackupScheduleIdsByJobId().get(jobId);
if (trackedScheduleId === undefined) { if (trackedScheduleId === undefined) {
logger.warn(`Received ${eventName} for unknown job ${jobId} from agent ${agentId}`); logger.warn(`Received ${eventName} for unknown job ${jobId} from agent ${agentId}`);
return null; return null;
} }
const activeBackupRun = activeBackupsByScheduleId.get(trackedScheduleId); const activeBackupRun = getActiveBackupsByScheduleId().get(trackedScheduleId);
if (!activeBackupRun) { if (!activeBackupRun) {
logger.warn(`Received ${eventName} for inactive job ${jobId} from agent ${agentId}`); logger.warn(`Received ${eventName} for inactive job ${jobId} from agent ${agentId}`);
return null; return null;
@ -114,7 +129,7 @@ const getActiveBackupRun = (jobId: string, scheduleId: string, eventName: string
}; };
const requestBackupCancellation = async (agentId: string, scheduleId: number) => { const requestBackupCancellation = async (agentId: string, scheduleId: number) => {
const activeBackupRun = activeBackupsByScheduleId.get(scheduleId); const activeBackupRun = getActiveBackupsByScheduleId().get(scheduleId);
if (!activeBackupRun) { if (!activeBackupRun) {
return false; return false;
} }
@ -223,7 +238,7 @@ export const agentManager = {
} }
const completion = new Promise<BackupExecutionResult>((resolve) => { const completion = new Promise<BackupExecutionResult>((resolve) => {
activeBackupsByScheduleId.set(request.scheduleId, { getActiveBackupsByScheduleId().set(request.scheduleId, {
scheduleId: request.scheduleId, scheduleId: request.scheduleId,
jobId: request.payload.jobId, jobId: request.payload.jobId,
scheduleShortId: request.payload.scheduleId, scheduleShortId: request.payload.scheduleId,
@ -231,7 +246,7 @@ export const agentManager = {
resolve, resolve,
cancellationRequested: false, cancellationRequested: false,
}); });
activeBackupScheduleIdsByJobId.set(request.payload.jobId, request.scheduleId); getActiveBackupScheduleIdsByJobId().set(request.payload.jobId, request.scheduleId);
}); });
try { try {