mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 01:01:23 +00:00
refactor(test): centralize trigger and cron test helpers
This commit is contained in:
@@ -1,35 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createMockCronStateForJobs } from "./service.test-harness.js";
|
||||
import { recomputeNextRunsForMaintenance } from "./service/jobs.js";
|
||||
import type { CronServiceState } from "./service/state.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
describe("issue #13992 regression - cron jobs skip execution", () => {
|
||||
function createMockState(jobs: CronJob[]): CronServiceState {
|
||||
return {
|
||||
store: { version: 1, jobs },
|
||||
running: false,
|
||||
timer: null,
|
||||
storeLoadedAtMs: Date.now(),
|
||||
storeFileMtimeMs: null,
|
||||
op: Promise.resolve(),
|
||||
warnedDisabled: false,
|
||||
deps: {
|
||||
storePath: "/mock/path",
|
||||
cronEnabled: true,
|
||||
nowMs: () => Date.now(),
|
||||
enqueueSystemEvent: () => {},
|
||||
requestHeartbeatNow: () => {},
|
||||
runIsolatedAgentJob: async () => ({ status: "ok" }),
|
||||
log: {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as never,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("should NOT recompute nextRunAtMs for past-due jobs during maintenance", () => {
|
||||
const now = Date.now();
|
||||
const pastDue = now - 60_000; // 1 minute ago
|
||||
@@ -49,7 +23,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([job]);
|
||||
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Should not have changed the past-due nextRunAtMs
|
||||
@@ -74,7 +48,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([job]);
|
||||
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Should have computed a nextRunAtMs
|
||||
@@ -101,7 +75,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([job]);
|
||||
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Should have cleared nextRunAtMs for disabled job
|
||||
@@ -129,7 +103,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([job]);
|
||||
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Should have cleared stuck running marker
|
||||
@@ -172,7 +146,7 @@ describe("issue #13992 regression - cron jobs skip execution", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const state = createMockState([dueJob, malformedJob]);
|
||||
const state = createMockCronStateForJobs({ jobs: [dueJob, malformedJob], nowMs: now });
|
||||
|
||||
expect(() => recomputeNextRunsForMaintenance(state)).not.toThrow();
|
||||
expect(dueJob.state.nextRunAtMs).toBe(pastDue);
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CronService } from "./service.js";
|
||||
import { createStartedCronServiceWithFinishedBarrier } from "./service.test-harness.js";
|
||||
import {
|
||||
createStartedCronServiceWithFinishedBarrier,
|
||||
setupCronServiceSuite,
|
||||
} from "./service.test-harness.js";
|
||||
|
||||
const noopLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
let fixtureRoot = "";
|
||||
let caseId = 0;
|
||||
|
||||
async function makeStorePath() {
|
||||
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 };
|
||||
}
|
||||
const { logger: noopLogger, makeStorePath } = setupCronServiceSuite({
|
||||
prefix: "openclaw-cron-16156-",
|
||||
baseTimeIso: "2025-12-13T00:00:00.000Z",
|
||||
});
|
||||
|
||||
async function writeJobsStore(storePath: string, jobs: unknown[]) {
|
||||
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
||||
@@ -39,29 +29,6 @@ function createCronFromStorePath(storePath: string) {
|
||||
}
|
||||
|
||||
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"));
|
||||
noopLogger.debug.mockClear();
|
||||
noopLogger.info.mockClear();
|
||||
noopLogger.warn.mockClear();
|
||||
noopLogger.error.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("does not skip a cron job when list() is called while the job is past-due", async () => {
|
||||
const store = await makeStorePath();
|
||||
const { cron, enqueueSystemEvent, finished } = createStartedCronServiceWithFinishedBarrier({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createMockCronStateForJobs } from "./service.test-harness.js";
|
||||
import { recomputeNextRuns, recomputeNextRunsForMaintenance } from "./service/jobs.js";
|
||||
import type { CronServiceState } from "./service/state.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
/**
|
||||
@@ -19,32 +19,6 @@ describe("issue #17852 - daily cron jobs should not skip days", () => {
|
||||
const HOUR_MS = 3_600_000;
|
||||
const DAY_MS = 24 * HOUR_MS;
|
||||
|
||||
function createMockState(jobs: CronJob[], nowMs: number): CronServiceState {
|
||||
return {
|
||||
store: { version: 1, jobs },
|
||||
running: false,
|
||||
timer: null,
|
||||
storeLoadedAtMs: nowMs,
|
||||
storeFileMtimeMs: null,
|
||||
op: Promise.resolve(),
|
||||
warnedDisabled: false,
|
||||
deps: {
|
||||
storePath: "/mock/path",
|
||||
cronEnabled: true,
|
||||
nowMs: () => nowMs,
|
||||
enqueueSystemEvent: () => {},
|
||||
requestHeartbeatNow: () => {},
|
||||
runIsolatedAgentJob: async () => ({ status: "ok" }),
|
||||
log: {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as never,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createDailyThreeAmJob(threeAM: number): CronJob {
|
||||
return {
|
||||
id: "daily-job",
|
||||
@@ -71,7 +45,7 @@ describe("issue #17852 - daily cron jobs should not skip days", () => {
|
||||
|
||||
const job = createDailyThreeAmJob(threeAM);
|
||||
|
||||
const state = createMockState([job], now);
|
||||
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||
recomputeNextRunsForMaintenance(state);
|
||||
|
||||
// Maintenance should NOT touch existing past-due nextRunAtMs.
|
||||
@@ -88,7 +62,7 @@ describe("issue #17852 - daily cron jobs should not skip days", () => {
|
||||
|
||||
const job = createDailyThreeAmJob(threeAM);
|
||||
|
||||
const state = createMockState([job], now);
|
||||
const state = createMockCronStateForJobs({ jobs: [job], nowMs: now });
|
||||
recomputeNextRuns(state);
|
||||
|
||||
// The full recomputeNextRuns advances it to TOMORROW — skipping today's
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as schedule from "./schedule.js";
|
||||
import { CronService } from "./service.js";
|
||||
import { createRunningCronServiceState } from "./service.test-harness.js";
|
||||
import { createDeferred, createRunningCronServiceState } from "./service.test-harness.js";
|
||||
import { computeJobNextRunAtMs } from "./service/jobs.js";
|
||||
import { createCronServiceState, type CronEvent } from "./service/state.js";
|
||||
import { onTimer } from "./service/timer.js";
|
||||
@@ -38,16 +38,6 @@ async function makeStorePath() {
|
||||
};
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function createDueIsolatedJob(params: {
|
||||
id: string;
|
||||
nowMs: number;
|
||||
@@ -563,7 +553,6 @@ describe("Cron issue regressions", () => {
|
||||
|
||||
let now = scheduledAt;
|
||||
let fireCount = 0;
|
||||
const events: CronEvent[] = [];
|
||||
const state = createCronServiceState({
|
||||
cronEnabled: true,
|
||||
storePath: store.storePath,
|
||||
@@ -571,9 +560,6 @@ describe("Cron issue regressions", () => {
|
||||
nowMs: () => now,
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
requestHeartbeatNow: vi.fn(),
|
||||
onEvent: (evt) => {
|
||||
events.push(evt);
|
||||
},
|
||||
runIsolatedAgentJob: vi.fn(async () => {
|
||||
// Job completes very quickly (7ms) — still within the same second
|
||||
now += 7;
|
||||
|
||||
@@ -2,16 +2,10 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CronService } from "./service.js";
|
||||
import {
|
||||
createCronStoreHarness,
|
||||
createNoopLogger,
|
||||
installCronTestHooks,
|
||||
} from "./service.test-harness.js";
|
||||
import { setupCronServiceSuite } from "./service.test-harness.js";
|
||||
|
||||
const noopLogger = createNoopLogger();
|
||||
const { makeStorePath } = createCronStoreHarness({ prefix: "openclaw-cron-" });
|
||||
installCronTestHooks({
|
||||
logger: noopLogger,
|
||||
const { logger: noopLogger, makeStorePath } = setupCronServiceSuite({
|
||||
prefix: "openclaw-cron-",
|
||||
baseTimeIso: "2025-12-13T17:00:00.000Z",
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HeartbeatRunResult } from "../infra/heartbeat-wake.js";
|
||||
import type { CronEvent, CronServiceDeps } from "./service.js";
|
||||
import { CronService } from "./service.js";
|
||||
import { createNoopLogger, installCronTestHooks } from "./service.test-harness.js";
|
||||
import { createDeferred, createNoopLogger, installCronTestHooks } from "./service.test-harness.js";
|
||||
|
||||
const noopLogger = createNoopLogger();
|
||||
installCronTestHooks({ logger: noopLogger });
|
||||
@@ -196,16 +196,6 @@ beforeEach(() => {
|
||||
ensureDir(fixturesRoot);
|
||||
});
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function createCronEventHarness() {
|
||||
const events: CronEvent[] = [];
|
||||
const waiters: Array<{
|
||||
|
||||
@@ -2,16 +2,10 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CronService } from "./service.js";
|
||||
import {
|
||||
createCronStoreHarness,
|
||||
createNoopLogger,
|
||||
installCronTestHooks,
|
||||
} from "./service.test-harness.js";
|
||||
import { setupCronServiceSuite } from "./service.test-harness.js";
|
||||
|
||||
const noopLogger = createNoopLogger();
|
||||
const { makeStorePath } = createCronStoreHarness({ prefix: "openclaw-cron-" });
|
||||
installCronTestHooks({
|
||||
logger: noopLogger,
|
||||
const { logger: noopLogger, makeStorePath } = setupCronServiceSuite({
|
||||
prefix: "openclaw-cron-",
|
||||
baseTimeIso: "2026-02-06T17:00:00.000Z",
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest";
|
||||
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
||||
import type { CronEvent } from "./service.js";
|
||||
import { CronService } from "./service.js";
|
||||
import { createCronServiceState } from "./service/state.js";
|
||||
import { createCronServiceState, type CronServiceState } from "./service/state.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
export type NoopLogger = {
|
||||
@@ -85,6 +85,16 @@ export function installCronTestHooks(options: {
|
||||
});
|
||||
}
|
||||
|
||||
export function setupCronServiceSuite(options?: { prefix?: string; baseTimeIso?: string }) {
|
||||
const logger = createNoopLogger();
|
||||
const { makeStorePath } = createCronStoreHarness({ prefix: options?.prefix });
|
||||
installCronTestHooks({
|
||||
logger,
|
||||
baseTimeIso: options?.baseTimeIso,
|
||||
});
|
||||
return { logger, makeStorePath };
|
||||
}
|
||||
|
||||
export function createFinishedBarrier() {
|
||||
const resolvers = new Map<string, (evt: CronEvent) => void>();
|
||||
return {
|
||||
@@ -152,3 +162,43 @@ export function createRunningCronServiceState(params: {
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
export function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
export function createMockCronStateForJobs(params: {
|
||||
jobs: CronJob[];
|
||||
nowMs?: number;
|
||||
}): CronServiceState {
|
||||
const nowMs = params.nowMs ?? Date.now();
|
||||
return {
|
||||
store: { version: 1, jobs: params.jobs },
|
||||
running: false,
|
||||
timer: null,
|
||||
storeLoadedAtMs: nowMs,
|
||||
storeFileMtimeMs: null,
|
||||
op: Promise.resolve(),
|
||||
warnedDisabled: false,
|
||||
deps: {
|
||||
storePath: "/mock/path",
|
||||
cronEnabled: true,
|
||||
nowMs: () => nowMs,
|
||||
enqueueSystemEvent: () => {},
|
||||
requestHeartbeatNow: () => {},
|
||||
runIsolatedAgentJob: async () => ({ status: "ok" }),
|
||||
log: {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as never,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user