mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:00:45 +00:00
cron: surface unreadable state sidecars
This commit is contained in:
@@ -243,6 +243,45 @@ describe("cron store", () => {
|
||||
expect(stateFile.jobs["job-1"].state.nextRunAtMs).toBe(legacy.jobs[0].createdAtMs + 60_000);
|
||||
});
|
||||
|
||||
it("treats a corrupt state sidecar as absent", async () => {
|
||||
const store = await makeStorePath();
|
||||
const payload = makeStore("job-1", true);
|
||||
payload.jobs[0].state = { nextRunAtMs: payload.jobs[0].createdAtMs + 60_000 };
|
||||
const statePath = store.storePath.replace(/\.json$/, "-state.json");
|
||||
|
||||
await saveCronStore(store.storePath, payload);
|
||||
await fs.writeFile(statePath, "{ not json", "utf-8");
|
||||
|
||||
const loaded = await loadCronStore(store.storePath);
|
||||
|
||||
expect(loaded.jobs[0]?.updatedAtMs).toBe(payload.jobs[0].createdAtMs);
|
||||
expect(loaded.jobs[0]?.state).toEqual({});
|
||||
});
|
||||
|
||||
it("propagates unreadable state sidecar errors", async () => {
|
||||
const store = await makeStorePath();
|
||||
const payload = makeStore("job-1", true);
|
||||
const statePath = store.storePath.replace(/\.json$/, "-state.json");
|
||||
|
||||
await saveCronStore(store.storePath, payload);
|
||||
|
||||
const origReadFile = fs.readFile.bind(fs);
|
||||
const spy = vi.spyOn(fs, "readFile").mockImplementation(async (filePath, options) => {
|
||||
if (filePath === statePath) {
|
||||
const err = new Error("permission denied") as NodeJS.ErrnoException;
|
||||
err.code = "EACCES";
|
||||
throw err;
|
||||
}
|
||||
return origReadFile(filePath, options as never) as never;
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(loadCronStore(store.storePath)).rejects.toThrow(/Failed to read cron state/);
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("sanitizes invalid updatedAtMs values from the state sidecar", async () => {
|
||||
const store = await makeStorePath();
|
||||
const job = makeStore("job-1", true).jobs[0];
|
||||
|
||||
@@ -81,8 +81,19 @@ export function resolveCronStorePath(storePath?: string) {
|
||||
}
|
||||
|
||||
async function loadStateFile(statePath: string): Promise<CronStateFile | null> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await fs.promises.readFile(statePath, "utf-8");
|
||||
} catch (err) {
|
||||
if ((err as { code?: unknown })?.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`Failed to read cron state at ${statePath}: ${String(err)}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await fs.promises.readFile(statePath, "utf-8");
|
||||
const parsed = parseJsonWithJson5Fallback(raw);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return null;
|
||||
@@ -92,10 +103,7 @@ async function loadStateFile(statePath: string): Promise<CronStateFile | null> {
|
||||
return null;
|
||||
}
|
||||
return { version: 1, jobs: record.jobs as Record<string, CronStateFileEntry> };
|
||||
} catch (err) {
|
||||
if ((err as { code?: unknown })?.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
// Best-effort: if state file is corrupt, treat as absent.
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user