schedule: fire --every now by default, add run-once for cron-style triggers (#689)

Interval schedules used to wait the full interval before their first run, which
made `--every 1d` feel broken for fresh schedules. Now `--every` fires
immediately on creation; `--cron` keeps waiting for the next slot. `--no-run-now`
and `--run-now` override the defaults, and the parser rejects redundant combos.

Adds `paseo schedule run-once <id>` to manually trigger a single run without
advancing cadence or recomputing completion. The new `runOnCreate` field and
`schedule/run-once` RPC are additive on the WebSocket schema.
This commit is contained in:
Mohamed Boudra
2026-05-05 19:56:51 +07:00
parent f11aea3223
commit 2b6d7d4ec2
13 changed files with 387 additions and 16 deletions
@@ -8,6 +8,12 @@ const baseOptions = {
provider: "claude",
};
const baseCron = {
prompt: "do the thing",
cron: "0 9 * * *",
provider: "claude",
};
describe("parseScheduleCreateInput cwd/host validation", () => {
beforeEach(() => {
vi.spyOn(process, "cwd").mockReturnValue("/local/project");
@@ -60,3 +66,43 @@ describe("parseScheduleCreateInput cwd/host validation", () => {
).toThrow(expect.objectContaining({ code: "MISSING_CWD" }));
});
});
describe("parseScheduleCreateInput first-run timing", () => {
test("--every with no run-now flag fires immediately on creation", () => {
const input = parseScheduleCreateInput(baseOptions);
expect(input.runOnCreate).toBe(true);
});
test("--every with --no-run-now waits the interval", () => {
const input = parseScheduleCreateInput({ ...baseOptions, runNow: false });
expect(input.runOnCreate).toBe(false);
});
test("--cron with no run-now flag waits for the next cron slot", () => {
const input = parseScheduleCreateInput(baseCron);
expect(input.runOnCreate).toBe(false);
});
test("--cron with --run-now fires immediately on creation", () => {
const input = parseScheduleCreateInput({ ...baseCron, runNow: true });
expect(input.runOnCreate).toBe(true);
});
test("--every with --run-now is rejected as redundant", () => {
expect(() => parseScheduleCreateInput({ ...baseOptions, runNow: true })).toThrow(
expect.objectContaining({
code: "REDUNDANT_RUN_NOW",
message: expect.stringContaining("--run-now is redundant with --every"),
}),
);
});
test("--cron with --no-run-now is rejected as redundant", () => {
expect(() => parseScheduleCreateInput({ ...baseCron, runNow: false })).toThrow(
expect.objectContaining({
code: "REDUNDANT_NO_RUN_NOW",
message: expect.stringContaining("--no-run-now is redundant with --cron"),
}),
);
});
});