From 7649f9cba4d5f952a7a7a9c156b0b1872e051ab5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Feb 2026 00:49:42 +0000 Subject: [PATCH] refactor(test): share heartbeat sandbox fixtures --- ...espects-ackmaxchars-heartbeat-acks.test.ts | 61 +---------------- src/infra/heartbeat-runner.test-utils.ts | 68 +++++++++++++++++++ .../heartbeat-runner.transcript-prune.test.ts | 58 +++------------- 3 files changed, 79 insertions(+), 108 deletions(-) create mode 100644 src/infra/heartbeat-runner.test-utils.ts diff --git a/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts b/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts index 8257ef6d99f..6e397011afe 100644 --- a/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts +++ b/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts @@ -1,12 +1,10 @@ import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; import { describe, expect, it, vi } from "vitest"; -import * as replyModule from "../auto-reply/reply.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveMainSessionKey } from "../config/sessions.js"; import { runHeartbeatOnce, type HeartbeatDeps } from "./heartbeat-runner.js"; import { installHeartbeatRunnerTestRuntime } from "./heartbeat-runner.test-harness.js"; +import { seedSessionStore, withTempHeartbeatSandbox } from "./heartbeat-runner.test-utils.js"; // Avoid pulling optional runtime deps during isolated runs. vi.mock("jiti", () => ({ createJiti: () => () => ({}) })); @@ -86,51 +84,6 @@ describe("resolveHeartbeatIntervalMs", () => { } satisfies HeartbeatDeps; } - async function seedSessionStore( - storePath: string, - sessionKey: string, - session: { - sessionId?: string; - updatedAt?: number; - lastChannel: string; - lastProvider: string; - lastTo: string; - }, - ) { - await fs.writeFile( - storePath, - JSON.stringify( - { - [sessionKey]: { - sessionId: session.sessionId ?? "sid", - updatedAt: session.updatedAt ?? Date.now(), - ...session, - }, - }, - null, - 2, - ), - ); - } - - async function withTempHeartbeatSandbox( - fn: (ctx: { - tmpDir: string; - storePath: string; - replySpy: ReturnType; - }) => Promise, - ) { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-")); - const storePath = path.join(tmpDir, "sessions.json"); - const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); - try { - return await fn({ tmpDir, storePath, replySpy }); - } finally { - replySpy.mockRestore(); - await fs.rm(tmpDir, { recursive: true, force: true }); - } - } - async function withTempTelegramHeartbeatSandbox( fn: (ctx: { tmpDir: string; @@ -138,17 +91,7 @@ describe("resolveHeartbeatIntervalMs", () => { replySpy: ReturnType; }) => Promise, ) { - const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; - process.env.TELEGRAM_BOT_TOKEN = ""; - try { - return await withTempHeartbeatSandbox(fn); - } finally { - if (prevTelegramToken === undefined) { - delete process.env.TELEGRAM_BOT_TOKEN; - } else { - process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken; - } - } + return withTempHeartbeatSandbox(fn, { unsetEnvVars: ["TELEGRAM_BOT_TOKEN"] }); } it("respects ackMaxChars for heartbeat acks", async () => { diff --git a/src/infra/heartbeat-runner.test-utils.ts b/src/infra/heartbeat-runner.test-utils.ts new file mode 100644 index 00000000000..8a187423e58 --- /dev/null +++ b/src/infra/heartbeat-runner.test-utils.ts @@ -0,0 +1,68 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { vi } from "vitest"; +import * as replyModule from "../auto-reply/reply.js"; + +export type HeartbeatSessionSeed = { + sessionId?: string; + updatedAt?: number; + lastChannel: string; + lastProvider: string; + lastTo: string; +}; + +export async function seedSessionStore( + storePath: string, + sessionKey: string, + session: HeartbeatSessionSeed, +): Promise { + await fs.writeFile( + storePath, + JSON.stringify( + { + [sessionKey]: { + sessionId: session.sessionId ?? "sid", + updatedAt: session.updatedAt ?? Date.now(), + ...session, + }, + }, + null, + 2, + ), + ); +} + +export async function withTempHeartbeatSandbox( + fn: (ctx: { + tmpDir: string; + storePath: string; + replySpy: ReturnType; + }) => Promise, + options?: { + prefix?: string; + unsetEnvVars?: string[]; + }, +): Promise { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), options?.prefix ?? "openclaw-hb-")); + const storePath = path.join(tmpDir, "sessions.json"); + const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); + const previousEnv = new Map(); + for (const envName of options?.unsetEnvVars ?? []) { + previousEnv.set(envName, process.env[envName]); + process.env[envName] = ""; + } + try { + return await fn({ tmpDir, storePath, replySpy }); + } finally { + replySpy.mockRestore(); + for (const [envName, previousValue] of previousEnv.entries()) { + if (previousValue === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = previousValue; + } + } + await fs.rm(tmpDir, { recursive: true, force: true }); + } +} diff --git a/src/infra/heartbeat-runner.transcript-prune.test.ts b/src/infra/heartbeat-runner.transcript-prune.test.ts index b8bc14fb7b7..24785d908c5 100644 --- a/src/infra/heartbeat-runner.transcript-prune.test.ts +++ b/src/infra/heartbeat-runner.transcript-prune.test.ts @@ -1,16 +1,15 @@ import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js"; -import * as replyModule from "../auto-reply/reply.js"; -import type { OpenClawConfig } from "../config/config.js"; import { resolveMainSessionKey } from "../config/sessions.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createPluginRuntime } from "../plugins/runtime/index.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { runHeartbeatOnce } from "./heartbeat-runner.js"; +import { seedSessionStore, withTempHeartbeatSandbox } from "./heartbeat-runner.test-utils.js"; // Avoid pulling optional runtime deps during isolated runs. vi.mock("jiti", () => ({ createJiti: () => () => ({}) })); @@ -24,33 +23,6 @@ beforeEach(() => { }); describe("heartbeat transcript pruning", () => { - async function seedSessionStore( - storePath: string, - sessionKey: string, - session: { - sessionId?: string; - updatedAt?: number; - lastChannel: string; - lastProvider: string; - lastTo: string; - }, - ) { - await fs.writeFile( - storePath, - JSON.stringify( - { - [sessionKey]: { - sessionId: session.sessionId ?? "sid", - updatedAt: session.updatedAt ?? Date.now(), - ...session, - }, - }, - null, - 2, - ), - ); - } - async function createTranscriptWithContent(transcriptPath: string, sessionId: string) { const header = { type: "session", @@ -65,33 +37,21 @@ describe("heartbeat transcript pruning", () => { return existingContent; } - async function withTempHeartbeatSandbox( + async function withTempTelegramHeartbeatSandbox( fn: (ctx: { tmpDir: string; storePath: string; replySpy: ReturnType; }) => Promise, ) { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-prune-")); - const storePath = path.join(tmpDir, "sessions.json"); - const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); - const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; - process.env.TELEGRAM_BOT_TOKEN = ""; - try { - return await fn({ tmpDir, storePath, replySpy }); - } finally { - replySpy.mockRestore(); - if (prevTelegramToken === undefined) { - delete process.env.TELEGRAM_BOT_TOKEN; - } else { - process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken; - } - await fs.rm(tmpDir, { recursive: true, force: true }); - } + return withTempHeartbeatSandbox(fn, { + prefix: "openclaw-hb-prune-", + unsetEnvVars: ["TELEGRAM_BOT_TOKEN"], + }); } it("prunes transcript when heartbeat returns HEARTBEAT_OK", async () => { - await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => { + await withTempTelegramHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => { const sessionKey = resolveMainSessionKey(undefined); const sessionId = "test-session-prune"; const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`); @@ -139,7 +99,7 @@ describe("heartbeat transcript pruning", () => { }); it("does not prune transcript when heartbeat returns meaningful content", async () => { - await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => { + await withTempTelegramHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => { const sessionKey = resolveMainSessionKey(undefined); const sessionId = "test-session-no-prune"; const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);