惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
小众软件
小众软件
博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
量子位
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
雷峰网
雷峰网
云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
T
Tailwind CSS Blog
Martin Fowler
Martin Fowler
I
InfoQ
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
The Cloudflare Blog
罗磊的独立博客

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(cron): reject cron expressions that have no reachable...
yetval · 2026-06-11 · via Recent Commits to openclaw:main

@@ -642,4 +642,188 @@ describe("cron service ops seam coverage", () => {

642642

await manualRun;

643643

});

644644

});

645+646+

it("rejects add of a structurally valid cron expression that never matches", async () => {

647+

const { storePath } = await makeStorePath();

648+

const now = Date.parse("2026-06-09T00:00:00.000Z");

649+

const state = createOkIsolatedCronState({ storePath, now });

650+651+

await expect(

652+

add(state, {

653+

name: "feb 30 cleanup",

654+

enabled: true,

655+

schedule: { kind: "cron", expr: "0 0 30 2 *" },

656+

sessionTarget: "isolated",

657+

wakeMode: "next-heartbeat",

658+

payload: { kind: "agentTurn", message: "do work" },

659+

}),

660+

).rejects.toThrow(/has no upcoming run time and would never fire/);

661+

if (state.timer) {

662+

clearTimeout(state.timer);

663+

}

664+665+

const loaded = await loadCronStore(storePath);

666+

expect(loaded.jobs).toEqual([]);

667+

});

668+669+

it("accepts add of a satisfiable cron expression and arms a next run", async () => {

670+

const { storePath } = await makeStorePath();

671+

const now = Date.parse("2026-06-09T00:00:00.000Z");

672+

const state = createOkIsolatedCronState({ storePath, now });

673+674+

const job = await add(state, {

675+

name: "daily cleanup",

676+

enabled: true,

677+

schedule: { kind: "cron", expr: "0 0 * * *" },

678+

sessionTarget: "isolated",

679+

wakeMode: "next-heartbeat",

680+

payload: { kind: "agentTurn", message: "do work" },

681+

});

682+

if (state.timer) {

683+

clearTimeout(state.timer);

684+

}

685+686+

expect(typeof job.state.nextRunAtMs).toBe("number");

687+

expect(job.state.nextRunAtMs).toBeGreaterThan(now);

688+

});

689+690+

it("rejects update that changes a job to a never-matching cron expression", async () => {

691+

const { storePath } = await makeStorePath();

692+

const now = Date.parse("2026-06-09T00:00:00.000Z");

693+

const state = createOkIsolatedCronState({ storePath, now });

694+695+

const job = await add(state, {

696+

name: "daily cleanup",

697+

enabled: true,

698+

schedule: { kind: "cron", expr: "0 0 * * *" },

699+

sessionTarget: "isolated",

700+

wakeMode: "next-heartbeat",

701+

payload: { kind: "agentTurn", message: "do work" },

702+

});

703+

if (state.timer) {

704+

clearTimeout(state.timer);

705+

}

706+707+

await expect(

708+

update(state, job.id, { schedule: { kind: "cron", expr: "0 0 30 2 *" } }),

709+

).rejects.toThrow(/has no upcoming run time and would never fire/);

710+

if (state.timer) {

711+

clearTimeout(state.timer);

712+

}

713+714+

const loaded = await loadCronStore(storePath);

715+

const stored = loaded.jobs.find((entry) => entry.id === job.id);

716+

expect(stored?.schedule).toMatchObject({ kind: "cron", expr: "0 0 * * *" });

717+

});

718+719+

it("allows non-schedule updates on a pre-existing never-matching job", async () => {

720+

const { storePath } = await makeStorePath();

721+

const now = Date.parse("2026-06-09T00:00:00.000Z");

722+

await writeCronStoreSnapshot({

723+

storePath,

724+

jobs: [

725+

{

726+

id: "legacy-unsatisfiable",

727+

name: "legacy unsatisfiable",

728+

enabled: true,

729+

createdAtMs: now - 60_000,

730+

updatedAtMs: now - 60_000,

731+

schedule: { kind: "cron", expr: "0 0 30 2 *" },

732+

sessionTarget: "isolated",

733+

wakeMode: "next-heartbeat",

734+

payload: { kind: "agentTurn", message: "do work" },

735+

state: {},

736+

},

737+

],

738+

});

739+

const state = createOkIsolatedCronState({ storePath, now });

740+741+

const updated = await update(state, "legacy-unsatisfiable", { enabled: false });

742+

if (state.timer) {

743+

clearTimeout(state.timer);

744+

}

745+746+

expect(updated.enabled).toBe(false);

747+

});

748+749+

it("rejects enabling a pre-existing never-matching job", async () => {

750+

const { storePath } = await makeStorePath();

751+

const now = Date.parse("2026-06-09T00:00:00.000Z");

752+

await writeCronStoreSnapshot({

753+

storePath,

754+

jobs: [

755+

{

756+

id: "legacy-unsatisfiable",

757+

name: "legacy unsatisfiable",

758+

enabled: false,

759+

createdAtMs: now,

760+

updatedAtMs: now,

761+

schedule: { kind: "cron", expr: "0 0 30 2 *" },

762+

sessionTarget: "isolated",

763+

wakeMode: "next-heartbeat",

764+

payload: { kind: "agentTurn", message: "do work" },

765+

state: {},

766+

},

767+

],

768+

});

769+

const state = createOkIsolatedCronState({ storePath, now });

770+771+

await expect(update(state, "legacy-unsatisfiable", { enabled: true })).rejects.toThrow(

772+

/has no upcoming run time and would never fire/,

773+

);

774+775+

const loaded = await loadCronStore(storePath);

776+

expect(loaded.jobs[0]?.enabled).toBe(false);

777+

});

778+779+

it("uses the service clock when validating a finite-year cron update", async () => {

780+

const { storePath } = await makeStorePath();

781+

const now = Date.parse("2000-06-09T00:00:00.000Z");

782+

const state = createOkIsolatedCronState({ storePath, now });

783+

const job = await add(state, {

784+

name: "future finite-year job",

785+

enabled: true,

786+

schedule: { kind: "cron", expr: "0 0 * * *" },

787+

sessionTarget: "isolated",

788+

wakeMode: "next-heartbeat",

789+

payload: { kind: "agentTurn", message: "do work" },

790+

});

791+

if (state.timer) {

792+

clearTimeout(state.timer);

793+

}

794+795+

const updated = await update(state, job.id, {

796+

schedule: { kind: "cron", expr: "0 0 0 1 1 * 2001", tz: "UTC" },

797+

});

798+

if (state.timer) {

799+

clearTimeout(state.timer);

800+

}

801+802+

expect(updated.state.nextRunAtMs).toBe(Date.parse("2001-01-01T00:00:00.000Z"));

803+

});

804+805+

it("accepts a finite-year cron while its final staggered run is pending", async () => {

806+

const { storePath } = await makeStorePath();

807+

const finalBaseRunAtMs = Date.parse("2001-01-01T00:00:00.000Z");

808+

const state = createOkIsolatedCronState({ storePath, now: finalBaseRunAtMs + 1 });

809+810+

const job = await add(state, {

811+

name: "final staggered run",

812+

enabled: true,

813+

schedule: {

814+

kind: "cron",

815+

expr: "0 0 0 1 1 * 2001",

816+

tz: "UTC",

817+

staggerMs: 3_600_000,

818+

},

819+

sessionTarget: "isolated",

820+

wakeMode: "next-heartbeat",

821+

payload: { kind: "agentTurn", message: "do work" },

822+

});

823+

if (state.timer) {

824+

clearTimeout(state.timer);

825+

}

826+827+

expect(job.state.nextRunAtMs).toBeGreaterThan(finalBaseRunAtMs);

828+

});

645829

});