
























@@ -1,190 +1,22 @@
1-import path from "node:path";
2-import { beforeEach, describe, expect, it, vi } from "vitest";
1+import { describe, expect, it, vi } from "vitest";
32import {
43HEARTBEAT_SKIP_CRON_IN_PROGRESS,
54HEARTBEAT_SKIP_REQUESTS_IN_FLIGHT,
65type HeartbeatRunResult,
76} from "../infra/heartbeat-wake.js";
87import type { CronEvent, CronServiceDeps } from "./service.js";
98import { CronService } from "./service.js";
10-import { createDeferred, createNoopLogger, installCronTestHooks } from "./service.test-harness.js";
9+import {
10+createCronStoreHarness,
11+createDeferred,
12+createNoopLogger,
13+installCronTestHooks,
14+} from "./service.test-harness.js";
11151216const noopLogger = createNoopLogger();
1317installCronTestHooks({ logger: noopLogger });
14-15-type FakeFsEntry =
16-| { kind: "file"; content: string; mtimeMs: number }
17-| { kind: "dir"; mtimeMs: number };
18-19-const fsState = vi.hoisted(() => ({
20-entries: new Map<string, FakeFsEntry>(),
21-nowMs: 0,
22-fixtureCount: 0,
23-}));
24-25-const abs = (p: string) => path.resolve(p);
26-const fixturesRoot = abs(path.join("__openclaw_vitest__", "cron", "runs-one-shot"));
27-const isFixturePath = (p: string) => {
28-const resolved = abs(p);
29-const rootPrefix = `${fixturesRoot}${path.sep}`;
30-return resolved === fixturesRoot || resolved.startsWith(rootPrefix);
31-};
32-33-function bumpMtimeMs() {
34-fsState.nowMs += 1;
35-return fsState.nowMs;
36-}
37-38-function ensureDir(dirPath: string) {
39-let current = abs(dirPath);
40-while (true) {
41-if (!fsState.entries.has(current)) {
42-fsState.entries.set(current, { kind: "dir", mtimeMs: bumpMtimeMs() });
43-}
44-const parent = path.dirname(current);
45-if (parent === current) {
46-break;
47-}
48-current = parent;
49-}
50-}
51-52-function setFile(filePath: string, content: string) {
53-const resolved = abs(filePath);
54-ensureDir(path.dirname(resolved));
55-fsState.entries.set(resolved, { kind: "file", content, mtimeMs: bumpMtimeMs() });
56-}
57-58-async function makeStorePath() {
59-const dir = path.join(fixturesRoot, `case-${fsState.fixtureCount++}`);
60-ensureDir(dir);
61-const storePath = path.join(dir, "cron", "jobs.json");
62-ensureDir(path.dirname(storePath));
63-return { storePath, cleanup: async () => {} };
64-}
65-66-vi.mock("node:fs", async () => {
67-const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
68-const pathMod = await import("node:path");
69-const absInMock = (p: string) => pathMod.resolve(p);
70-const isFixtureInMock = (p: string) => {
71-const resolved = absInMock(p);
72-const rootPrefix = `${absInMock(fixturesRoot)}${pathMod.sep}`;
73-return resolved === absInMock(fixturesRoot) || resolved.startsWith(rootPrefix);
74-};
75-76-const mkErr = (code: string, message: string) => Object.assign(new Error(message), { code });
77-78-const promises = {
79- ...actual.promises,
80-mkdir: async (p: string) => {
81-if (!isFixtureInMock(p)) {
82-return await actual.promises.mkdir(p, { recursive: true });
83-}
84-ensureDir(p);
85-return undefined;
86-},
87-readFile: async (p: string) => {
88-if (!isFixtureInMock(p)) {
89-return await actual.promises.readFile(p, "utf-8");
90-}
91-const entry = fsState.entries.get(absInMock(p));
92-if (!entry || entry.kind !== "file") {
93-throw mkErr("ENOENT", `ENOENT: no such file or directory, open '${p}'`);
94-}
95-return entry.content;
96-},
97-writeFile: async (p: string, data: string | Uint8Array) => {
98-if (!isFixtureInMock(p)) {
99-return await actual.promises.writeFile(p, data, "utf-8");
100-}
101-const content = typeof data === "string" ? data : Buffer.from(data).toString("utf-8");
102-setFile(p, content);
103-},
104-rename: async (from: string, to: string) => {
105-if (!isFixtureInMock(from) || !isFixtureInMock(to)) {
106-return await actual.promises.rename(from, to);
107-}
108-const fromAbs = absInMock(from);
109-const toAbs = absInMock(to);
110-const entry = fsState.entries.get(fromAbs);
111-if (!entry || entry.kind !== "file") {
112-throw mkErr("ENOENT", `ENOENT: no such file or directory, rename '${from}' -> '${to}'`);
113-}
114-ensureDir(pathMod.dirname(toAbs));
115-fsState.entries.delete(fromAbs);
116-fsState.entries.set(toAbs, { ...entry, mtimeMs: bumpMtimeMs() });
117-},
118-copyFile: async (from: string, to: string) => {
119-if (!isFixtureInMock(from) || !isFixtureInMock(to)) {
120-return await actual.promises.copyFile(from, to);
121-}
122-const entry = fsState.entries.get(absInMock(from));
123-if (!entry || entry.kind !== "file") {
124-throw mkErr("ENOENT", `ENOENT: no such file or directory, copyfile '${from}' -> '${to}'`);
125-}
126-setFile(to, entry.content);
127-},
128-stat: async (p: string) => {
129-if (!isFixtureInMock(p)) {
130-return await actual.promises.stat(p);
131-}
132-const entry = fsState.entries.get(absInMock(p));
133-if (!entry) {
134-throw mkErr("ENOENT", `ENOENT: no such file or directory, stat '${p}'`);
135-}
136-return {
137-mtimeMs: entry.mtimeMs,
138-isDirectory: () => entry.kind === "dir",
139-isFile: () => entry.kind === "file",
140-};
141-},
142-access: async (p: string) => {
143-if (!isFixtureInMock(p)) {
144-return await actual.promises.access(p);
145-}
146-const entry = fsState.entries.get(absInMock(p));
147-if (!entry) {
148-throw mkErr("ENOENT", `ENOENT: no such file or directory, access '${p}'`);
149-}
150-},
151-unlink: async (p: string) => {
152-if (!isFixtureInMock(p)) {
153-return await actual.promises.unlink(p);
154-}
155-fsState.entries.delete(absInMock(p));
156-},
157-} as unknown as typeof actual.promises;
158-159-const wrapped = { ...actual, promises };
160-return { ...wrapped, default: wrapped };
161-});
162-163-vi.mock("node:fs/promises", async () => {
164-const actual = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
165-const wrapped = {
166- ...actual,
167-mkdir: async (p: string, _opts?: unknown) => {
168-if (!isFixturePath(p)) {
169-return await actual.mkdir(p, { recursive: true });
170-}
171-ensureDir(p);
172-return undefined;
173-},
174-writeFile: async (p: string, data: string, _enc?: unknown) => {
175-if (!isFixturePath(p)) {
176-return await actual.writeFile(p, data, "utf-8");
177-}
178-setFile(p, data);
179-},
180-};
181-return { ...wrapped, default: wrapped };
182-});
183-184-beforeEach(() => {
185-fsState.entries.clear();
186-fsState.nowMs = 0;
187-ensureDir(fixturesRoot);
18+const { makeStorePath } = createCronStoreHarness({
19+prefix: "openclaw-cron-runs-one-shot-",
18820});
1892119022function createCronEventHarness() {
@@ -229,7 +61,6 @@ type CronHarnessOptions = {
22961};
2306223163async function createCronHarness(options: CronHarnessOptions = {}) {
232-ensureDir(fixturesRoot);
23364const store = await makeStorePath();
23465const enqueueSystemEvent = vi.fn();
23566const requestHeartbeat = vi.fn();
@@ -377,6 +208,7 @@ function expectMainSystemEventPosted(enqueueSystemEvent: unknown, text: string)
377208}
378209379210async function stopCronAndCleanup(cron: CronService, store: { cleanup: () => Promise<void> }) {
211+await cron.status();
380212cron.stop();
381213await store.cleanup();
382214}
@@ -678,7 +510,6 @@ describe("CronService", () => {
678510});
679511680512it("rejects unsupported session/payload combinations", async () => {
681-ensureDir(fixturesRoot);
682513const store = await makeStorePath();
683514684515const cron = createStartedCronService(
@@ -712,7 +543,6 @@ describe("CronService", () => {
712543}),
713544).rejects.toThrow(/isolated.*cron jobs require/);
714545715-cron.stop();
716-await store.cleanup();
546+await stopCronAndCleanup(cron, store);
717547});
718548});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。