import { Command } from "commander"; import { describe, expect, it, vi } from "vitest"; const callGatewayFromCli = vi.fn(async (method: string, _opts: unknown, params?: unknown) => { if (method === "cron.status") return { enabled: true }; return { ok: true, params }; }); vi.mock("./gateway-rpc.js", async () => { const actual = await vi.importActual("./gateway-rpc.js"); return { ...actual, callGatewayFromCli: (method: string, opts: unknown, params?: unknown, extra?: unknown) => callGatewayFromCli(method, opts, params, extra), }; }); vi.mock("../runtime.js", () => ({ defaultRuntime: { log: vi.fn(), error: vi.fn(), exit: (code: number) => { throw new Error(`__exit__:${code}`); }, }, })); describe("cron cli", () => { it("trims model and thinking on cron add", { timeout: 60_000 }, async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync( [ "cron", "add", "--name", "Daily", "--cron", "* * * * *", "--session", "isolated", "--message", "hello", "--model", " opus ", "--thinking", " low ", ], { from: "user" }, ); const addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add"); const params = addCall?.[2] as { payload?: { model?: string; thinking?: string }; }; expect(params?.payload?.model).toBe("opus"); expect(params?.payload?.thinking).toBe("low"); }); it("sends agent id on cron add", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync( [ "cron", "add", "--name", "Agent pinned", "--cron", "* * * * *", "--session", "isolated", "--message", "hi", "--agent", "ops", ], { from: "user" }, ); const addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add"); const params = addCall?.[2] as { agentId?: string }; expect(params?.agentId).toBe("ops"); }); it("omits empty model and thinking on cron edit", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync( ["cron", "edit", "job-1", "--message", "hello", "--model", " ", "--thinking", " "], { from: "user" }, ); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { model?: string; thinking?: string } }; }; expect(patch?.patch?.payload?.model).toBeUndefined(); expect(patch?.patch?.payload?.thinking).toBeUndefined(); }); it("trims model and thinking on cron edit", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync( [ "cron", "edit", "job-1", "--message", "hello", "--model", " opus ", "--thinking", " high ", ], { from: "user" }, ); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { model?: string; thinking?: string } }; }; expect(patch?.patch?.payload?.model).toBe("opus"); expect(patch?.patch?.payload?.thinking).toBe("high"); }); it("sets and clears agent id on cron edit", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync(["cron", "edit", "job-1", "--agent", " Ops ", "--message", "hello"], { from: "user", }); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { agentId?: unknown } }; expect(patch?.patch?.agentId).toBe("ops"); callGatewayFromCli.mockClear(); await program.parseAsync(["cron", "edit", "job-2", "--clear-agent"], { from: "user", }); const clearCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const clearPatch = clearCall?.[2] as { patch?: { agentId?: unknown } }; expect(clearPatch?.patch?.agentId).toBeNull(); }); it("allows model/thinking updates without --message", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync(["cron", "edit", "job-1", "--model", "opus", "--thinking", "low"], { from: "user", }); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { kind?: string; model?: string; thinking?: string } }; }; expect(patch?.patch?.payload?.kind).toBe("agentTurn"); expect(patch?.patch?.payload?.model).toBe("opus"); expect(patch?.patch?.payload?.thinking).toBe("low"); }); it("updates delivery settings without requiring --message", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync( ["cron", "edit", "job-1", "--deliver", "--channel", "telegram", "--to", "19098680"], { from: "user" }, ); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { kind?: string; message?: string; deliver?: boolean; channel?: string; to?: string; }; }; }; expect(patch?.patch?.payload?.kind).toBe("agentTurn"); expect(patch?.patch?.payload?.deliver).toBe(true); expect(patch?.patch?.payload?.channel).toBe("telegram"); expect(patch?.patch?.payload?.to).toBe("19098680"); expect(patch?.patch?.payload?.message).toBeUndefined(); }); it("supports --no-deliver on cron edit", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync(["cron", "edit", "job-1", "--no-deliver"], { from: "user" }); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { kind?: string; deliver?: boolean } }; }; expect(patch?.patch?.payload?.kind).toBe("agentTurn"); expect(patch?.patch?.payload?.deliver).toBe(false); }); it("does not include undefined delivery fields when updating message", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); // Update message without delivery flags - should NOT include undefined delivery fields await program.parseAsync(["cron", "edit", "job-1", "--message", "Updated message"], { from: "user", }); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { message?: string; deliver?: boolean; channel?: string; to?: string; bestEffortDeliver?: boolean; }; }; }; // Should include the new message expect(patch?.patch?.payload?.message).toBe("Updated message"); // Should NOT include delivery fields at all (to preserve existing values) expect(patch?.patch?.payload).not.toHaveProperty("deliver"); expect(patch?.patch?.payload).not.toHaveProperty("channel"); expect(patch?.patch?.payload).not.toHaveProperty("to"); expect(patch?.patch?.payload).not.toHaveProperty("bestEffortDeliver"); }); it("includes delivery fields when explicitly provided with message", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); // Update message AND delivery - should include both await program.parseAsync( [ "cron", "edit", "job-1", "--message", "Updated message", "--deliver", "--channel", "telegram", "--to", "19098680", ], { from: "user" }, ); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { message?: string; deliver?: boolean; channel?: string; to?: string; }; }; }; // Should include everything expect(patch?.patch?.payload?.message).toBe("Updated message"); expect(patch?.patch?.payload?.deliver).toBe(true); expect(patch?.patch?.payload?.channel).toBe("telegram"); expect(patch?.patch?.payload?.to).toBe("19098680"); }); it("includes best-effort delivery when provided with message", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync( ["cron", "edit", "job-1", "--message", "Updated message", "--best-effort-deliver"], { from: "user" }, ); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { message?: string; bestEffortDeliver?: boolean } }; }; expect(patch?.patch?.payload?.message).toBe("Updated message"); expect(patch?.patch?.payload?.bestEffortDeliver).toBe(true); }); it("includes no-best-effort delivery when provided with message", async () => { callGatewayFromCli.mockClear(); const { registerCronCli } = await import("./cron-cli.js"); const program = new Command(); program.exitOverride(); registerCronCli(program); await program.parseAsync( ["cron", "edit", "job-1", "--message", "Updated message", "--no-best-effort-deliver"], { from: "user" }, ); const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update"); const patch = updateCall?.[2] as { patch?: { payload?: { message?: string; bestEffortDeliver?: boolean } }; }; expect(patch?.patch?.payload?.message).toBe("Updated message"); expect(patch?.patch?.payload?.bestEffortDeliver).toBe(false); }); });