mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(cron): hint after disable about list filtering disabled jobs (#78139)
* fix(cron): hint after disable about list filtering disabled jobs by default * fix(cron): use !params.enabled in disable-hint guard for oxlint compliance * docs(cron): clarify disabled jobs in list output * fix(cron): keep disable hint interactive * test(cron): use exported store snapshot helper * test(cron): create disable-list regression job via service * docs(cron): defer list default contract wording * test(cron): tighten disable list coverage Co-authored-by: Kate Stahnke <35552+kate@users.noreply.github.com> * docs(cron): document enabled-only list default Co-authored-by: Kate Stahnke <35552+kate@users.noreply.github.com> --------- Co-authored-by: Kate <35552+kate@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -371,9 +371,12 @@ For template files, keep the language instruction in the rendered prompt and ver
|
||||
## Managing jobs
|
||||
|
||||
```bash
|
||||
# List all jobs
|
||||
# List enabled jobs
|
||||
openclaw cron list
|
||||
|
||||
# Include disabled jobs
|
||||
openclaw cron list --all
|
||||
|
||||
# Get one stored job as JSON
|
||||
openclaw cron get <jobId>
|
||||
|
||||
|
||||
+1
-1
@@ -298,7 +298,7 @@ openclaw cron runs --id <job-id> --limit 50
|
||||
openclaw cron runs --id <job-id> --run-id <run-id>
|
||||
```
|
||||
|
||||
`openclaw cron list` shows all matching jobs by default. Pass `--agent <id>` to show only jobs whose effective normalized agent id matches; jobs without a stored agent id count as the configured default agent.
|
||||
`openclaw cron list` shows enabled jobs by default. Pass `--all` to include disabled jobs, or `--agent <id>` to show only jobs whose effective normalized agent id matches; jobs without a stored agent id count as the configured default agent.
|
||||
|
||||
`openclaw cron get <job-id>` returns the stored job JSON directly. Use `cron show <job-id>` when you want the human-readable view with delivery-route preview.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ vi.mock("../gateway-rpc.js", async () => {
|
||||
});
|
||||
|
||||
const { registerCronSimpleCommands } = await import("./register.cron-simple.js");
|
||||
const originalStderrIsTTY = Object.getOwnPropertyDescriptor(process.stderr, "isTTY");
|
||||
|
||||
async function runCronShow(id: string): Promise<void> {
|
||||
const cron = new Command();
|
||||
@@ -23,6 +24,28 @@ async function runCronShow(id: string): Promise<void> {
|
||||
await cron.parseAsync(["show", id, "--json"], { from: "user" });
|
||||
}
|
||||
|
||||
async function runCronToggle(command: "enable" | "disable"): Promise<void> {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerCronSimpleCommands(program);
|
||||
await program.parseAsync([command, "job-1"], { from: "user" });
|
||||
}
|
||||
|
||||
function setStderrIsTTY(value: boolean): void {
|
||||
Object.defineProperty(process.stderr, "isTTY", {
|
||||
value,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
function restoreStderrIsTTY(): void {
|
||||
if (originalStderrIsTTY) {
|
||||
Object.defineProperty(process.stderr, "isTTY", originalStderrIsTTY);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stderr, "isTTY");
|
||||
}
|
||||
}
|
||||
|
||||
describe("cron show pagination guard (regression for #83856)", () => {
|
||||
beforeEach(() => {
|
||||
callGatewayFromCli.mockReset();
|
||||
@@ -85,3 +108,42 @@ describe("cron show pagination guard (regression for #83856)", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cron disable hint", () => {
|
||||
beforeEach(() => {
|
||||
callGatewayFromCli.mockReset();
|
||||
callGatewayFromCli.mockImplementation(async (method: string) => {
|
||||
if (method === "cron.status") {
|
||||
return { enabled: true };
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
vi.spyOn(defaultRuntime, "writeJson").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreStderrIsTTY();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ command: "disable" as const, tty: false, expectedHint: false },
|
||||
{ command: "disable" as const, tty: true, expectedHint: true },
|
||||
{ command: "enable" as const, tty: true, expectedHint: false },
|
||||
])("$command with stderr TTY=$tty emits hint=$expectedHint", async (params) => {
|
||||
setStderrIsTTY(params.tty);
|
||||
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
||||
|
||||
await runCronToggle(params.command);
|
||||
|
||||
expect(callGatewayFromCli).toHaveBeenCalledWith("cron.update", expect.anything(), {
|
||||
id: "job-1",
|
||||
patch: { enabled: params.command === "enable" },
|
||||
});
|
||||
if (params.expectedHint) {
|
||||
expect(stderrWrite).toHaveBeenCalledWith(expect.stringContaining("openclaw cron list --all"));
|
||||
} else {
|
||||
expect(stderrWrite).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,6 +143,11 @@ function registerCronToggleCommand(params: {
|
||||
patch: { enabled: params.enabled },
|
||||
});
|
||||
printCronJson(res);
|
||||
if (!params.enabled && process.stderr.isTTY) {
|
||||
process.stderr.write(
|
||||
`Note: 'openclaw cron list' hides disabled jobs by default. Use 'openclaw cron list --all' to see this job, or 'openclaw cron enable <id>' to re-enable it.\n`,
|
||||
);
|
||||
}
|
||||
await warnIfCronSchedulerDisabled(opts);
|
||||
} catch (err) {
|
||||
handleCronCliError(err);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
noopLogger,
|
||||
setupCronRegressionFixtures,
|
||||
} from "../../../test/helpers/cron/service-regression-fixtures.js";
|
||||
import { add, list, update } from "./ops.js";
|
||||
import { createCronServiceState } from "./state.js";
|
||||
|
||||
const fixtures = setupCronRegressionFixtures({ prefix: "cron-disable-list-" });
|
||||
|
||||
describe("cron service ops: disable + list round-trip", () => {
|
||||
it("keeps a disabled job available to --all and restores it after enable", async () => {
|
||||
const { storePath } = fixtures.makeStorePath();
|
||||
const state = createCronServiceState({
|
||||
cronEnabled: true,
|
||||
storePath,
|
||||
log: noopLogger,
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
requestHeartbeat: vi.fn(),
|
||||
runIsolatedAgentJob: vi.fn(),
|
||||
});
|
||||
|
||||
const job = await add(state, {
|
||||
name: "disable-and-list",
|
||||
enabled: true,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
payload: { kind: "agentTurn", message: "ping" },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "next-heartbeat",
|
||||
delivery: { mode: "announce" },
|
||||
});
|
||||
|
||||
try {
|
||||
await update(state, job.id, { enabled: false });
|
||||
expect((await list(state)).map(({ id }) => id)).not.toContain(job.id);
|
||||
expect(await list(state, { includeDisabled: true })).toContainEqual(
|
||||
expect.objectContaining({ id: job.id, enabled: false }),
|
||||
);
|
||||
|
||||
await update(state, job.id, { enabled: true });
|
||||
expect(await list(state)).toContainEqual(
|
||||
expect.objectContaining({ id: job.id, enabled: true }),
|
||||
);
|
||||
} finally {
|
||||
if (state.timer) {
|
||||
clearTimeout(state.timer);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user