First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
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.");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user