openclaw/apps/web/app/api/cron/cron.test.ts
2026-02-16 01:01:12 -08:00

154 lines
5.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock node:fs
vi.mock("node:fs", () => ({
existsSync: vi.fn(() => false),
readFileSync: vi.fn(() => ""),
readdirSync: vi.fn(() => []),
statSync: vi.fn(() => ({ mtimeMs: Date.now() })),
}));
// Mock node:os
vi.mock("node:os", () => ({
homedir: vi.fn(() => "/home/testuser"),
}));
describe("Cron API routes", () => {
beforeEach(() => {
vi.resetModules();
vi.mock("node:fs", () => ({
existsSync: vi.fn(() => false),
readFileSync: vi.fn(() => ""),
readdirSync: vi.fn(() => []),
statSync: vi.fn(() => ({ mtimeMs: Date.now() })),
}));
vi.mock("node:os", () => ({
homedir: vi.fn(() => "/home/testuser"),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
// ─── GET /api/cron/jobs ─────────────────────────────────────────
describe("GET /api/cron/jobs", () => {
it("returns empty jobs when no config file", async () => {
const { GET } = await import("./jobs/route.js");
const res = await GET();
const json = await res.json();
expect(json.jobs).toEqual([]);
});
it("returns jobs from config file", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
const cronStore = {
version: 1,
jobs: [
{ id: "j1", name: "Daily sync", schedule: "0 8 * * *", enabled: true, command: "sync" },
],
};
vi.mocked(mockReadFile).mockReturnValue(JSON.stringify(cronStore) as never);
const { GET } = await import("./jobs/route.js");
const res = await GET();
const json = await res.json();
expect(json.jobs).toHaveLength(1);
expect(json.jobs[0].name).toBe("Daily sync");
});
it("handles corrupt jobs file gracefully", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
vi.mocked(mockReadFile).mockReturnValue("not json" as never);
const { GET } = await import("./jobs/route.js");
const res = await GET();
const json = await res.json();
expect(json.jobs).toEqual([]);
});
});
// ─── GET /api/cron/jobs/[jobId]/runs ────────────────────────────
describe("GET /api/cron/jobs/[jobId]/runs", () => {
it("returns empty entries when no runs file", async () => {
const { GET } = await import("./jobs/[jobId]/runs/route.js");
const res = await GET(
new Request("http://localhost/api/cron/jobs/j1/runs"),
{ params: Promise.resolve({ jobId: "j1" }) },
);
const json = await res.json();
expect(json.entries).toEqual([]);
});
it("returns run entries from jsonl file", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
const lines = [
JSON.stringify({ ts: 1000, jobId: "j1", action: "finished", status: "completed", summary: "Done" }),
JSON.stringify({ ts: 2000, jobId: "j1", action: "finished", status: "completed", summary: "In progress" }),
].join("\n");
vi.mocked(mockReadFile).mockReturnValue(lines as never);
const { GET } = await import("./jobs/[jobId]/runs/route.js");
const res = await GET(
new Request("http://localhost/api/cron/jobs/j1/runs"),
{ params: Promise.resolve({ jobId: "j1" }) },
);
const json = await res.json();
expect(json.entries.length).toBeGreaterThan(0);
});
it("respects limit query param", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
const lines = Array.from({ length: 50 }, (_, i) =>
JSON.stringify({ ts: i, status: "completed" }),
).join("\n");
vi.mocked(mockReadFile).mockReturnValue(lines as never);
const { GET } = await import("./jobs/[jobId]/runs/route.js");
const res = await GET(
new Request("http://localhost/api/cron/jobs/j1/runs?limit=5"),
{ params: Promise.resolve({ jobId: "j1" }) },
);
const json = await res.json();
expect(json.entries.length).toBeLessThanOrEqual(5);
});
});
// ─── GET /api/cron/runs/[sessionId] ─────────────────────────────
describe("GET /api/cron/runs/[sessionId]", () => {
it("returns 404 when session not found", async () => {
const { GET } = await import("./runs/[sessionId]/route.js");
const res = await GET(
new Request("http://localhost"),
{ params: Promise.resolve({ sessionId: "nonexistent" }) },
);
expect(res.status).toBe(404);
});
});
// ─── GET /api/cron/runs/search-transcript ───────────────────────
describe("GET /api/cron/runs/search-transcript", () => {
it("returns 400 when missing required params", async () => {
const { GET } = await import("./runs/search-transcript/route.js");
const req = new Request("http://localhost/api/cron/runs/search-transcript");
const res = await GET(req);
expect(res.status).toBe(400);
});
it("returns 404 when no transcript found", async () => {
const { GET } = await import("./runs/search-transcript/route.js");
const req = new Request("http://localhost/api/cron/runs/search-transcript?jobId=j1&runAtMs=1000");
const res = await GET(req);
expect(res.status).toBe(404);
});
});
});