import { beforeEach, describe, expect, it, vi } from "vitest"; const mockState = vi.hoisted(() => ({ schedules: [] as Array>, startedRuns: [] as Array>, finishedRuns: [] as Array>, settings: { enabled: true, catch_up_on_startup: true, max_run_history: 100, max_catch_up_runs: 5, max_concurrent_schedule_runs: 1, }, })); vi.mock("../startupPorts", () => ({ getConfiguredPorts: () => ({ agent: 60006 }), })); vi.mock("../constants", () => ({ LOCALHOST_HOSTNAME: "127.0.0.1", })); vi.mock("./stateService", () => ({ readDigitalEmployeeCronSettings: () => mockState.settings, listDigitalEmployeeSchedules: () => mockState.schedules, listDueDigitalEmployeeSchedules: () => mockState.schedules, readDigitalEmployeeScheduleRuns: () => [], upsertDigitalEmployeeSchedule: vi.fn(), recordDigitalEmployeeScheduleRunStarted: (input: Record) => { const run = { id: `${input.scheduleId}:${input.dueAt}:1`, ...input }; mockState.startedRuns.push(run); return run; }, finishDigitalEmployeeScheduleRun: (id: string, status: string, options: Record) => { mockState.finishedRuns.push({ id, status, ...options }); return { id, status, ...options }; }, })); describe("digital employee scheduler service", () => { beforeEach(() => { vi.resetModules(); mockState.startedRuns = []; mockState.finishedRuns = []; mockState.schedules = [{ id: "schedule-1", planId: "plan-1", name: "晨间巡检", description: "每日巡检", cronExpr: "0 9 * * *", timezone: "system", status: "enabled", enabled: true, prompt: "执行晨间巡检", command: null, payload: {}, nextRunAt: "2026-06-07T01:00:00.000Z", lastRunAt: null, catchUpOnStartup: true, maxCatchUpRuns: 5, maxRunHistory: 100, failureCount: 0, lastError: null, syncStatus: "pending", createdAt: "2026-06-07T00:00:00.000Z", updatedAt: "2026-06-07T00:00:00.000Z", deletedAt: null, }]; vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, text: async () => JSON.stringify({ code: "0000", success: true, data: { request_id: "r1" } }), }))); }); it("computes the next 5-field cron run", async () => { const { nextCronRun } = await import("./schedulerService"); expect(nextCronRun("30 9 * * *", new Date("2026-06-07T01:29:30.000Z")).toISOString()) .toBe("2026-06-07T01:30:00.000Z"); }); it("activates due schedules through local ComputerServer", async () => { const { runDigitalEmployeeSchedulerCheck } = await import("./schedulerService"); const result = await runDigitalEmployeeSchedulerCheck(new Date("2026-06-07T01:00:10.000Z")); expect(result.activated).toBe(1); expect(mockState.startedRuns).toHaveLength(1); expect(fetch).toHaveBeenCalledWith( "http://127.0.0.1:60006/computer/chat", expect.objectContaining({ method: "POST" }), ); expect(mockState.finishedRuns[0]).toMatchObject({ status: "completed" }); }); it("records failed runs with retry next run", async () => { vi.stubGlobal("fetch", vi.fn(async () => ({ ok: false, status: 500, text: async () => "boom", }))); const { runDigitalEmployeeSchedulerCheck } = await import("./schedulerService"); const result = await runDigitalEmployeeSchedulerCheck(new Date("2026-06-07T01:00:10.000Z")); expect(result.errors).toHaveLength(1); expect(mockState.finishedRuns[0]).toMatchObject({ status: "failed", error: "ComputerServer 500: boom" }); expect(mockState.finishedRuns[0].nextRunAt).toBeTruthy(); }); });