Skip to content

Commit 9ca7491

Browse files
committed
Keep legacy cron store doctor check lint-only
1 parent a9f1220 commit 9ca7491

5 files changed

Lines changed: 4 additions & 301 deletions

File tree

scripts/check-kysely-guardrails.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const rawSqliteAllowPathGroups = {
4949
"agent auth profile read-only bootstrap": ["src/agents/auth-profiles/sqlite.ts"],
5050
"read-only SQLite status probes": ["src/commands/status.scan.shared.ts"],
5151
"doctor legacy state migration": [
52+
"src/commands/doctor/cron/migration-ledger.ts",
5253
"src/infra/state-migrations.ts",
5354
"src/infra/state-migrations.debug-proxy.ts",
5455
],

src/commands/doctor/cron/index.test.ts

Lines changed: 0 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@ import {
1414
saveCronStore,
1515
} from "../../../cron/store.js";
1616
import { runOpenClawStateWriteTransaction } from "../../../state/openclaw-state-db.js";
17-
import { resolveOpenClawStateSqlitePath } from "../../../state/openclaw-state-db.paths.js";
1817
import { withRestoredMocks } from "../../../test-utils/vitest-spies.js";
1918
import {
2019
collectLegacyCronStoreHealthFindings,
21-
collectLegacyCronStoreRepairEffects,
2220
collectLegacyWhatsAppCrontabHealthWarning,
2321
maybeRepairLegacyCronStore,
2422
noteLegacyWhatsAppCrontabHealthCheck,
@@ -302,146 +300,6 @@ describe("collectLegacyCronStoreHealthFindings", () => {
302300
});
303301
});
304302

305-
describe("collectLegacyCronStoreRepairEffects", () => {
306-
it("does not create the shared state database while previewing legacy cron state", async () => {
307-
const storePath = await makeTempStorePath();
308-
const stateDir = path.join(path.dirname(storePath), "state-root");
309-
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
310-
const stateDbPath = resolveOpenClawStateSqlitePath({ OPENCLAW_STATE_DIR: stateDir });
311-
await writeLegacyCronArrayStore(storePath, [createLegacyCronJob()]);
312-
313-
await expect(
314-
collectLegacyCronStoreHealthFindings({ cfg: createCronConfig(storePath) }),
315-
).resolves.toEqual(
316-
expect.arrayContaining([
317-
expect.objectContaining({
318-
checkId: "core/doctor/legacy-cron-store",
319-
requirement: "legacy-cron-store",
320-
}),
321-
]),
322-
);
323-
await expect(
324-
collectLegacyCronStoreRepairEffects({ cfg: createCronConfig(storePath) }),
325-
).resolves.toEqual(
326-
expect.arrayContaining([
327-
expect.objectContaining({
328-
action: "would-migrate-legacy-cron-store",
329-
target: storePath,
330-
}),
331-
]),
332-
);
333-
334-
expect(fsSync.existsSync(stateDbPath)).toBe(false);
335-
});
336-
337-
it("previews legacy cron store repair effects without mutating files", async () => {
338-
const storePath = await makeTempStorePath();
339-
await writeLegacyCronArrayStore(storePath, [
340-
createLegacyCronJob({
341-
jobId: "legacy-notify",
342-
payload: {
343-
kind: "systemEvent",
344-
text: "Morning brief",
345-
},
346-
}),
347-
]);
348-
const runLogPath = path.join(path.dirname(storePath), "runs", "legacy-notify.jsonl");
349-
await fs.mkdir(path.dirname(runLogPath), { recursive: true });
350-
await fs.writeFile(runLogPath, "", "utf-8");
351-
352-
const effects = await collectLegacyCronStoreRepairEffects({
353-
cfg: createCronConfig(storePath),
354-
});
355-
356-
expect(effects).toEqual(
357-
expect.arrayContaining([
358-
expect.objectContaining({
359-
kind: "state",
360-
action: "would-migrate-legacy-cron-store",
361-
target: storePath,
362-
dryRunSafe: false,
363-
}),
364-
expect.objectContaining({
365-
kind: "state",
366-
action: "would-import-legacy-cron-run-logs",
367-
target: path.join(path.dirname(storePath), "runs"),
368-
dryRunSafe: false,
369-
}),
370-
expect.objectContaining({
371-
kind: "state",
372-
action: "would-normalize-cron-store",
373-
target: storePath,
374-
dryRunSafe: false,
375-
}),
376-
expect.objectContaining({
377-
kind: "state",
378-
action: "would-migrate-legacy-cron-notify-fallback",
379-
target: storePath,
380-
dryRunSafe: false,
381-
}),
382-
]),
383-
);
384-
await expect(fs.readFile(storePath, "utf-8")).resolves.toContain("legacy-notify");
385-
await expect(fs.stat(runLogPath)).resolves.toBeDefined();
386-
});
387-
388-
it("previews quarantine and dreaming payload rewrite effects", async () => {
389-
const storePath = await makeTempStorePath();
390-
await writeCronStore(storePath, [
391-
{ id: "invalid-cron", name: "Invalid cron", payload: { kind: "systemEvent" } },
392-
{
393-
id: "memory-dreaming",
394-
name: "Memory Dreaming Promotion",
395-
description:
396-
"[managed-by=memory-core.short-term-promotion] Promote weighted short-term recalls.",
397-
enabled: true,
398-
createdAtMs: Date.parse("2026-04-01T00:00:00.000Z"),
399-
updatedAtMs: Date.parse("2026-04-01T00:00:00.000Z"),
400-
schedule: { kind: "cron", expr: "0 3 * * *", tz: "UTC" },
401-
sessionTarget: "main",
402-
wakeMode: "now",
403-
payload: {
404-
kind: "systemEvent",
405-
text: "__openclaw_memory_core_short_term_promotion_dream__",
406-
},
407-
state: {},
408-
},
409-
]);
410-
411-
const effects = await collectLegacyCronStoreRepairEffects({
412-
cfg: createCronConfig(storePath),
413-
});
414-
415-
expect(effects).toEqual(
416-
expect.arrayContaining([
417-
expect.objectContaining({
418-
action: "would-quarantine-invalid-cron-jobs",
419-
target: resolveCronQuarantinePath(storePath),
420-
}),
421-
expect.objectContaining({
422-
action: "would-normalize-cron-store",
423-
target: storePath,
424-
}),
425-
expect.objectContaining({
426-
action: "would-rewrite-legacy-dreaming-cron-payload",
427-
target: storePath,
428-
}),
429-
]),
430-
);
431-
await expect(fs.readFile(storePath, "utf-8")).resolves.toContain("invalid-cron");
432-
await expect(fs.readFile(storePath, "utf-8")).resolves.toContain("memory-dreaming");
433-
});
434-
435-
it("returns no repair effects for an already-normalized empty cron store", async () => {
436-
const storePath = await makeTempStorePath();
437-
await writeCurrentCronStore(storePath, []);
438-
439-
await expect(
440-
collectLegacyCronStoreRepairEffects({ cfg: createCronConfig(storePath) }),
441-
).resolves.toEqual([]);
442-
});
443-
});
444-
445303
describe("maybeRepairLegacyCronStore", () => {
446304
it("reports quarantined cron rows even when the active store is already sanitized", async () => {
447305
const storePath = await makeTempStorePath();

src/commands/doctor/cron/index.ts

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Doctor cron repair orchestration for legacy stores, run logs, payloads, and warnings.
2-
import path from "node:path";
32
import { normalizeOptionalString } from "../../../../packages/normalization-core/src/string-coerce.js";
43
import { note } from "../../../../packages/terminal-core/src/note.js";
54
import { formatCliCommand } from "../../../cli/command-format.js";
@@ -15,7 +14,7 @@ import {
1514
saveCronJobsStoreWithMetadata,
1615
} from "../../../cron/store.js";
1716
import type { CronJob } from "../../../cron/types.js";
18-
import type { HealthFinding, HealthRepairEffect } from "../../../flows/health-checks.js";
17+
import type { HealthFinding } from "../../../flows/health-checks.js";
1918
import { shortenHomePath } from "../../../utils.js";
2019
import type { DoctorPrompter, DoctorOptions } from "../../doctor-prompter.js";
2120
import {
@@ -123,10 +122,6 @@ function legacyCronStoreFinding(params: {
123122
};
124123
}
125124

126-
function cloneCronJobRecords(jobs: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
127-
return structuredClone(jobs);
128-
}
129-
130125
async function loadLegacyCronRepairState(params: {
131126
cfg: OpenClawConfig;
132127
onlyIfLegacyDetected?: boolean;
@@ -447,97 +442,6 @@ export async function collectLegacyCronStoreHealthFindings(params: {
447442
return findings;
448443
}
449444

450-
export async function collectLegacyCronStoreRepairEffects(params: {
451-
cfg: OpenClawConfig;
452-
}): Promise<readonly HealthRepairEffect[]> {
453-
let state: LegacyCronRepairState | null;
454-
try {
455-
state = await loadLegacyCronRepairState({ cfg: params.cfg, readOnly: true });
456-
} catch {
457-
return [];
458-
}
459-
if (!state) {
460-
return [];
461-
}
462-
463-
const effects: HealthRepairEffect[] = [];
464-
if (state.legacyStoreDetected) {
465-
effects.push({
466-
kind: "state",
467-
action: state.legacyMigrationAlreadyImported
468-
? "would-archive-legacy-cron-store"
469-
: "would-migrate-legacy-cron-store",
470-
target: state.storePath,
471-
dryRunSafe: false,
472-
});
473-
}
474-
if (state.legacyRunLogDetected) {
475-
effects.push({
476-
kind: "state",
477-
action: "would-import-legacy-cron-run-logs",
478-
target: path.join(path.dirname(state.storePath), "runs"),
479-
dryRunSafe: false,
480-
});
481-
}
482-
if (state.rawJobs.length === 0) {
483-
return effects;
484-
}
485-
486-
const normalizedPreviewJobs = cloneCronJobRecords(state.rawJobs);
487-
const normalized = normalizeStoredCronJobs(normalizedPreviewJobs);
488-
if (normalized.removedJobs.length > 0) {
489-
effects.push({
490-
kind: "state",
491-
action: "would-quarantine-invalid-cron-jobs",
492-
target: state.quarantinePath,
493-
dryRunSafe: false,
494-
});
495-
}
496-
if (normalized.mutated) {
497-
effects.push({
498-
kind: "state",
499-
action: "would-normalize-cron-store",
500-
target: state.storePath,
501-
dryRunSafe: false,
502-
});
503-
}
504-
if (state.sqliteProjectionBackfillCount > 0) {
505-
effects.push({
506-
kind: "state",
507-
action: "would-backfill-cron-sqlite-projection",
508-
target: state.storePath,
509-
dryRunSafe: false,
510-
});
511-
}
512-
513-
const notifyPreviewJobs = cloneCronJobRecords(state.rawJobs);
514-
const notifyMigration = migrateLegacyNotifyFallback({
515-
jobs: notifyPreviewJobs,
516-
legacyWebhook: normalizeOptionalString(params.cfg.cron?.webhook),
517-
});
518-
if (notifyMigration.changed) {
519-
effects.push({
520-
kind: "state",
521-
action: "would-migrate-legacy-cron-notify-fallback",
522-
target: state.storePath,
523-
dryRunSafe: false,
524-
});
525-
}
526-
527-
const dreamingPreviewJobs = cloneCronJobRecords(state.rawJobs);
528-
const dreamingMigration = migrateLegacyDreamingPayloadShape(dreamingPreviewJobs);
529-
if (dreamingMigration.changed) {
530-
effects.push({
531-
kind: "state",
532-
action: "would-rewrite-legacy-dreaming-cron-payload",
533-
target: state.storePath,
534-
dryRunSafe: false,
535-
});
536-
}
537-
538-
return effects;
539-
}
540-
541445
export async function repairLegacyCronStoreWithoutPrompt(params: {
542446
cfg: OpenClawConfig;
543447
}): Promise<LegacyCronRepairResult> {

src/flows/doctor-core-checks.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@ const legacyWhatsAppCrontabCheck: HealthCheck = {
678678
},
679679
};
680680

681-
const legacyCronStoreCheck: HealthCheck = {
681+
const legacyCronStoreCheck: SplitHealthCheckInput = {
682682
id: "core/doctor/legacy-cron-store",
683683
kind: "core",
684684
description: "Legacy cron store, run-log, and payload state is normalized.",
@@ -689,20 +689,6 @@ const legacyCronStoreCheck: HealthCheck = {
689689
await import("../commands/doctor/cron/index.js");
690690
return collectLegacyCronStoreHealthFindings({ cfg: ctx.cfg });
691691
},
692-
async repair(ctx) {
693-
const { collectLegacyCronStoreRepairEffects } =
694-
await import("../commands/doctor/cron/index.js");
695-
const effects = await collectLegacyCronStoreRepairEffects({ cfg: ctx.cfg });
696-
if (ctx.dryRun === true) {
697-
return { status: "repaired", changes: [], effects };
698-
}
699-
return {
700-
status: "skipped",
701-
reason: "legacy doctor cron contribution owns real cron store repair",
702-
changes: [],
703-
effects,
704-
};
705-
},
706692
};
707693

708694
const codexSessionRoutesCheck: HealthCheck = {

src/flows/doctor-health-contributions.test.ts

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ const mocks = vi.hoisted(() => ({
104104
collectWhatsappResponsivenessHealthFindings: vi.fn((): readonly HealthFinding[] => []),
105105
noteWhatsappResponsivenessHealth: vi.fn().mockResolvedValue(undefined),
106106
collectDevicePairingHealthFindings: vi.fn(async () => []),
107-
collectLegacyCronStoreHealthFindings: vi.fn(async () => []),
108-
collectLegacyCronStoreRepairEffects: vi.fn(async () => []),
107+
collectLegacyCronStoreHealthFindings: vi.fn(async (): Promise<readonly HealthFinding[]> => []),
109108
collectLegacyWhatsAppCrontabHealthWarning: vi.fn(async () => undefined),
110109
maybeRepairLegacyCronStore: vi.fn().mockResolvedValue(undefined),
111110
noteLegacyWhatsAppCrontabHealthCheck: vi.fn().mockResolvedValue(undefined),
@@ -359,7 +358,6 @@ vi.mock("../commands/doctor-device-pairing.js", () => ({
359358

360359
vi.mock("../commands/doctor/cron/index.js", () => ({
361360
collectLegacyCronStoreHealthFindings: mocks.collectLegacyCronStoreHealthFindings,
362-
collectLegacyCronStoreRepairEffects: mocks.collectLegacyCronStoreRepairEffects,
363361
collectLegacyWhatsAppCrontabHealthWarning: mocks.collectLegacyWhatsAppCrontabHealthWarning,
364362
maybeRepairLegacyCronStore: mocks.maybeRepairLegacyCronStore,
365363
noteLegacyWhatsAppCrontabHealthCheck: mocks.noteLegacyWhatsAppCrontabHealthCheck,
@@ -574,8 +572,6 @@ describe("doctor health contributions", () => {
574572
mocks.collectDevicePairingHealthFindings.mockResolvedValue([]);
575573
mocks.collectLegacyCronStoreHealthFindings.mockReset();
576574
mocks.collectLegacyCronStoreHealthFindings.mockResolvedValue([]);
577-
mocks.collectLegacyCronStoreRepairEffects.mockReset();
578-
mocks.collectLegacyCronStoreRepairEffects.mockResolvedValue([]);
579575
mocks.collectLegacyWhatsAppCrontabHealthWarning.mockReset();
580576
mocks.collectLegacyWhatsAppCrontabHealthWarning.mockResolvedValue(undefined);
581577
mocks.maybeRepairLegacyCronStore.mockReset();
@@ -1930,48 +1926,6 @@ describe("doctor health contributions", () => {
19301926
expect(mocks.collectLegacyCronStoreHealthFindings).toHaveBeenCalledWith({ cfg: ctx.cfg });
19311927
});
19321928

1933-
it("previews legacy cron store repair effects without taking over real repair", async () => {
1934-
const contributionChecks = await resolveDoctorContributionHealthChecks();
1935-
const cronStoreCheck = contributionChecks.find(
1936-
(check) => check.id === "core/doctor/legacy-cron-store",
1937-
);
1938-
expect(cronStoreCheck?.repair).toBeDefined();
1939-
mocks.collectLegacyCronStoreRepairEffects.mockResolvedValue([
1940-
{
1941-
kind: "state",
1942-
action: "would-migrate-legacy-cron-store",
1943-
target: "/tmp/openclaw-cron/jobs.json",
1944-
dryRunSafe: false,
1945-
},
1946-
]);
1947-
const ctx = {
1948-
cfg: { cron: { store: "/tmp/openclaw-cron/jobs.json" } },
1949-
mode: "fix",
1950-
dryRun: true,
1951-
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
1952-
} as const;
1953-
1954-
await expect(cronStoreCheck!.repair!(ctx, [])).resolves.toEqual({
1955-
status: "repaired",
1956-
changes: [],
1957-
effects: [
1958-
{
1959-
kind: "state",
1960-
action: "would-migrate-legacy-cron-store",
1961-
target: "/tmp/openclaw-cron/jobs.json",
1962-
dryRunSafe: false,
1963-
},
1964-
],
1965-
});
1966-
await expect(cronStoreCheck!.repair!({ ...ctx, dryRun: false }, [])).resolves.toMatchObject({
1967-
status: "skipped",
1968-
reason: "legacy doctor cron contribution owns real cron store repair",
1969-
changes: [],
1970-
effects: [expect.objectContaining({ action: "would-migrate-legacy-cron-store" })],
1971-
});
1972-
expect(mocks.collectLegacyCronStoreRepairEffects).toHaveBeenCalledWith({ cfg: ctx.cfg });
1973-
});
1974-
19751929
it("keeps channel plugin blockers opt-in for default lint selection", async () => {
19761930
const contributionChecks = await resolveDoctorContributionHealthChecks();
19771931
const blockerCheck = contributionChecks.find(

0 commit comments

Comments
 (0)