From d015dc921630fab6cdf34dfda08c6483b04c93c0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 19:33:08 +0000 Subject: [PATCH] test(cron): dedupe run-log temp fixtures and cover invalid line filtering --- src/cron/run-log.test.ts | 267 ++++++++++++++++++++++----------------- 1 file changed, 150 insertions(+), 117 deletions(-) diff --git a/src/cron/run-log.test.ts b/src/cron/run-log.test.ts index 0a7e5c319..a2a31970b 100644 --- a/src/cron/run-log.test.ts +++ b/src/cron/run-log.test.ts @@ -5,6 +5,15 @@ import { describe, expect, it } from "vitest"; import { appendCronRunLog, readCronRunLogEntries, resolveCronRunLogPath } from "./run-log.js"; describe("cron run log", () => { + async function withRunLogDir(prefix: string, run: (dir: string) => Promise) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); + try { + await run(dir); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + } + it("resolves store path to per-job runs/.jsonl", () => { const storePath = path.join(os.tmpdir(), "cron", "jobs.json"); const p = resolveCronRunLogPath({ storePath, jobId: "job-1" }); @@ -12,140 +21,164 @@ describe("cron run log", () => { }); it("appends JSONL and prunes by line count", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-log-")); - const logPath = path.join(dir, "runs", "job-1.jsonl"); + await withRunLogDir("openclaw-cron-log-", async (dir) => { + const logPath = path.join(dir, "runs", "job-1.jsonl"); - for (let i = 0; i < 10; i++) { - await appendCronRunLog( - logPath, - { - ts: 1000 + i, - jobId: "job-1", - action: "finished", - status: "ok", - durationMs: i, - }, - { maxBytes: 1, keepLines: 3 }, - ); - } + for (let i = 0; i < 10; i++) { + await appendCronRunLog( + logPath, + { + ts: 1000 + i, + jobId: "job-1", + action: "finished", + status: "ok", + durationMs: i, + }, + { maxBytes: 1, keepLines: 3 }, + ); + } - const raw = await fs.readFile(logPath, "utf-8"); - const lines = raw - .split("\n") - .map((l) => l.trim()) - .filter(Boolean); - expect(lines.length).toBe(3); - const last = JSON.parse(lines[2] ?? "{}") as { ts?: number }; - expect(last.ts).toBe(1009); - - await fs.rm(dir, { recursive: true, force: true }); + const raw = await fs.readFile(logPath, "utf-8"); + const lines = raw + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + expect(lines.length).toBe(3); + const last = JSON.parse(lines[2] ?? "{}") as { ts?: number }; + expect(last.ts).toBe(1009); + }); }); it("reads newest entries and filters by jobId", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-log-read-")); - const logPathA = path.join(dir, "runs", "a.jsonl"); - const logPathB = path.join(dir, "runs", "b.jsonl"); + await withRunLogDir("openclaw-cron-log-read-", async (dir) => { + const logPathA = path.join(dir, "runs", "a.jsonl"); + const logPathB = path.join(dir, "runs", "b.jsonl"); - await appendCronRunLog(logPathA, { - ts: 1, - jobId: "a", - action: "finished", - status: "ok", + await appendCronRunLog(logPathA, { + ts: 1, + jobId: "a", + action: "finished", + status: "ok", + }); + await appendCronRunLog(logPathB, { + ts: 2, + jobId: "b", + action: "finished", + status: "error", + error: "nope", + summary: "oops", + }); + await appendCronRunLog(logPathA, { + ts: 3, + jobId: "a", + action: "finished", + status: "skipped", + sessionId: "run-123", + sessionKey: "agent:main:cron:a:run:run-123", + }); + + const allA = await readCronRunLogEntries(logPathA, { limit: 10 }); + expect(allA.map((e) => e.jobId)).toEqual(["a", "a"]); + + const onlyA = await readCronRunLogEntries(logPathA, { + limit: 10, + jobId: "a", + }); + expect(onlyA.map((e) => e.ts)).toEqual([1, 3]); + + const lastOne = await readCronRunLogEntries(logPathA, { limit: 1 }); + expect(lastOne.map((e) => e.ts)).toEqual([3]); + expect(lastOne[0]?.sessionId).toBe("run-123"); + expect(lastOne[0]?.sessionKey).toBe("agent:main:cron:a:run:run-123"); + + const onlyB = await readCronRunLogEntries(logPathB, { + limit: 10, + jobId: "b", + }); + expect(onlyB[0]?.summary).toBe("oops"); + + const wrongFilter = await readCronRunLogEntries(logPathA, { + limit: 10, + jobId: "b", + }); + expect(wrongFilter).toEqual([]); }); - await appendCronRunLog(logPathB, { - ts: 2, - jobId: "b", - action: "finished", - status: "error", - error: "nope", - summary: "oops", + }); + + it("ignores invalid and non-finished lines while preserving delivered flag", async () => { + await withRunLogDir("openclaw-cron-log-filter-", async (dir) => { + const logPath = path.join(dir, "runs", "job-1.jsonl"); + await fs.mkdir(path.dirname(logPath), { recursive: true }); + await fs.writeFile( + logPath, + [ + '{"bad":', + JSON.stringify({ ts: 1, jobId: "job-1", action: "started", status: "ok" }), + JSON.stringify({ + ts: 2, + jobId: "job-1", + action: "finished", + status: "ok", + delivered: true, + }), + ].join("\n") + "\n", + "utf-8", + ); + + const entries = await readCronRunLogEntries(logPath, { limit: 10, jobId: "job-1" }); + expect(entries).toHaveLength(1); + expect(entries[0]?.ts).toBe(2); + expect(entries[0]?.delivered).toBe(true); }); - await appendCronRunLog(logPathA, { - ts: 3, - jobId: "a", - action: "finished", - status: "skipped", - sessionId: "run-123", - sessionKey: "agent:main:cron:a:run:run-123", - }); - - const allA = await readCronRunLogEntries(logPathA, { limit: 10 }); - expect(allA.map((e) => e.jobId)).toEqual(["a", "a"]); - - const onlyA = await readCronRunLogEntries(logPathA, { - limit: 10, - jobId: "a", - }); - expect(onlyA.map((e) => e.ts)).toEqual([1, 3]); - - const lastOne = await readCronRunLogEntries(logPathA, { limit: 1 }); - expect(lastOne.map((e) => e.ts)).toEqual([3]); - expect(lastOne[0]?.sessionId).toBe("run-123"); - expect(lastOne[0]?.sessionKey).toBe("agent:main:cron:a:run:run-123"); - - const onlyB = await readCronRunLogEntries(logPathB, { - limit: 10, - jobId: "b", - }); - expect(onlyB[0]?.summary).toBe("oops"); - - const wrongFilter = await readCronRunLogEntries(logPathA, { - limit: 10, - jobId: "b", - }); - expect(wrongFilter).toEqual([]); - - await fs.rm(dir, { recursive: true, force: true }); }); it("reads telemetry fields", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-log-telemetry-")); - const logPath = path.join(dir, "runs", "job-1.jsonl"); + await withRunLogDir("openclaw-cron-log-telemetry-", async (dir) => { + const logPath = path.join(dir, "runs", "job-1.jsonl"); - await appendCronRunLog(logPath, { - ts: 1, - jobId: "job-1", - action: "finished", - status: "ok", - model: "gpt-5.2", - provider: "openai", - usage: { + await appendCronRunLog(logPath, { + ts: 1, + jobId: "job-1", + action: "finished", + status: "ok", + model: "gpt-5.2", + provider: "openai", + usage: { + input_tokens: 10, + output_tokens: 5, + total_tokens: 15, + cache_read_tokens: 2, + cache_write_tokens: 1, + }, + }); + + await fs.appendFile( + logPath, + `${JSON.stringify({ + ts: 2, + jobId: "job-1", + action: "finished", + status: "ok", + model: " ", + provider: "", + usage: { input_tokens: "oops" }, + })}\n`, + "utf-8", + ); + + const entries = await readCronRunLogEntries(logPath, { limit: 10, jobId: "job-1" }); + expect(entries[0]?.model).toBe("gpt-5.2"); + expect(entries[0]?.provider).toBe("openai"); + expect(entries[0]?.usage).toEqual({ input_tokens: 10, output_tokens: 5, total_tokens: 15, cache_read_tokens: 2, cache_write_tokens: 1, - }, + }); + expect(entries[1]?.model).toBeUndefined(); + expect(entries[1]?.provider).toBeUndefined(); + expect(entries[1]?.usage?.input_tokens).toBeUndefined(); }); - - await fs.appendFile( - logPath, - `${JSON.stringify({ - ts: 2, - jobId: "job-1", - action: "finished", - status: "ok", - model: " ", - provider: "", - usage: { input_tokens: "oops" }, - })}\n`, - "utf-8", - ); - - const entries = await readCronRunLogEntries(logPath, { limit: 10, jobId: "job-1" }); - expect(entries[0]?.model).toBe("gpt-5.2"); - expect(entries[0]?.provider).toBe("openai"); - expect(entries[0]?.usage).toEqual({ - input_tokens: 10, - output_tokens: 5, - total_tokens: 15, - cache_read_tokens: 2, - cache_write_tokens: 1, - }); - expect(entries[1]?.model).toBeUndefined(); - expect(entries[1]?.provider).toBeUndefined(); - expect(entries[1]?.usage?.input_tokens).toBeUndefined(); - - await fs.rm(dir, { recursive: true, force: true }); }); });