test: simplify mocks
This commit is contained in:
parent
9a242eb445
commit
faf5c7f26d
4 changed files with 81 additions and 82 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue