- Added support for new delivery modes in cron jobs: `announce`, `deliver`, and `none`. - Updated documentation to reflect changes in delivery options and usage examples. - Enhanced the cron job schema to include delivery configuration. - Refactored related CLI commands and UI components to accommodate the new delivery settings. - Improved handling of legacy delivery fields for backward compatibility. This update allows users to choose how output from isolated jobs is delivered, enhancing flexibility in job management.
369 lines
11 KiB
TypeScript
369 lines
11 KiB
TypeScript
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<typeof import("./gateway-rpc.js")>("./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 };
|
|
delivery?: { mode?: string; channel?: string; to?: string };
|
|
};
|
|
};
|
|
|
|
expect(patch?.patch?.payload?.kind).toBe("agentTurn");
|
|
expect(patch?.patch?.delivery?.mode).toBe("deliver");
|
|
expect(patch?.patch?.delivery?.channel).toBe("telegram");
|
|
expect(patch?.patch?.delivery?.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 }; delivery?: { mode?: string } };
|
|
};
|
|
|
|
expect(patch?.patch?.payload?.kind).toBe("agentTurn");
|
|
expect(patch?.patch?.delivery?.mode).toBe("none");
|
|
});
|
|
|
|
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;
|
|
};
|
|
delivery?: unknown;
|
|
};
|
|
};
|
|
|
|
// 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");
|
|
expect(patch?.patch).not.toHaveProperty("delivery");
|
|
});
|
|
|
|
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 };
|
|
delivery?: { mode?: string; channel?: string; to?: string };
|
|
};
|
|
};
|
|
|
|
// Should include everything
|
|
expect(patch?.patch?.payload?.message).toBe("Updated message");
|
|
expect(patch?.patch?.delivery?.mode).toBe("deliver");
|
|
expect(patch?.patch?.delivery?.channel).toBe("telegram");
|
|
expect(patch?.patch?.delivery?.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);
|
|
});
|
|
});
|