Files
claw3d/tests/unit/cronGatewayClient.test.ts
T
Luke The Dev 4fa4f13558 First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
2026-03-19 23:14:04 -05:00

315 lines
10 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
createCronJob,
listCronJobs,
removeCronJob,
removeCronJobsForAgent,
removeCronJobsForAgentWithBackup,
restoreCronJobs,
runCronJobNow,
type CronJobSummary,
} from "@/lib/cron/types";
import type { GatewayClient } from "@/lib/gateway/GatewayClient";
const createListedJob = (params: {
id: string;
name: string;
agentId?: string;
updatedAtMs?: number;
}): CronJobSummary => ({
id: params.id,
name: params.name,
agentId: params.agentId,
enabled: true,
updatedAtMs: params.updatedAtMs ?? 1_700_000_000_000,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "Run checks." },
state: {},
});
describe("cron gateway client", () => {
it("lists_jobs_via_cron_list_include_disabled_true", async () => {
const client = {
call: vi.fn(async () => ({ jobs: [] })),
} as unknown as GatewayClient;
await listCronJobs(client);
expect(client.call).toHaveBeenCalledWith("cron.list", { includeDisabled: true });
});
it("runs_job_now_with_force_mode", async () => {
const client = {
call: vi.fn(async () => ({ ok: true, ran: true })),
} as unknown as GatewayClient;
await runCronJobNow(client, "job-1");
expect(client.call).toHaveBeenCalledWith("cron.run", { id: "job-1", mode: "force" });
});
it("removes_job_by_id", async () => {
const client = {
call: vi.fn(async () => ({ ok: true, removed: true })),
} as unknown as GatewayClient;
await removeCronJob(client, "job-1");
expect(client.call).toHaveBeenCalledWith("cron.remove", { id: "job-1" });
});
it("throws_when_job_id_missing_for_run_or_remove", async () => {
const client = {
call: vi.fn(async () => ({ ok: true })),
} as unknown as GatewayClient;
await expect(runCronJobNow(client, " ")).rejects.toThrow("Cron job id is required.");
await expect(removeCronJob(client, "")).rejects.toThrow("Cron job id is required.");
});
it("removes_all_jobs_for_agent", async () => {
const client = {
call: vi.fn(async (method: string, payload: { id?: string }) => {
if (method === "cron.list") {
return {
jobs: [
createListedJob({ id: "job-1", name: "Job 1", agentId: "agent-1" }),
createListedJob({ id: "job-2", name: "Job 2", agentId: "agent-2" }),
createListedJob({ id: "job-3", name: "Job 3", agentId: "agent-1" }),
],
};
}
if (method === "cron.remove") {
return { ok: true, removed: payload.id !== "job-3" };
}
throw new Error(`Unexpected method: ${method}`);
}),
} as unknown as GatewayClient;
await expect(removeCronJobsForAgent(client, "agent-1")).resolves.toBe(1);
expect(client.call).toHaveBeenCalledWith("cron.list", { includeDisabled: true });
expect(client.call).toHaveBeenCalledWith("cron.remove", { id: "job-1" });
expect(client.call).toHaveBeenCalledWith("cron.remove", { id: "job-3" });
});
it("throws_when_agent_id_missing_for_bulk_remove", async () => {
const client = {
call: vi.fn(async () => ({ jobs: [] })),
} as unknown as GatewayClient;
await expect(removeCronJobsForAgent(client, " ")).rejects.toThrow("Agent id is required.");
});
it("throws_when_any_bulk_remove_call_fails", async () => {
const client = {
call: vi.fn(async (method: string) => {
if (method === "cron.list") {
return {
jobs: [createListedJob({ id: "job-1", name: "Job 1", agentId: "agent-1" })],
};
}
if (method === "cron.remove") {
return { ok: false, removed: false };
}
throw new Error(`Unexpected method: ${method}`);
}),
} as unknown as GatewayClient;
await expect(removeCronJobsForAgent(client, "agent-1")).rejects.toThrow(
'Failed to delete cron job "Job 1" (job-1).'
);
});
it("returns_restore_inputs_when_removing_jobs_with_backup", async () => {
const client = {
call: vi.fn(async (method: string, payload: { id?: string }) => {
if (method === "cron.list") {
return {
jobs: [
createListedJob({ id: "job-1", name: "Job 1", agentId: "agent-1" }),
createListedJob({ id: "job-2", name: "Job 2", agentId: "agent-2" }),
createListedJob({ id: "job-3", name: "Job 3", agentId: "agent-1" }),
],
};
}
if (method === "cron.remove") {
return { ok: true, removed: payload.id !== "job-3" };
}
throw new Error(`Unexpected method: ${method}`);
}),
} as unknown as GatewayClient;
await expect(removeCronJobsForAgentWithBackup(client, "agent-1")).resolves.toEqual([
{
name: "Job 1",
agentId: "agent-1",
sessionKey: undefined,
description: undefined,
enabled: true,
deleteAfterRun: undefined,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "Run checks." },
delivery: undefined,
},
]);
});
it("restores_removed_jobs_when_backup_remove_fails_midway", async () => {
const client = {
call: vi.fn(async (method: string, payload: { id?: string; name?: string }) => {
if (method === "cron.list") {
return {
jobs: [
createListedJob({ id: "job-1", name: "Job 1", agentId: "agent-1" }),
createListedJob({ id: "job-2", name: "Job 2", agentId: "agent-1" }),
],
};
}
if (method === "cron.remove") {
if (payload.id === "job-1") return { ok: true, removed: true };
return { ok: false, removed: false };
}
if (method === "cron.add") {
return { id: "restored-job-1", name: payload.name };
}
throw new Error(`Unexpected method: ${method}`);
}),
} as unknown as GatewayClient;
await expect(removeCronJobsForAgentWithBackup(client, "agent-1")).rejects.toThrow(
'Failed to delete cron job "Job 2" (job-2).'
);
expect(client.call).toHaveBeenCalledWith("cron.add", {
name: "Job 1",
agentId: "agent-1",
sessionKey: undefined,
description: undefined,
enabled: true,
deleteAfterRun: undefined,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "Run checks." },
delivery: undefined,
});
});
it("restores_removed_jobs_when_remove_call_throws_midway", async () => {
const thrown = new Error("network interrupted");
const client = {
call: vi.fn(async (method: string, payload: { id?: string; name?: string }) => {
if (method === "cron.list") {
return {
jobs: [
createListedJob({ id: "job-1", name: "Job 1", agentId: "agent-1" }),
createListedJob({ id: "job-2", name: "Job 2", agentId: "agent-1" }),
],
};
}
if (method === "cron.remove") {
if (payload.id === "job-1") return { ok: true, removed: true };
throw thrown;
}
if (method === "cron.add") {
return { id: "restored-job-1", name: payload.name };
}
throw new Error(`Unexpected method: ${method}`);
}),
} as unknown as GatewayClient;
await expect(removeCronJobsForAgentWithBackup(client, "agent-1")).rejects.toBe(thrown);
expect(client.call).toHaveBeenCalledWith("cron.add", {
name: "Job 1",
agentId: "agent-1",
sessionKey: undefined,
description: undefined,
enabled: true,
deleteAfterRun: undefined,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "Run checks." },
delivery: undefined,
});
});
it("throws_actionable_error_when_restore_fails", async () => {
const client = {
call: vi.fn(async (_method: string, payload: { name?: string }) => {
if (payload.name === "Job 2") {
throw new Error("cron.add failed");
}
return { id: "job-restored", name: payload.name };
}),
} as unknown as GatewayClient;
await expect(
restoreCronJobs(client, [
{
name: "Job 1",
agentId: "agent-1",
enabled: true,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "Run checks." },
},
{
name: "Job 2",
agentId: "agent-1",
enabled: true,
schedule: { kind: "every", everyMs: 120_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "Run checks again." },
},
])
).rejects.toThrow('Failed to restore cron job "Job 2" (agent-1): cron.add failed');
});
it("creates_job_via_cron_add", async () => {
const client = {
call: vi.fn(async () => ({ id: "job-1", name: "Morning brief" })),
} as unknown as GatewayClient;
const input = {
name: "Morning brief",
agentId: "agent-1",
enabled: true,
schedule: { kind: "cron" as const, expr: "0 7 * * *", tz: "America/Chicago" },
sessionTarget: "isolated" as const,
wakeMode: "now" as const,
payload: { kind: "agentTurn" as const, message: "Summarize overnight updates." },
delivery: { mode: "announce" as const, channel: "last" },
};
await createCronJob(client, input);
expect(client.call).toHaveBeenCalledWith("cron.add", input);
});
it("throws_when_create_payload_missing_required_name", async () => {
const client = {
call: vi.fn(async () => ({ id: "job-1" })),
} as unknown as GatewayClient;
await expect(
createCronJob(client, {
name: " ",
agentId: "agent-1",
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "Run checks." },
})
).rejects.toThrow("Cron job name is required.");
});
});