168 lines
4.3 KiB
TypeScript
168 lines
4.3 KiB
TypeScript
import { afterEach, expect, test, vi } from "vitest";
|
|
import { Effect } from "effect";
|
|
import waitForExpect from "wait-for-expect";
|
|
import { fromPartial } from "@total-typescript/shoehorn";
|
|
import { createControllerMessage, parseAgentMessage } from "@zerobyte/contracts/agent-protocol";
|
|
import * as resticServer from "@zerobyte/core/restic/server";
|
|
import { createControllerSession } from "../controller-session";
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
test("emits backup.failed when a backup command hits a restic error", async () => {
|
|
vi.spyOn(resticServer, "createRestic").mockReturnValue(
|
|
fromPartial({
|
|
backup: () => Effect.fail("source path missing"),
|
|
}),
|
|
);
|
|
|
|
const outboundMessages: string[] = [];
|
|
const session = createControllerSession(
|
|
fromPartial({
|
|
send: (message: string) => {
|
|
outboundMessages.push(message);
|
|
},
|
|
}),
|
|
);
|
|
|
|
try {
|
|
session.onOpen();
|
|
session.onMessage(
|
|
createControllerMessage("backup.run", {
|
|
jobId: "job-1",
|
|
scheduleId: "schedule-1",
|
|
organizationId: "org-1",
|
|
volume: {
|
|
id: 1,
|
|
shortId: "volume-1",
|
|
name: "Volume 1",
|
|
config: { backend: "directory", path: "/tmp" },
|
|
createdAt: 0,
|
|
updatedAt: 0,
|
|
lastHealthCheck: 0,
|
|
type: "directory",
|
|
status: "mounted",
|
|
lastError: null,
|
|
autoRemount: true,
|
|
agentId: "local",
|
|
organizationId: "org-1",
|
|
},
|
|
repositoryConfig: {
|
|
backend: "local",
|
|
path: "/tmp/test-repository",
|
|
},
|
|
options: {
|
|
oneFileSystem: false,
|
|
excludePatterns: null,
|
|
excludeIfPresent: null,
|
|
includePaths: null,
|
|
includePatterns: null,
|
|
customResticParams: null,
|
|
compressionMode: "auto",
|
|
},
|
|
runtime: {
|
|
password: "password",
|
|
},
|
|
webhooks: { pre: null, post: null },
|
|
webhookAllowedOrigins: [],
|
|
webhookTimeoutMs: 60_000,
|
|
}),
|
|
);
|
|
|
|
await waitForExpect(() => {
|
|
const failedMessage = outboundMessages
|
|
.map((message) => parseAgentMessage(message))
|
|
.find((message) => message?.success && message.data.type === "backup.failed");
|
|
|
|
expect(failedMessage?.success).toBe(true);
|
|
if (!failedMessage || !failedMessage.success || failedMessage.data.type !== "backup.failed") {
|
|
return;
|
|
}
|
|
|
|
expect(failedMessage.data.payload).toEqual({
|
|
jobId: "job-1",
|
|
scheduleId: "schedule-1",
|
|
error: "source path missing",
|
|
errorDetails: "source path missing",
|
|
});
|
|
});
|
|
} finally {
|
|
session.close();
|
|
}
|
|
});
|
|
|
|
test("closes the websocket when an outbound send throws", async () => {
|
|
const close = vi.fn(() => undefined);
|
|
const session = createControllerSession(
|
|
fromPartial({
|
|
send: () => {
|
|
throw new Error("socket write failed");
|
|
},
|
|
close,
|
|
}),
|
|
);
|
|
|
|
try {
|
|
session.onOpen();
|
|
|
|
await waitForExpect(() => {
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
});
|
|
} finally {
|
|
session.close();
|
|
}
|
|
});
|
|
|
|
test("continues processing inbound messages after a volume command fails", async () => {
|
|
const outboundMessages: string[] = [];
|
|
const session = createControllerSession(
|
|
fromPartial({
|
|
send: (message: string) => {
|
|
outboundMessages.push(message);
|
|
},
|
|
}),
|
|
);
|
|
|
|
try {
|
|
session.onMessage(
|
|
createControllerMessage("volume.command", {
|
|
commandId: "command-1",
|
|
command: {
|
|
name: "filesystem.browse",
|
|
path: "/path/that/does/not/exist",
|
|
},
|
|
}),
|
|
);
|
|
session.onMessage(createControllerMessage("heartbeat.ping", { sentAt: 123 }));
|
|
|
|
await waitForExpect(() => {
|
|
const parsedMessages = outboundMessages.map((message) => parseAgentMessage(message));
|
|
const volumeResult = parsedMessages.find(
|
|
(message) => message?.success && message.data.type === "volume.commandResult",
|
|
);
|
|
const heartbeatPong = parsedMessages.find(
|
|
(message) => message?.success && message.data.type === "heartbeat.pong",
|
|
);
|
|
|
|
expect(volumeResult?.success).toBe(true);
|
|
if (!volumeResult || !volumeResult.success || volumeResult.data.type !== "volume.commandResult") {
|
|
return;
|
|
}
|
|
|
|
expect(volumeResult.data.payload).toEqual({
|
|
commandId: "command-1",
|
|
status: "error",
|
|
error: "ENOENT: no such file or directory, scandir '/path/that/does/not/exist'",
|
|
});
|
|
expect(heartbeatPong?.success).toBe(true);
|
|
if (!heartbeatPong || !heartbeatPong.success || heartbeatPong.data.type !== "heartbeat.pong") {
|
|
return;
|
|
}
|
|
|
|
expect(heartbeatPong.data.payload).toEqual({ sentAt: 123 });
|
|
});
|
|
} finally {
|
|
session.close();
|
|
}
|
|
});
|