test(perf): trim repeated setup in cron memory and config suites

This commit is contained in:
Peter Steinberger
2026-03-02 19:16:28 +00:00
parent 6add2bcc15
commit 83ec545bed
4 changed files with 56 additions and 59 deletions

View File

@@ -41,10 +41,7 @@ describe("config plugin validation", () => {
OPENCLAW_PLUGIN_MANIFEST_CACHE_MS: process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS, OPENCLAW_PLUGIN_MANIFEST_CACHE_MS: process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS,
}; };
const validateInSuite = (raw: unknown) => { const validateInSuite = (raw: unknown) => validateConfigObjectWithPlugins(raw);
process.env.OPENCLAW_STATE_DIR = path.join(suiteHome, ".openclaw");
return validateConfigObjectWithPlugins(raw);
};
beforeAll(async () => { beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-plugin-validation-")); fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-plugin-validation-"));
@@ -70,6 +67,7 @@ describe("config plugin validation", () => {
channels: ["bluebubbles"], channels: ["bluebubbles"],
schema: { type: "object" }, schema: { type: "object" },
}); });
process.env.OPENCLAW_STATE_DIR = path.join(suiteHome, ".openclaw");
process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS = "10000"; process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS = "10000";
clearPluginManifestRegistryCache(); clearPluginManifestRegistryCache();
}); });

View File

@@ -38,7 +38,7 @@ function topOfHourOffsetMs(jobId: string) {
let fixtureRoot = ""; let fixtureRoot = "";
let fixtureCount = 0; let fixtureCount = 0;
async function makeStorePath() { function makeStorePath() {
const storePath = path.join(fixtureRoot, `case-${fixtureCount++}.jobs.json`); const storePath = path.join(fixtureRoot, `case-${fixtureCount++}.jobs.json`);
return { return {
storePath, storePath,
@@ -157,7 +157,6 @@ describe("Cron issue regressions", () => {
}); });
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers(); vi.useFakeTimers();
vi.setSystemTime(new Date("2026-02-06T10:05:00.000Z")); vi.setSystemTime(new Date("2026-02-06T10:05:00.000Z"));
}); });
@@ -168,7 +167,7 @@ describe("Cron issue regressions", () => {
}); });
it("covers schedule updates and payload patching", async () => { it("covers schedule updates and payload patching", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const cron = await startCronForStore({ const cron = await startCronForStore({
storePath: store.storePath, storePath: store.storePath,
cronEnabled: false, cronEnabled: false,
@@ -214,7 +213,7 @@ describe("Cron issue regressions", () => {
}); });
it("repairs isolated every jobs missing createdAtMs and sets nextWakeAtMs", async () => { it("repairs isolated every jobs missing createdAtMs and sets nextWakeAtMs", async () => {
const store = await makeStorePath(); const store = makeStorePath();
await fs.writeFile( await fs.writeFile(
store.storePath, store.storePath,
JSON.stringify({ JSON.stringify({
@@ -263,7 +262,7 @@ describe("Cron issue regressions", () => {
}); });
it("repairs missing nextRunAtMs on non-schedule updates without touching other jobs", async () => { it("repairs missing nextRunAtMs on non-schedule updates without touching other jobs", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const cron = await startCronForStore({ storePath: store.storePath, cronEnabled: false }); const cron = await startCronForStore({ storePath: store.storePath, cronEnabled: false });
const created = await cron.add({ const created = await cron.add({
@@ -287,7 +286,7 @@ describe("Cron issue regressions", () => {
}); });
it("does not advance unrelated due jobs when updating another job", async () => { it("does not advance unrelated due jobs when updating another job", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const now = Date.parse("2026-02-06T10:05:00.000Z"); const now = Date.parse("2026-02-06T10:05:00.000Z");
vi.setSystemTime(now); vi.setSystemTime(now);
const cron = await startCronForStore({ storePath: store.storePath, cronEnabled: false }); const cron = await startCronForStore({ storePath: store.storePath, cronEnabled: false });
@@ -329,7 +328,7 @@ describe("Cron issue regressions", () => {
}); });
it("treats persisted jobs with missing enabled as enabled during update()", async () => { it("treats persisted jobs with missing enabled as enabled during update()", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const now = Date.parse("2026-02-06T10:05:00.000Z"); const now = Date.parse("2026-02-06T10:05:00.000Z");
await fs.writeFile( await fs.writeFile(
store.storePath, store.storePath,
@@ -372,7 +371,7 @@ describe("Cron issue regressions", () => {
}); });
it("treats persisted due jobs with missing enabled as runnable", async () => { it("treats persisted due jobs with missing enabled as runnable", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const now = Date.parse("2026-02-06T10:05:00.000Z"); const now = Date.parse("2026-02-06T10:05:00.000Z");
const dueAt = now - 30_000; const dueAt = now - 30_000;
await fs.writeFile( await fs.writeFile(
@@ -419,7 +418,7 @@ describe("Cron issue regressions", () => {
it("caps timer delay to 60s for far-future schedules", async () => { it("caps timer delay to 60s for far-future schedules", async () => {
const timeoutSpy = vi.spyOn(globalThis, "setTimeout"); const timeoutSpy = vi.spyOn(globalThis, "setTimeout");
const store = await makeStorePath(); const store = makeStorePath();
const cron = await startCronForStore({ storePath: store.storePath }); const cron = await startCronForStore({ storePath: store.storePath });
const callsBeforeAdd = timeoutSpy.mock.calls.length; const callsBeforeAdd = timeoutSpy.mock.calls.length;
@@ -444,7 +443,7 @@ describe("Cron issue regressions", () => {
it("re-arms timer without hot-looping when a run is already in progress", async () => { it("re-arms timer without hot-looping when a run is already in progress", async () => {
const timeoutSpy = vi.spyOn(globalThis, "setTimeout"); const timeoutSpy = vi.spyOn(globalThis, "setTimeout");
const store = await makeStorePath(); const store = makeStorePath();
const now = Date.parse("2026-02-06T10:05:00.000Z"); const now = Date.parse("2026-02-06T10:05:00.000Z");
const state = createRunningCronServiceState({ const state = createRunningCronServiceState({
storePath: store.storePath, storePath: store.storePath,
@@ -468,7 +467,7 @@ describe("Cron issue regressions", () => {
}); });
it("skips forced manual runs while a timer-triggered run is in progress", async () => { it("skips forced manual runs while a timer-triggered run is in progress", async () => {
const store = await makeStorePath(); const store = makeStorePath();
let resolveRun: let resolveRun:
| ((value: { status: "ok" | "error" | "skipped"; summary?: string; error?: string }) => void) | ((value: { status: "ok" | "error" | "skipped"; summary?: string; error?: string }) => void)
| undefined; | undefined;
@@ -529,7 +528,7 @@ describe("Cron issue regressions", () => {
}); });
it("does not double-run a job when cron.run overlaps a due timer tick", async () => { it("does not double-run a job when cron.run overlaps a due timer tick", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const runStarted = createDeferred<void>(); const runStarted = createDeferred<void>();
const runFinished = createDeferred<void>(); const runFinished = createDeferred<void>();
const runResolvers: Array< const runResolvers: Array<
@@ -586,7 +585,7 @@ describe("Cron issue regressions", () => {
}); });
it("does not advance unrelated due jobs after manual cron.run", async () => { it("does not advance unrelated due jobs after manual cron.run", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const nowMs = Date.now(); const nowMs = Date.now();
const dueNextRunAtMs = nowMs - 1_000; const dueNextRunAtMs = nowMs - 1_000;
@@ -627,7 +626,7 @@ describe("Cron issue regressions", () => {
}); });
it("keeps telegram delivery target writeback after manual cron.run", async () => { it("keeps telegram delivery target writeback after manual cron.run", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const originalTarget = "https://t.me/obviyus"; const originalTarget = "https://t.me/obviyus";
const rewrittenTarget = "-10012345/6789"; const rewrittenTarget = "-10012345/6789";
const runIsolatedAgentJob = vi.fn(async (params: { job: { id: string } }) => { const runIsolatedAgentJob = vi.fn(async (params: { job: { id: string } }) => {
@@ -675,7 +674,7 @@ describe("Cron issue regressions", () => {
}); });
it("#13845: one-shot jobs with terminal statuses do not re-fire on restart", async () => { it("#13845: one-shot jobs with terminal statuses do not re-fire on restart", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const pastAt = Date.parse("2026-02-06T09:00:00.000Z"); const pastAt = Date.parse("2026-02-06T09:00:00.000Z");
const baseJob = { const baseJob = {
name: "reminder", name: "reminder",
@@ -732,7 +731,7 @@ describe("Cron issue regressions", () => {
runIsolatedAgentJob: ReturnType<typeof vi.fn>; runIsolatedAgentJob: ReturnType<typeof vi.fn>;
firstRetryAtMs: number; firstRetryAtMs: number;
}> => { }> => {
const store = await makeStorePath(); const store = makeStorePath();
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
id: params.id, id: params.id,
name: "reminder", name: "reminder",
@@ -794,7 +793,7 @@ describe("Cron issue regressions", () => {
}); });
it("#24355: one-shot job disabled after max transient retries", async () => { it("#24355: one-shot job disabled after max transient retries", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z"); const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z");
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
@@ -837,7 +836,7 @@ describe("Cron issue regressions", () => {
}); });
it("#24355: one-shot job respects cron.retry config", async () => { it("#24355: one-shot job respects cron.retry config", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z"); const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z");
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
@@ -883,7 +882,7 @@ describe("Cron issue regressions", () => {
}); });
it("#24355: one-shot job disabled immediately on permanent error", async () => { it("#24355: one-shot job disabled immediately on permanent error", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z"); const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z");
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
@@ -920,7 +919,7 @@ describe("Cron issue regressions", () => {
}); });
it("prevents spin loop when cron job completes within the scheduled second (#17821)", async () => { it("prevents spin loop when cron job completes within the scheduled second (#17821)", async () => {
const store = await makeStorePath(); const store = makeStorePath();
// Simulate a cron job "0 13 * * *" (daily 13:00 UTC) that fires exactly // Simulate a cron job "0 13 * * *" (daily 13:00 UTC) that fires exactly
// at 13:00:00.000 and completes 7ms later (still in the same second). // at 13:00:00.000 and completes 7ms later (still in the same second).
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
@@ -970,7 +969,7 @@ describe("Cron issue regressions", () => {
}); });
it("enforces a minimum refire gap for second-granularity cron schedules (#17821)", async () => { it("enforces a minimum refire gap for second-granularity cron schedules (#17821)", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
@@ -1008,7 +1007,7 @@ describe("Cron issue regressions", () => {
}); });
it("treats timeoutSeconds=0 as no timeout for isolated agentTurn jobs", async () => { it("treats timeoutSeconds=0 as no timeout for isolated agentTurn jobs", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
@@ -1055,7 +1054,7 @@ describe("Cron issue regressions", () => {
}); });
it("does not time out agentTurn jobs at the default 10-minute safety window", async () => { it("does not time out agentTurn jobs at the default 10-minute safety window", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
@@ -1108,7 +1107,7 @@ describe("Cron issue regressions", () => {
it("aborts isolated runs when cron timeout fires", async () => { it("aborts isolated runs when cron timeout fires", async () => {
vi.useRealTimers(); vi.useRealTimers();
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
id: "abort-on-timeout", id: "abort-on-timeout",
@@ -1147,7 +1146,7 @@ describe("Cron issue regressions", () => {
it("suppresses isolated follow-up side effects after timeout", async () => { it("suppresses isolated follow-up side effects after timeout", async () => {
vi.useRealTimers(); vi.useRealTimers();
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
const enqueueSystemEvent = vi.fn(); const enqueueSystemEvent = vi.fn();
@@ -1201,7 +1200,7 @@ describe("Cron issue regressions", () => {
it("applies timeoutSeconds to manual cron.run isolated executions", async () => { it("applies timeoutSeconds to manual cron.run isolated executions", async () => {
vi.useRealTimers(); vi.useRealTimers();
const store = await makeStorePath(); const store = makeStorePath();
const abortAwareRunner = createAbortAwareIsolatedRunner(); const abortAwareRunner = createAbortAwareIsolatedRunner();
const cron = await startCronForStore({ const cron = await startCronForStore({
@@ -1237,7 +1236,7 @@ describe("Cron issue regressions", () => {
it("applies timeoutSeconds to startup catch-up isolated executions", async () => { it("applies timeoutSeconds to startup catch-up isolated executions", async () => {
vi.useRealTimers(); vi.useRealTimers();
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
const cronJob = createIsolatedRegressionJob({ const cronJob = createIsolatedRegressionJob({
id: "startup-timeout", id: "startup-timeout",
@@ -1354,7 +1353,7 @@ describe("Cron issue regressions", () => {
}); });
it("records per-job start time and duration for batched due jobs", async () => { it("records per-job start time and duration for batched due jobs", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const dueAt = Date.parse("2026-02-06T10:05:01.000Z"); const dueAt = Date.parse("2026-02-06T10:05:01.000Z");
const first = createDueIsolatedJob({ id: "batch-first", nowMs: dueAt, nextRunAtMs: dueAt }); const first = createDueIsolatedJob({ id: "batch-first", nowMs: dueAt, nextRunAtMs: dueAt });
const second = createDueIsolatedJob({ id: "batch-second", nowMs: dueAt, nextRunAtMs: dueAt }); const second = createDueIsolatedJob({ id: "batch-second", nowMs: dueAt, nextRunAtMs: dueAt });
@@ -1399,7 +1398,7 @@ describe("Cron issue regressions", () => {
}); });
it("#17554: run() clears stale runningAtMs and executes the job", async () => { it("#17554: run() clears stale runningAtMs and executes the job", async () => {
const store = await makeStorePath(); const store = makeStorePath();
const now = Date.parse("2026-02-06T10:05:00.000Z"); const now = Date.parse("2026-02-06T10:05:00.000Z");
const staleRunningAtMs = now - 2 * 60 * 60 * 1000 - 1; const staleRunningAtMs = now - 2 * 60 * 60 * 1000 - 1;
@@ -1455,7 +1454,7 @@ describe("Cron issue regressions", () => {
it("honors cron maxConcurrentRuns for due jobs", async () => { it("honors cron maxConcurrentRuns for due jobs", async () => {
vi.useRealTimers(); vi.useRealTimers();
const store = await makeStorePath(); const store = makeStorePath();
const dueAt = Date.parse("2026-02-06T10:05:01.000Z"); const dueAt = Date.parse("2026-02-06T10:05:01.000Z");
const first = createDueIsolatedJob({ id: "parallel-first", nowMs: dueAt, nextRunAtMs: dueAt }); const first = createDueIsolatedJob({ id: "parallel-first", nowMs: dueAt, nextRunAtMs: dueAt });
const second = createDueIsolatedJob({ const second = createDueIsolatedJob({
@@ -1528,7 +1527,7 @@ describe("Cron issue regressions", () => {
// job abort that fires much sooner than the configured outer timeout. // job abort that fires much sooner than the configured outer timeout.
it("outer cron timeout fires at configured timeoutSeconds, not at 1/3 (#29774)", async () => { it("outer cron timeout fires at configured timeoutSeconds, not at 1/3 (#29774)", async () => {
vi.useRealTimers(); vi.useRealTimers();
const store = await makeStorePath(); const store = makeStorePath();
const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z"); const scheduledAt = Date.parse("2026-02-15T13:00:00.000Z");
// Keep this short for suite speed while still separating expected timeout // Keep this short for suite speed while still separating expected timeout

View File

@@ -133,10 +133,9 @@ describe("QmdMemoryManager", () => {
tmpRoot = path.join(fixtureRoot, `case-${fixtureCount++}`); tmpRoot = path.join(fixtureRoot, `case-${fixtureCount++}`);
workspaceDir = path.join(tmpRoot, "workspace"); workspaceDir = path.join(tmpRoot, "workspace");
stateDir = path.join(tmpRoot, "state"); stateDir = path.join(tmpRoot, "state");
await Promise.all([ // Only workspace must exist for configured collection paths; state paths are
fs.mkdir(workspaceDir, { recursive: true }), // created lazily by manager code when needed.
fs.mkdir(stateDir, { recursive: true }), await fs.mkdir(workspaceDir, { recursive: true });
]);
process.env.OPENCLAW_STATE_DIR = stateDir; process.env.OPENCLAW_STATE_DIR = stateDir;
cfg = { cfg = {
agents: { agents: {

View File

@@ -104,6 +104,25 @@ describe("slack prepareSlackMessage inbound contract", () => {
userTokenSource: "none", userTokenSource: "none",
config: {}, config: {},
}; };
const defaultMessageTemplate: SlackMessageEvent = {
channel: "D123",
channel_type: "im",
user: "U1",
text: "hi",
ts: "1.000",
} as SlackMessageEvent;
const threadAccount: ResolvedSlackAccount = {
accountId: "default",
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config: {
replyToMode: "all",
thread: { initialHistoryLimit: 20 },
},
replyToMode: "all",
};
async function prepareWithDefaultCtx(message: SlackMessageEvent) { async function prepareWithDefaultCtx(message: SlackMessageEvent) {
return prepareSlackMessage({ return prepareSlackMessage({
@@ -129,14 +148,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
} }
function createSlackMessage(overrides: Partial<SlackMessageEvent>): SlackMessageEvent { function createSlackMessage(overrides: Partial<SlackMessageEvent>): SlackMessageEvent {
return { return { ...defaultMessageTemplate, ...overrides } as SlackMessageEvent;
channel: "D123",
channel_type: "im",
user: "U1",
text: "hi",
ts: "1.000",
...overrides,
} as SlackMessageEvent;
} }
async function prepareMessageWith( async function prepareMessageWith(
@@ -162,18 +174,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
} }
function createThreadAccount(): ResolvedSlackAccount { function createThreadAccount(): ResolvedSlackAccount {
return { return threadAccount;
accountId: "default",
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config: {
replyToMode: "all",
thread: { initialHistoryLimit: 20 },
},
replyToMode: "all",
};
} }
function createThreadReplyMessage(overrides: Partial<SlackMessageEvent>): SlackMessageEvent { function createThreadReplyMessage(overrides: Partial<SlackMessageEvent>): SlackMessageEvent {