mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-02 23:47:11 +02:00
- Updated isolated cron jobs to default to `announce` delivery mode, improving user experience. - Enhanced scheduling options to accept ISO 8601 timestamps for `schedule.at`, while still supporting epoch milliseconds. - Refined documentation to clarify delivery modes and scheduling formats. - Adjusted related CLI commands and UI components to reflect these changes, ensuring consistency across the platform. - Improved handling of legacy delivery fields for backward compatibility. This update streamlines the configuration of isolated jobs, making it easier for users to manage job outputs and schedules.
399 lines
12 KiB
TypeScript
399 lines
12 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("defaults isolated cron add to announce delivery", 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",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
const addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add");
|
|
const params = addCall?.[2] as { delivery?: { mode?: string } };
|
|
|
|
expect(params?.delivery?.mode).toBe("announce");
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|