fix(schedules): refresh untouched builtin config so checks get new capabilities

Built-in agent checks kept the config stored at first registration, so
check.pending never received decision.list / subscription.list added in
AG-3 and the patrol could not read pending items (REAL-2 scenario g).
Track config_customized (migration 0045); sync replaces config with the
code default unless the user PATCHed it. Needs image rebuild.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 03:06:05 +08:00
1 parent 96fc089f92
commit bd0e011748
6 files changed
+52 -5

No files matched your search

+5 -2
View File
@@ -94,7 +94,8 @@ export function runView(r: ScheduledJobRunRow): ScheduledJobRun {
/**
* 把代码登记的定义同步进表:新行按默认值插入;已有行刷新名称/分组/默认值,
* 用户改过(customized)的 enabled/schedule 保留;config 以表内值为准、补上新增的默认键。
* 用户改过(customized)的 enabled/schedule 保留;config 用户改过(config_customized)时以表内值为准、补上新增的默认键,
* 否则整体换成代码默认值(新增的巡检白名单能力等才能进旧行)。
* 未改过且默认调度变了的行,下次运行改为立即(随后按新周期)。
* 同步时代码里已不再登记的内置 task 行标为不可用(不会执行,看板显示"未登记")。
*/
@@ -121,7 +122,8 @@ export async function syncDefinitions(db: Kysely<DB>, defs: readonly ScheduledJo
default_schedule = excluded.default_schedule,
enabled = case when scheduled_job.customized then scheduled_job.enabled else excluded.enabled end,
schedule = case when scheduled_job.customized then scheduled_job.schedule else excluded.schedule end,
config = excluded.config || scheduled_job.config,
config = case when scheduled_job.config_customized then excluded.config || scheduled_job.config
else excluded.config end,
next_run_at = case
when not scheduled_job.customized and scheduled_job.schedule is distinct from excluded.schedule then ${now}
else coalesce(scheduled_job.next_run_at, excluded.next_run_at) end`.execute(db);
@@ -221,6 +223,7 @@ export async function updateJob(
} else {
set.config = JSON.stringify({ ...((row.config ?? {}) as object), ...patch.config });
}
set.config_customized = true;
}
return await db.updateTable("scheduled_job").set(set).where("id", "=", id).returningAll().executeTakeFirstOrThrow();
}
@@ -144,6 +144,22 @@ describe.skipIf(!hasPg)("定时任务注册表(真实 PG)", () => {
expect(new Date(cron.body.data.nextRunAt).getHours()).toBe(4);
});
it("config:没改过的行随代码默认值整体更新;PATCH 过的保留表内值", async () => {
const def = (config: Record<string, unknown>) =>
counting("test.cfg", {
defaults: { name: "c", group: "download", schedule: { type: "interval", everyMs: 60_000 }, config },
});
registerScheduledJob(ctx, def({ list: ["a"] }));
await sched.tick();
registerScheduledJob(ctx, def({ list: ["a", "b"] }));
await sched.tick();
expect((await job("test.cfg")).config).toEqual({ list: ["a", "b"] });
expect((await api("PATCH", "/schedules/test.cfg", { config: { list: ["x"] } })).status).toBe(200);
registerScheduledJob(ctx, def({ list: ["a", "b", "c"], extra: 1 }));
await sched.tick();
expect((await job("test.cfg")).config).toEqual({ list: ["x"], extra: 1 });
});
it("立即执行幂等(合并重复请求)且不改下次运行时间", async () => {
registerScheduledJob(
ctx,