mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:01:23 +00:00
perf(test): speed up suites and reduce fs churn
This commit is contained in:
@@ -190,12 +190,14 @@ describe("CronService interval/cron jobs fire on time", () => {
|
||||
});
|
||||
|
||||
await cron.start();
|
||||
for (let minute = 1; minute <= 6; minute++) {
|
||||
// Perf: a few recomputation cycles are enough to catch legacy "every" drift.
|
||||
for (let minute = 1; minute <= 3; minute++) {
|
||||
vi.setSystemTime(new Date(nowMs + minute * 60_000));
|
||||
const minuteRun = await cron.run("minute-cron", "force");
|
||||
expect(minuteRun).toEqual({ ok: true, ran: true });
|
||||
}
|
||||
|
||||
// "every" cadence is 2m; verify it stays due at the 6-minute boundary.
|
||||
vi.setSystemTime(new Date(nowMs + 6 * 60_000));
|
||||
const sfRun = await cron.run("legacy-every", "due");
|
||||
expect(sfRun).toEqual({ ok: true, ran: true });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CronEvent } from "./service.js";
|
||||
import { CronService } from "./service.js";
|
||||
|
||||
@@ -12,14 +12,14 @@ const noopLogger = {
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
let fixtureRoot = "";
|
||||
let caseId = 0;
|
||||
|
||||
async function makeStorePath() {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-16156-"));
|
||||
return {
|
||||
storePath: path.join(dir, "cron", "jobs.json"),
|
||||
cleanup: async () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
const dir = path.join(fixtureRoot, `case-${caseId++}`);
|
||||
const storePath = path.join(dir, "cron", "jobs.json");
|
||||
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
||||
return { storePath };
|
||||
}
|
||||
|
||||
function createFinishedBarrier() {
|
||||
@@ -44,6 +44,16 @@ function createFinishedBarrier() {
|
||||
}
|
||||
|
||||
describe("#16156: cron.list() must not silently advance past-due recurring jobs", () => {
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-16156-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (fixtureRoot) {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2025-12-13T00:00:00.000Z"));
|
||||
@@ -119,7 +129,6 @@ describe("#16156: cron.list() must not silently advance past-due recurring jobs"
|
||||
expect(updated?.state.nextRunAtMs).toBeGreaterThan(firstDueAt);
|
||||
|
||||
cron.stop();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("does not skip a cron job when status() is called while the job is past-due", async () => {
|
||||
@@ -172,7 +181,6 @@ describe("#16156: cron.list() must not silently advance past-due recurring jobs"
|
||||
expect(updated?.state.lastStatus).toBe("ok");
|
||||
|
||||
cron.stop();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("still fills missing nextRunAtMs via list() for enabled jobs", async () => {
|
||||
@@ -226,6 +234,5 @@ describe("#16156: cron.list() must not silently advance past-due recurring jobs"
|
||||
expect(job?.state.nextRunAtMs).toBeGreaterThan(nowMs);
|
||||
|
||||
cron.stop();
|
||||
await store.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,9 +24,6 @@ async function makeStorePath() {
|
||||
const storePath = path.join(dir, "jobs.json");
|
||||
return {
|
||||
storePath,
|
||||
cleanup: async () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,7 +149,6 @@ describe("Cron issue regressions", () => {
|
||||
}
|
||||
|
||||
cron.stop();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("repairs missing nextRunAtMs on non-schedule updates without touching other jobs", async () => {
|
||||
@@ -183,7 +179,6 @@ describe("Cron issue regressions", () => {
|
||||
expect(updated.state.nextRunAtMs).toBe(created.state.nextRunAtMs);
|
||||
|
||||
cron.stop();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("does not advance unrelated due jobs when updating another job", async () => {
|
||||
@@ -230,7 +225,6 @@ describe("Cron issue regressions", () => {
|
||||
expect(persistedDueJob?.state?.nextRunAtMs).toBe(originalDueNextRunAtMs);
|
||||
|
||||
cron.stop();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("treats persisted jobs with missing enabled as enabled during update()", async () => {
|
||||
@@ -366,7 +360,6 @@ describe("Cron issue regressions", () => {
|
||||
|
||||
cron.stop();
|
||||
timeoutSpy.mockRestore();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("re-arms timer without hot-looping when a run is already in progress", async () => {
|
||||
@@ -400,7 +393,6 @@ describe("Cron issue regressions", () => {
|
||||
.filter((d): d is number => typeof d === "number");
|
||||
expect(delays).toContain(60_000);
|
||||
timeoutSpy.mockRestore();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("skips forced manual runs while a timer-triggered run is in progress", async () => {
|
||||
@@ -467,7 +459,6 @@ describe("Cron issue regressions", () => {
|
||||
await cron.list({ includeDisabled: true });
|
||||
|
||||
cron.stop();
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("#13845: one-shot jobs with terminal statuses do not re-fire on restart", async () => {
|
||||
@@ -523,7 +514,6 @@ describe("Cron issue regressions", () => {
|
||||
expect(enqueueSystemEvent).not.toHaveBeenCalled();
|
||||
cron.stop();
|
||||
}
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("records per-job start time and duration for batched due jobs", async () => {
|
||||
@@ -569,7 +559,5 @@ describe("Cron issue regressions", () => {
|
||||
expect(secondDone?.state.lastRunAtMs).toBe(dueAt + 50);
|
||||
expect(secondDone?.state.lastDurationMs).toBe(20);
|
||||
expect(startedAtEvents).toEqual([dueAt, dueAt + 50]);
|
||||
|
||||
await store.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,201 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HeartbeatRunResult } from "../infra/heartbeat-wake.js";
|
||||
import type { CronEvent } from "./service.js";
|
||||
import { CronService } from "./service.js";
|
||||
import {
|
||||
createCronStoreHarness,
|
||||
createNoopLogger,
|
||||
installCronTestHooks,
|
||||
} from "./service.test-harness.js";
|
||||
import { createNoopLogger, installCronTestHooks } from "./service.test-harness.js";
|
||||
|
||||
const noopLogger = createNoopLogger();
|
||||
const { makeStorePath } = createCronStoreHarness();
|
||||
installCronTestHooks({ logger: noopLogger });
|
||||
|
||||
type FakeFsEntry =
|
||||
| { kind: "file"; content: string; mtimeMs: number }
|
||||
| { kind: "dir"; mtimeMs: number };
|
||||
|
||||
const fsState = vi.hoisted(() => ({
|
||||
entries: new Map<string, FakeFsEntry>(),
|
||||
nowMs: 0,
|
||||
fixtureCount: 0,
|
||||
}));
|
||||
|
||||
const abs = (p: string) => path.resolve(p);
|
||||
const fixturesRoot = abs(path.join("__openclaw_vitest__", "cron", "runs-one-shot"));
|
||||
const isFixturePath = (p: string) => {
|
||||
const resolved = abs(p);
|
||||
const rootPrefix = `${fixturesRoot}${path.sep}`;
|
||||
return resolved === fixturesRoot || resolved.startsWith(rootPrefix);
|
||||
};
|
||||
|
||||
function bumpMtimeMs() {
|
||||
fsState.nowMs += 1;
|
||||
return fsState.nowMs;
|
||||
}
|
||||
|
||||
function ensureDir(dirPath: string) {
|
||||
let current = abs(dirPath);
|
||||
while (true) {
|
||||
if (!fsState.entries.has(current)) {
|
||||
fsState.entries.set(current, { kind: "dir", mtimeMs: bumpMtimeMs() });
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function setFile(filePath: string, content: string) {
|
||||
const resolved = abs(filePath);
|
||||
ensureDir(path.dirname(resolved));
|
||||
fsState.entries.set(resolved, { kind: "file", content, mtimeMs: bumpMtimeMs() });
|
||||
}
|
||||
|
||||
async function makeStorePath() {
|
||||
const dir = path.join(fixturesRoot, `case-${fsState.fixtureCount++}`);
|
||||
ensureDir(dir);
|
||||
const storePath = path.join(dir, "cron", "jobs.json");
|
||||
ensureDir(path.dirname(storePath));
|
||||
return { storePath, cleanup: async () => {} };
|
||||
}
|
||||
|
||||
function writeStoreFile(storePath: string, payload: unknown) {
|
||||
setFile(storePath, JSON.stringify(payload, null, 2));
|
||||
}
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
const pathMod = await import("node:path");
|
||||
const absInMock = (p: string) => pathMod.resolve(p);
|
||||
const isFixtureInMock = (p: string) => {
|
||||
const resolved = absInMock(p);
|
||||
const rootPrefix = `${absInMock(fixturesRoot)}${pathMod.sep}`;
|
||||
return resolved === absInMock(fixturesRoot) || resolved.startsWith(rootPrefix);
|
||||
};
|
||||
|
||||
const mkErr = (code: string, message: string) => Object.assign(new Error(message), { code });
|
||||
|
||||
const promises = {
|
||||
...actual.promises,
|
||||
mkdir: async (p: string) => {
|
||||
if (!isFixtureInMock(p)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.promises.mkdir as any)(p, { recursive: true });
|
||||
}
|
||||
ensureDir(p);
|
||||
},
|
||||
readFile: async (p: string) => {
|
||||
if (!isFixtureInMock(p)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.promises.readFile as any)(p, "utf-8");
|
||||
}
|
||||
const entry = fsState.entries.get(absInMock(p));
|
||||
if (!entry || entry.kind !== "file") {
|
||||
throw mkErr("ENOENT", `ENOENT: no such file or directory, open '${p}'`);
|
||||
}
|
||||
return entry.content;
|
||||
},
|
||||
writeFile: async (p: string, data: string | Uint8Array) => {
|
||||
if (!isFixtureInMock(p)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.promises.writeFile as any)(p, data, "utf-8");
|
||||
}
|
||||
const content = typeof data === "string" ? data : Buffer.from(data).toString("utf-8");
|
||||
setFile(p, content);
|
||||
},
|
||||
rename: async (from: string, to: string) => {
|
||||
if (!isFixtureInMock(from) || !isFixtureInMock(to)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.promises.rename as any)(from, to);
|
||||
}
|
||||
const fromAbs = absInMock(from);
|
||||
const toAbs = absInMock(to);
|
||||
const entry = fsState.entries.get(fromAbs);
|
||||
if (!entry || entry.kind !== "file") {
|
||||
throw mkErr("ENOENT", `ENOENT: no such file or directory, rename '${from}' -> '${to}'`);
|
||||
}
|
||||
ensureDir(pathMod.dirname(toAbs));
|
||||
fsState.entries.delete(fromAbs);
|
||||
fsState.entries.set(toAbs, { ...entry, mtimeMs: bumpMtimeMs() });
|
||||
},
|
||||
copyFile: async (from: string, to: string) => {
|
||||
if (!isFixtureInMock(from) || !isFixtureInMock(to)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.promises.copyFile as any)(from, to);
|
||||
}
|
||||
const entry = fsState.entries.get(absInMock(from));
|
||||
if (!entry || entry.kind !== "file") {
|
||||
throw mkErr("ENOENT", `ENOENT: no such file or directory, copyfile '${from}' -> '${to}'`);
|
||||
}
|
||||
setFile(to, entry.content);
|
||||
},
|
||||
stat: async (p: string) => {
|
||||
if (!isFixtureInMock(p)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.promises.stat as any)(p);
|
||||
}
|
||||
const entry = fsState.entries.get(absInMock(p));
|
||||
if (!entry) {
|
||||
throw mkErr("ENOENT", `ENOENT: no such file or directory, stat '${p}'`);
|
||||
}
|
||||
return {
|
||||
mtimeMs: entry.mtimeMs,
|
||||
isDirectory: () => entry.kind === "dir",
|
||||
isFile: () => entry.kind === "file",
|
||||
};
|
||||
},
|
||||
access: async (p: string) => {
|
||||
if (!isFixtureInMock(p)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.promises.access as any)(p);
|
||||
}
|
||||
const entry = fsState.entries.get(absInMock(p));
|
||||
if (!entry) {
|
||||
throw mkErr("ENOENT", `ENOENT: no such file or directory, access '${p}'`);
|
||||
}
|
||||
},
|
||||
unlink: async (p: string) => {
|
||||
if (!isFixtureInMock(p)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.promises.unlink as any)(p);
|
||||
}
|
||||
fsState.entries.delete(absInMock(p));
|
||||
},
|
||||
} satisfies typeof actual.promises;
|
||||
|
||||
const wrapped = { ...actual, promises };
|
||||
return { ...wrapped, default: wrapped };
|
||||
});
|
||||
|
||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
||||
const wrapped = {
|
||||
...actual,
|
||||
mkdir: async (p: string, _opts?: unknown) => {
|
||||
if (!isFixturePath(p)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.mkdir as any)(p, { recursive: true });
|
||||
}
|
||||
ensureDir(p);
|
||||
},
|
||||
writeFile: async (p: string, data: string, _enc?: unknown) => {
|
||||
if (!isFixturePath(p)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await (actual.writeFile as any)(p, data, "utf-8");
|
||||
}
|
||||
setFile(p, data);
|
||||
},
|
||||
};
|
||||
return { ...wrapped, default: wrapped };
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fsState.entries.clear();
|
||||
fsState.nowMs = 0;
|
||||
fsState.fixtureCount = 0;
|
||||
ensureDir(fixturesRoot);
|
||||
});
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
@@ -57,35 +239,8 @@ function createCronEventHarness() {
|
||||
}
|
||||
|
||||
describe("CronService", () => {
|
||||
async function loadLegacyJobFromStore(rawJob: Record<string, unknown>) {
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
|
||||
await fs.mkdir(path.dirname(store.storePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
store.storePath,
|
||||
JSON.stringify({ version: 1, jobs: [rawJob] }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const cron = new CronService({
|
||||
storePath: store.storePath,
|
||||
cronEnabled: true,
|
||||
log: noopLogger,
|
||||
enqueueSystemEvent,
|
||||
requestHeartbeatNow,
|
||||
runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })),
|
||||
});
|
||||
|
||||
await cron.start();
|
||||
const jobs = await cron.list({ includeDisabled: true });
|
||||
const job = jobs.find((j) => j.id === rawJob.id);
|
||||
|
||||
return { cron, store, enqueueSystemEvent, requestHeartbeatNow, job };
|
||||
}
|
||||
|
||||
it("runs a one-shot main job and disables it after success when requested", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
@@ -134,6 +289,7 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("runs a one-shot job and deletes it after success by default", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
@@ -177,6 +333,7 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("wakeMode now waits for heartbeat completion when available", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
@@ -244,6 +401,7 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("passes agentId to runHeartbeatOnce for main-session wakeMode now jobs", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
@@ -293,6 +451,7 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("wakeMode now falls back to queued heartbeat when main lane stays busy", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
@@ -343,6 +502,7 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("runs an isolated job and posts summary to main", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
@@ -391,6 +551,7 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("does not post isolated summary to main when run already delivered output", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
@@ -437,6 +598,11 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("migrates legacy payload.provider to payload.channel on load", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
|
||||
const rawJob = {
|
||||
id: "legacy-1",
|
||||
name: "legacy",
|
||||
@@ -456,7 +622,20 @@ describe("CronService", () => {
|
||||
state: {},
|
||||
};
|
||||
|
||||
const { cron, store, job } = await loadLegacyJobFromStore(rawJob);
|
||||
writeStoreFile(store.storePath, { version: 1, jobs: [rawJob] });
|
||||
|
||||
const cron = new CronService({
|
||||
storePath: store.storePath,
|
||||
cronEnabled: true,
|
||||
log: noopLogger,
|
||||
enqueueSystemEvent,
|
||||
requestHeartbeatNow,
|
||||
runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })),
|
||||
});
|
||||
|
||||
await cron.start();
|
||||
const jobs = await cron.list({ includeDisabled: true });
|
||||
const job = jobs.find((j) => j.id === rawJob.id);
|
||||
// Legacy delivery fields are migrated to the top-level delivery object
|
||||
const delivery = job?.delivery as unknown as Record<string, unknown>;
|
||||
expect(delivery?.channel).toBe("telegram");
|
||||
@@ -469,6 +648,11 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("canonicalizes payload.channel casing on load", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
|
||||
const rawJob = {
|
||||
id: "legacy-2",
|
||||
name: "legacy",
|
||||
@@ -488,7 +672,20 @@ describe("CronService", () => {
|
||||
state: {},
|
||||
};
|
||||
|
||||
const { cron, store, job } = await loadLegacyJobFromStore(rawJob);
|
||||
writeStoreFile(store.storePath, { version: 1, jobs: [rawJob] });
|
||||
|
||||
const cron = new CronService({
|
||||
storePath: store.storePath,
|
||||
cronEnabled: true,
|
||||
log: noopLogger,
|
||||
enqueueSystemEvent,
|
||||
requestHeartbeatNow,
|
||||
runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })),
|
||||
});
|
||||
|
||||
await cron.start();
|
||||
const jobs = await cron.list({ includeDisabled: true });
|
||||
const job = jobs.find((j) => j.id === rawJob.id);
|
||||
// Legacy delivery fields are migrated to the top-level delivery object
|
||||
const delivery = job?.delivery as unknown as Record<string, unknown>;
|
||||
expect(delivery?.channel).toBe("telegram");
|
||||
@@ -498,6 +695,7 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("posts last output to main even when isolated job errors", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
@@ -546,6 +744,7 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("rejects unsupported session/payload combinations", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
|
||||
const cron = new CronService({
|
||||
@@ -586,32 +785,29 @@ describe("CronService", () => {
|
||||
});
|
||||
|
||||
it("skips invalid main jobs with agentTurn payloads from disk", async () => {
|
||||
ensureDir(fixturesRoot);
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
const events = createCronEventHarness();
|
||||
|
||||
const atMs = Date.parse("2025-12-13T00:00:01.000Z");
|
||||
await fs.mkdir(path.dirname(store.storePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
store.storePath,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
jobs: [
|
||||
{
|
||||
id: "job-1",
|
||||
enabled: true,
|
||||
createdAtMs: Date.parse("2025-12-13T00:00:00.000Z"),
|
||||
updatedAtMs: Date.parse("2025-12-13T00:00:00.000Z"),
|
||||
schedule: { kind: "at", at: new Date(atMs).toISOString() },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "now",
|
||||
payload: { kind: "agentTurn", message: "bad" },
|
||||
state: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
writeStoreFile(store.storePath, {
|
||||
version: 1,
|
||||
jobs: [
|
||||
{
|
||||
id: "job-1",
|
||||
enabled: true,
|
||||
createdAtMs: Date.parse("2025-12-13T00:00:00.000Z"),
|
||||
updatedAtMs: Date.parse("2025-12-13T00:00:00.000Z"),
|
||||
schedule: { kind: "at", at: new Date(atMs).toISOString() },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "now",
|
||||
payload: { kind: "agentTurn", message: "bad" },
|
||||
state: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const cron = new CronService({
|
||||
storePath: store.storePath,
|
||||
|
||||
Reference in New Issue
Block a user