import fs from "node:fs/promises" ;
import path from "node:path" ;
import { describe, expect, it, vi } from "vitest" ;
import { setupCronServiceSuite } from "../service.test-harness.js" ;
import { findJobOrThrow } from "./jobs.js" ;
import { createCronServiceState } from "./state.js" ;
import { ensureLoaded, persist } from "./store.js" ;
const { logger, makeStorePath } = setupCronServiceSuite({
prefix: "cron-service-store-seam" ,
});
const STORE_TEST_NOW = Date.parse("2026-03-23T12:00:00.000Z" );
async function writeSingleJobStore(storePath: string, job: Record<string, unknown>) {
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(
storePath,
JSON.stringify(
{
version: 1 ,
jobs: [job],
},
null ,
2 ,
),
"utf8" ,
);
}
function createStoreTestState(storePath: string) {
return createCronServiceState({
storePath,
cronEnabled: true ,
log: logger,
nowMs: () => STORE_TEST_NOW,
enqueueSystemEvent: vi.fn(),
requestHeartbeatNow: vi.fn(),
runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })),
});
}
describe("cron service store seam coverage" , () => {
it("loads stored jobs, recomputes next runs, and does not rewrite the store on load" , async () => {
const { storePath } = await makeStorePath();
await writeSingleJobStore(storePath, {
id: "modern-job" ,
name: "modern job" ,
enabled: true ,
createdAtMs: STORE_TEST_NOW - 60 _000 ,
updatedAtMs: STORE_TEST_NOW - 60 _000 ,
schedule: { kind: "every" , everyMs: 60 _000 },
sessionTarget: "isolated" ,
wakeMode: "now" ,
payload: { kind: "agentTurn" , message: "ping" },
delivery: { mode: "announce" , channel: "telegram" , to: "123" },
state: {},
});
const state = createStoreTestState(storePath);
await ensureLoaded(state);
const job = state.store?.jobs[0 ];
expect(job).toBeDefined();
expect(job?.sessionTarget).toBe("isolated" );
expect(job?.payload.kind).toBe("agentTurn" );
if (job?.payload.kind === "agentTurn" ) {
expect(job.payload.message).toBe("ping" );
}
expect(job?.delivery).toMatchObject({
mode: "announce" ,
channel: "telegram" ,
to: "123" ,
});
expect(job?.state.nextRunAtMs).toBe(STORE_TEST_NOW);
const persisted = JSON.parse(await fs.readFile(storePath, "utf8" )) as {
jobs: Array<Record<string, unknown>>;
};
const persistedJob = persisted.jobs[0 ];
expect(persistedJob?.payload).toMatchObject({
kind: "agentTurn" ,
message: "ping" ,
});
expect(persistedJob?.delivery).toMatchObject({
mode: "announce" ,
channel: "telegram" ,
to: "123" ,
});
const firstMtime = state.storeFileMtimeMs;
expect(typeof firstMtime).toBe("number" );
await persist(state);
expect(typeof state.storeFileMtimeMs).toBe("number" );
expect((state.storeFileMtimeMs ?? 0 ) >= (firstMtime ?? 0 )).toBe(true );
});
it("normalizes jobId-only jobs in memory so scheduler lookups resolve by stable id" , async () => {
const { storePath } = await makeStorePath();
await writeSingleJobStore(storePath, {
jobId: "repro-stable-id" ,
name: "handed" ,
enabled: true ,
createdAtMs: STORE_TEST_NOW - 60 _000 ,
updatedAtMs: STORE_TEST_NOW - 60 _000 ,
schedule: { kind: "every" , everyMs: 60 _000 },
sessionTarget: "main" ,
wakeMode: "now" ,
payload: { kind: "systemEvent" , text: "tick" },
state: {},
});
const state = createStoreTestState(storePath);
await ensureLoaded(state);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({ storePath, jobId: "repro-stable-id" }),
expect.stringContaining("legacy jobId" ),
);
const job = findJobOrThrow(state, "repro-stable-id" );
expect(job.id).toBe("repro-stable-id" );
expect((job as { jobId?: unknown }).jobId).toBeUndefined();
const raw = JSON.parse(await fs.readFile(storePath, "utf8" )) as {
jobs: Array<Record<string, unknown>>;
};
expect(raw.jobs[0 ]?.jobId).toBe("repro-stable-id" );
expect(raw.jobs[0 ]?.id).toBeUndefined();
});
it("preserves disabled jobs when persisted booleans roundtrip through string values" , async () => {
const { storePath } = await makeStorePath();
await writeSingleJobStore(storePath, {
id: "disabled-string-job" ,
name: "disabled string job" ,
enabled: "false" ,
createdAtMs: STORE_TEST_NOW - 60 _000 ,
updatedAtMs: STORE_TEST_NOW - 60 _000 ,
schedule: { kind: "every" , everyMs: 60 _000 },
sessionTarget: "main" ,
wakeMode: "now" ,
payload: { kind: "systemEvent" , text: "tick" },
state: {},
});
const before = await fs.readFile(storePath, "utf8" );
const state = createStoreTestState(storePath);
await ensureLoaded(state);
const job = findJobOrThrow(state, "disabled-string-job" );
expect(job.enabled).toBe(false );
const after = await fs.readFile(storePath, "utf8" );
expect(after).toBe(before);
});
it("loads persisted jobs with unsafe custom session ids so run paths can fail closed" , async () => {
const { storePath } = await makeStorePath();
await writeSingleJobStore(storePath, {
id: "unsafe-session-target-job" ,
name: "unsafe session target job" ,
enabled: true ,
createdAtMs: STORE_TEST_NOW - 60 _000 ,
updatedAtMs: STORE_TEST_NOW - 60 _000 ,
schedule: { kind: "every" , everyMs: 60 _000 },
sessionTarget: "session:../../outside" ,
wakeMode: "now" ,
payload: { kind: "agentTurn" , message: "ping" },
state: {},
});
const state = createStoreTestState(storePath);
await ensureLoaded(state, { skipRecompute: true });
const job = findJobOrThrow(state, "unsafe-session-target-job" );
expect(job.sessionTarget).toBe("session:../../outside" );
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({ storePath, jobId: "unsafe-session-target-job" }),
expect.stringContaining("invalid persisted sessionTarget" ),
);
});
});
Messung V0.5 in Prozent C=98 H=98 G=97
¤ Dauer der Verarbeitung: 0.8 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland