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:
Kate Stahnke
2026-07-21 15:04:15 -04:00
committed by GitHub
parent a69ebf3067
commit 4400f4ca91
5 changed files with 122 additions and 2 deletions
+4 -1
View File
@@ -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
View File
@@ -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();
}
});
});
+5
View File
@@ -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);
}
}
});
});