From dac8f5ba3f5e5f1e9e6628b9e30a121ca54fbf41 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Feb 2026 22:28:50 +0000 Subject: [PATCH] perf(test): trim fixture and import overhead in hot suites --- src/auto-reply/reply.block-streaming.test.ts | 32 +++--- ...-contract-form-layout-act-commands.test.ts | 5 +- src/canvas-host/server.test.ts | 44 +++++--- src/config/io.write-config.test.ts | 72 +++++++++++- ...onse-has-heartbeat-ok-but-includes.test.ts | 19 +++- src/infra/gateway-lock.test.ts | 44 +++++--- src/memory/index.test.ts | 20 +++- src/memory/qmd-manager.test.ts | 105 ++++++++---------- src/telegram/bot.test.ts | 18 ++- src/web/media.test.ts | 69 ++++-------- 10 files changed, 262 insertions(+), 166 deletions(-) diff --git a/src/auto-reply/reply.block-streaming.test.ts b/src/auto-reply/reply.block-streaming.test.ts index 5a1f97d1d..21e8bdf17 100644 --- a/src/auto-reply/reply.block-streaming.test.ts +++ b/src/auto-reply/reply.block-streaming.test.ts @@ -39,23 +39,20 @@ describe("block streaming", () => { ]); }); - async function waitForCalls(fn: () => number, calls: number) { - const deadline = Date.now() + 5000; - while (fn() < calls) { - if (Date.now() > deadline) { - throw new Error(`Expected ${calls} call(s), got ${fn()}`); - } - await new Promise((resolve) => setTimeout(resolve, 5)); - } - } - it("waits for block replies before returning final payloads", async () => { await withTempHome(async (home) => { let releaseTyping: (() => void) | undefined; const typingGate = new Promise((resolve) => { releaseTyping = resolve; }); - const onReplyStart = vi.fn(() => typingGate); + let resolveOnReplyStart: (() => void) | undefined; + const onReplyStartCalled = new Promise((resolve) => { + resolveOnReplyStart = resolve; + }); + const onReplyStart = vi.fn(() => { + resolveOnReplyStart?.(); + return typingGate; + }); const onBlockReply = vi.fn().mockResolvedValue(undefined); const impl = async (params: RunEmbeddedPiAgentParams) => { @@ -95,7 +92,7 @@ describe("block streaming", () => { }, ); - await waitForCalls(() => onReplyStart.mock.calls.length, 1); + await onReplyStartCalled; releaseTyping?.(); const res = await replyPromise; @@ -110,7 +107,14 @@ describe("block streaming", () => { const typingGate = new Promise((resolve) => { releaseTyping = resolve; }); - const onReplyStart = vi.fn(() => typingGate); + let resolveOnReplyStart: (() => void) | undefined; + const onReplyStartCalled = new Promise((resolve) => { + resolveOnReplyStart = resolve; + }); + const onReplyStart = vi.fn(() => { + resolveOnReplyStart?.(); + return typingGate; + }); const seen: string[] = []; const onBlockReply = vi.fn(async (payload) => { seen.push(payload.text ?? ""); @@ -154,7 +158,7 @@ describe("block streaming", () => { }, ); - await waitForCalls(() => onReplyStart.mock.calls.length, 1); + await onReplyStartCalled; releaseTyping?.(); const res = await replyPromise; diff --git a/src/browser/server.agent-contract-form-layout-act-commands.test.ts b/src/browser/server.agent-contract-form-layout-act-commands.test.ts index a63eef29c..2c5c22347 100644 --- a/src/browser/server.agent-contract-form-layout-act-commands.test.ts +++ b/src/browser/server.agent-contract-form-layout-act-commands.test.ts @@ -156,6 +156,9 @@ vi.mock("./screenshot.js", () => ({ })), })); +const { startBrowserControlServerFromConfig, stopBrowserControlServer } = + await import("./server.js"); + async function getFreePort(): Promise { while (true) { const port = await new Promise((resolve, reject) => { @@ -274,12 +277,10 @@ describe("browser control server", () => { } else { process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; } - const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); const startServerAndBase = async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json()); diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index b768aa02b..5c360cd1c 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import { createServer } from "node:http"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { WebSocket } from "ws"; import { rawDataToString } from "../infra/ws.js"; import { defaultRuntime } from "../runtime.js"; @@ -11,6 +11,23 @@ import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH, injectCanvasLiveReload } f import { createCanvasHostHandler, startCanvasHost } from "./server.js"; describe("canvas host", () => { + let fixtureRoot = ""; + let fixtureCount = 0; + + const createCaseDir = async () => { + const dir = path.join(fixtureRoot, `case-${fixtureCount++}`); + await fs.mkdir(dir, { recursive: true }); + return dir; + }; + + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-fixtures-")); + }); + + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + }); + it("injects live reload script", () => { const out = injectCanvasLiveReload("Hello"); expect(out).toContain(CANVAS_WS_PATH); @@ -20,7 +37,7 @@ describe("canvas host", () => { }); it("creates a default index.html when missing", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-")); + const dir = await createCaseDir(); const server = await startCanvasHost({ runtime: defaultRuntime, @@ -39,12 +56,11 @@ describe("canvas host", () => { expect(html).toContain(CANVAS_WS_PATH); } finally { await server.close(); - await fs.rm(dir, { recursive: true, force: true }); } }); it("skips live reload injection when disabled", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-")); + const dir = await createCaseDir(); await fs.writeFile(path.join(dir, "index.html"), "no-reload", "utf8"); const server = await startCanvasHost({ @@ -67,12 +83,11 @@ describe("canvas host", () => { expect(wsRes.status).toBe(404); } finally { await server.close(); - await fs.rm(dir, { recursive: true, force: true }); } }); it("serves canvas content from the mounted base path", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-")); + const dir = await createCaseDir(); await fs.writeFile(path.join(dir, "index.html"), "v1", "utf8"); const handler = await createCanvasHostHandler({ @@ -116,12 +131,11 @@ describe("canvas host", () => { await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())), ); - await fs.rm(dir, { recursive: true, force: true }); } }); it("reuses a handler without closing it twice", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-")); + const dir = await createCaseDir(); await fs.writeFile(path.join(dir, "index.html"), "v1", "utf8"); const handler = await createCanvasHostHandler({ @@ -149,12 +163,11 @@ describe("canvas host", () => { await server.close(); expect(closeSpy).not.toHaveBeenCalled(); await originalClose(); - await fs.rm(dir, { recursive: true, force: true }); } }); it("serves HTML with injection and broadcasts reload on file changes", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-")); + const dir = await createCaseDir(); const index = path.join(dir, "index.html"); await fs.writeFile(index, "v1", "utf8"); @@ -194,18 +207,16 @@ describe("canvas host", () => { }); }); - await new Promise((resolve) => setTimeout(resolve, 100)); await fs.writeFile(index, "v2", "utf8"); expect(await msg).toBe("reload"); ws.close(); } finally { await server.close(); - await fs.rm(dir, { recursive: true, force: true }); } }, 20_000); it("serves the gateway-hosted A2UI scaffold", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-")); + const dir = await createCaseDir(); const a2uiRoot = path.resolve(process.cwd(), "src/canvas-host/a2ui"); const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js"); let createdBundle = false; @@ -243,12 +254,11 @@ describe("canvas host", () => { if (createdBundle) { await fs.rm(bundlePath, { force: true }); } - await fs.rm(dir, { recursive: true, force: true }); } }); it("rejects traversal-style A2UI asset requests", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-")); + const dir = await createCaseDir(); const a2uiRoot = path.resolve(process.cwd(), "src/canvas-host/a2ui"); const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js"); let createdBundle = false; @@ -277,12 +287,11 @@ describe("canvas host", () => { if (createdBundle) { await fs.rm(bundlePath, { force: true }); } - await fs.rm(dir, { recursive: true, force: true }); } }); it("rejects A2UI symlink escapes", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-")); + const dir = await createCaseDir(); const a2uiRoot = path.resolve(process.cwd(), "src/canvas-host/a2ui"); const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js"); const linkName = `test-link-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`; @@ -320,7 +329,6 @@ describe("canvas host", () => { if (createdBundle) { await fs.rm(bundlePath, { force: true }); } - await fs.rm(dir, { recursive: true, force: true }); } }); }); diff --git a/src/config/io.write-config.test.ts b/src/config/io.write-config.test.ts index 917a3f3f0..8bdfb7981 100644 --- a/src/config/io.write-config.test.ts +++ b/src/config/io.write-config.test.ts @@ -1,10 +1,78 @@ import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { createConfigIO } from "./io.js"; -import { withTempHome } from "./test-helpers.js"; + +type HomeEnvSnapshot = { + home: string | undefined; + userProfile: string | undefined; + homeDrive: string | undefined; + homePath: string | undefined; + stateDir: string | undefined; +}; + +function snapshotHomeEnv(): HomeEnvSnapshot { + return { + home: process.env.HOME, + userProfile: process.env.USERPROFILE, + homeDrive: process.env.HOMEDRIVE, + homePath: process.env.HOMEPATH, + stateDir: process.env.OPENCLAW_STATE_DIR, + }; +} + +function restoreHomeEnv(snapshot: HomeEnvSnapshot) { + const restoreKey = (key: string, value: string | undefined) => { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + }; + restoreKey("HOME", snapshot.home); + restoreKey("USERPROFILE", snapshot.userProfile); + restoreKey("HOMEDRIVE", snapshot.homeDrive); + restoreKey("HOMEPATH", snapshot.homePath); + restoreKey("OPENCLAW_STATE_DIR", snapshot.stateDir); +} describe("config io write", () => { + let fixtureRoot = ""; + let fixtureCount = 0; + + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-io-")); + }); + + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + }); + + const withTempHome = async (fn: (home: string) => Promise): Promise => { + const home = path.join(fixtureRoot, `home-${fixtureCount++}`); + await fs.mkdir(path.join(home, ".openclaw"), { recursive: true }); + + const snapshot = snapshotHomeEnv(); + process.env.HOME = home; + process.env.USERPROFILE = home; + process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw"); + + if (process.platform === "win32") { + const match = home.match(/^([A-Za-z]:)(.*)$/); + if (match) { + process.env.HOMEDRIVE = match[1]; + process.env.HOMEPATH = match[2] || "\\"; + } + } + + try { + return await fn(home); + } finally { + restoreHomeEnv(snapshot); + } + }; + it("persists caller changes onto resolved config without leaking runtime defaults", async () => { await withTempHome(async (home) => { const configPath = path.join(home, ".openclaw", "openclaw.json"); diff --git a/src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts b/src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts index 674763f8e..079657262 100644 --- a/src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts +++ b/src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts @@ -1,10 +1,10 @@ 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 { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { CliDeps } from "../cli/deps.js"; import type { OpenClawConfig } from "../config/config.js"; import type { CronJob } from "./types.js"; -import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; import { telegramOutbound } from "../channels/plugins/outbound/telegram.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; @@ -26,8 +26,13 @@ import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js"; import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; +let fixtureRoot = ""; +let fixtureCount = 0; + async function withTempHome(fn: (home: string) => Promise): Promise { - return withTempHomeBase(fn, { prefix: "openclaw-cron-" }); + const home = path.join(fixtureRoot, `home-${fixtureCount++}`); + await fs.mkdir(path.join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true }); + return await fn(home); } async function writeSessionStore(home: string) { @@ -87,6 +92,14 @@ function makeJob(payload: CronJob["payload"]): CronJob { } describe("runCronIsolatedAgentTurn", () => { + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-fixtures-")); + }); + + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + }); + beforeEach(() => { vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(loadModelCatalog).mockResolvedValue([]); diff --git a/src/infra/gateway-lock.test.ts b/src/infra/gateway-lock.test.ts index 12a93fd58..3b19f25dd 100644 --- a/src/infra/gateway-lock.test.ts +++ b/src/infra/gateway-lock.test.ts @@ -3,12 +3,16 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { resolveConfigPath, resolveGatewayLockDir, resolveStateDir } from "../config/paths.js"; import { acquireGatewayLock, GatewayLockError } from "./gateway-lock.js"; +let fixtureRoot = ""; +let fixtureCount = 0; + async function makeEnv() { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-lock-")); + const dir = path.join(fixtureRoot, `case-${fixtureCount++}`); + await fs.mkdir(dir, { recursive: true }); const configPath = path.join(dir, "openclaw.json"); await fs.writeFile(configPath, "{}", "utf8"); await fs.mkdir(resolveGatewayLockDir(), { recursive: true }); @@ -18,9 +22,7 @@ async function makeEnv() { OPENCLAW_STATE_DIR: dir, OPENCLAW_CONFIG_PATH: configPath, }, - cleanup: async () => { - await fs.rm(dir, { recursive: true, force: true }); - }, + cleanup: async () => {}, }; } @@ -61,13 +63,21 @@ function makeProcStat(pid: number, startTime: number) { } describe("gateway lock", () => { + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-lock-")); + }); + + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + }); + it("blocks concurrent acquisition until release", async () => { const { env, cleanup } = await makeEnv(); const lock = await acquireGatewayLock({ env, allowInTests: true, - timeoutMs: 200, - pollIntervalMs: 20, + timeoutMs: 80, + pollIntervalMs: 5, }); expect(lock).not.toBeNull(); @@ -75,8 +85,8 @@ describe("gateway lock", () => { acquireGatewayLock({ env, allowInTests: true, - timeoutMs: 200, - pollIntervalMs: 20, + timeoutMs: 80, + pollIntervalMs: 5, }), ).rejects.toBeInstanceOf(GatewayLockError); @@ -84,8 +94,8 @@ describe("gateway lock", () => { const lock2 = await acquireGatewayLock({ env, allowInTests: true, - timeoutMs: 200, - pollIntervalMs: 20, + timeoutMs: 80, + pollIntervalMs: 5, }); await lock2?.release(); await cleanup(); @@ -114,8 +124,8 @@ describe("gateway lock", () => { const lock = await acquireGatewayLock({ env, allowInTests: true, - timeoutMs: 200, - pollIntervalMs: 20, + timeoutMs: 80, + pollIntervalMs: 5, platform: "linux", }); expect(lock).not.toBeNull(); @@ -148,8 +158,8 @@ describe("gateway lock", () => { acquireGatewayLock({ env, allowInTests: true, - timeoutMs: 120, - pollIntervalMs: 20, + timeoutMs: 50, + pollIntervalMs: 5, staleMs: 10_000, platform: "linux", }), @@ -173,8 +183,8 @@ describe("gateway lock", () => { const lock = await acquireGatewayLock({ env, allowInTests: true, - timeoutMs: 200, - pollIntervalMs: 20, + timeoutMs: 80, + pollIntervalMs: 5, staleMs: 1, platform: "linux", }); diff --git a/src/memory/index.test.ts b/src/memory/index.test.ts index 3f01ab855..3e319a5fd 100644 --- a/src/memory/index.test.ts +++ b/src/memory/index.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { getMemorySearchManager, type MemoryIndexManager } from "./index.js"; let embedBatchCalls = 0; @@ -34,14 +34,25 @@ vi.mock("./embeddings.js", () => { }); describe("memory index", () => { + let fixtureRoot = ""; + let fixtureCount = 0; let workspaceDir: string; let indexPath: string; let manager: MemoryIndexManager | null = null; + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-fixtures-")); + }); + + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + }); + beforeEach(async () => { embedBatchCalls = 0; failEmbeddings = false; - workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-")); + workspaceDir = path.join(fixtureRoot, `case-${fixtureCount++}`); + await fs.mkdir(workspaceDir, { recursive: true }); indexPath = path.join(workspaceDir, "index.sqlite"); await fs.mkdir(path.join(workspaceDir, "memory")); await fs.writeFile( @@ -56,7 +67,6 @@ describe("memory index", () => { await manager.close(); manager = null; } - await fs.rm(workspaceDir, { recursive: true, force: true }); }); it("indexes memory files and searches by vector", async () => { @@ -270,7 +280,7 @@ describe("memory index", () => { }); it("hybrid weights can favor vector-only matches over keyword-only matches", async () => { - const manyAlpha = Array.from({ length: 200 }, () => "Alpha").join(" "); + const manyAlpha = Array.from({ length: 80 }, () => "Alpha").join(" "); await fs.writeFile( path.join(workspaceDir, "memory", "vector-only.md"), "Alpha beta. Alpha beta. Alpha beta. Alpha beta.", @@ -328,7 +338,7 @@ describe("memory index", () => { }); it("hybrid weights can favor keyword matches when text weight dominates", async () => { - const manyAlpha = Array.from({ length: 200 }, () => "Alpha").join(" "); + const manyAlpha = Array.from({ length: 80 }, () => "Alpha").join(" "); await fs.writeFile( path.join(workspaceDir, "memory", "vector-only.md"), "Alpha beta. Alpha beta. Alpha beta. Alpha beta.", diff --git a/src/memory/qmd-manager.test.ts b/src/memory/qmd-manager.test.ts index e83968028..a4877417c 100644 --- a/src/memory/qmd-manager.test.ts +++ b/src/memory/qmd-manager.test.ts @@ -2,7 +2,7 @@ import { EventEmitter } from "node:events"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { logWarnMock, logDebugMock, logInfoMock } = vi.hoisted(() => ({ logWarnMock: vi.fn(), @@ -44,6 +44,18 @@ function createMockChild(params?: { autoClose?: boolean; closeDelayMs?: number } return child; } +function emitAndClose( + child: MockChild, + stream: "stdout" | "stderr", + data: string, + code: number = 0, +) { + queueMicrotask(() => { + child[stream].emit("data", data); + child.closeWith(code); + }); +} + vi.mock("../logging/subsystem.js", () => ({ createSubsystemLogger: () => { const logger = { @@ -66,19 +78,30 @@ import { QmdMemoryManager } from "./qmd-manager.js"; const spawnMock = mockedSpawn as unknown as vi.Mock; describe("QmdMemoryManager", () => { + let fixtureRoot: string; + let fixtureCount = 0; let tmpRoot: string; let workspaceDir: string; let stateDir: string; let cfg: OpenClawConfig; const agentId = "main"; + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qmd-manager-test-fixtures-")); + }); + + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + }); + beforeEach(async () => { spawnMock.mockReset(); spawnMock.mockImplementation(() => createMockChild()); logWarnMock.mockReset(); logDebugMock.mockReset(); logInfoMock.mockReset(); - tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qmd-manager-test-")); + tmpRoot = path.join(fixtureRoot, `case-${fixtureCount++}`); + await fs.mkdir(tmpRoot, { recursive: true }); workspaceDir = path.join(tmpRoot, "workspace"); await fs.mkdir(workspaceDir, { recursive: true }); stateDir = path.join(tmpRoot, "state"); @@ -102,7 +125,6 @@ describe("QmdMemoryManager", () => { afterEach(async () => { vi.useRealTimers(); delete process.env.OPENCLAW_STATE_DIR; - await fs.rm(tmpRoot, { recursive: true, force: true }); }); it("debounces back-to-back sync calls", async () => { @@ -158,14 +180,11 @@ describe("QmdMemoryManager", () => { const createPromise = QmdMemoryManager.create({ cfg, agentId, resolved }); const race = await Promise.race([ createPromise.then(() => "created" as const), - new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 80)), + new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 40)), ]); expect(race).toBe("created"); - - if (!releaseUpdate) { - throw new Error("update child missing"); - } - releaseUpdate(); + await waitForCondition(() => releaseUpdate !== null, 200); + releaseUpdate?.(); const manager = await createPromise; await manager?.close(); }); @@ -202,14 +221,11 @@ describe("QmdMemoryManager", () => { const createPromise = QmdMemoryManager.create({ cfg, agentId, resolved }); const race = await Promise.race([ createPromise.then(() => "created" as const), - new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 80)), + new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 40)), ]); expect(race).toBe("timeout"); - - if (!releaseUpdate) { - throw new Error("update child missing"); - } - releaseUpdate(); + await waitForCondition(() => releaseUpdate !== null, 200); + releaseUpdate?.(); const manager = await createPromise; await manager?.close(); }); @@ -301,10 +317,7 @@ describe("QmdMemoryManager", () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { if (args[0] === "search") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { - child.stdout.emit("data", "[]"); - child.closeWith(0); - }, 0); + emitAndClose(child, "stdout", "[]"); return child; } return createMockChild(); @@ -348,18 +361,12 @@ describe("QmdMemoryManager", () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { if (args[0] === "search") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { - child.stderr.emit("data", "unknown flag: --json"); - child.closeWith(2); - }, 0); + emitAndClose(child, "stderr", "unknown flag: --json", 2); return child; } if (args[0] === "query") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { - child.stdout.emit("data", "[]"); - child.closeWith(0); - }, 0); + emitAndClose(child, "stdout", "[]"); return child; } return createMockChild(); @@ -435,7 +442,7 @@ describe("QmdMemoryManager", () => { const inFlight = manager.sync({ reason: "interval" }); const forced = manager.sync({ reason: "manual", force: true }); - await new Promise((resolve) => setTimeout(resolve, 20)); + await waitForCondition(() => updateCalls >= 1, 80); expect(updateCalls).toBe(1); if (!releaseFirstUpdate) { throw new Error("first update release missing"); @@ -496,14 +503,14 @@ describe("QmdMemoryManager", () => { const inFlight = manager.sync({ reason: "interval" }); const forcedOne = manager.sync({ reason: "manual", force: true }); - await new Promise((resolve) => setTimeout(resolve, 20)); + await waitForCondition(() => updateCalls >= 1, 80); expect(updateCalls).toBe(1); if (!releaseFirstUpdate) { throw new Error("first update release missing"); } releaseFirstUpdate(); - await waitForCondition(() => updateCalls >= 2, 200); + await waitForCondition(() => updateCalls >= 2, 120); const forcedTwo = manager.sync({ reason: "manual-again", force: true }); if (!releaseSecondUpdate) { @@ -535,10 +542,7 @@ describe("QmdMemoryManager", () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { if (args[0] === "query") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { - child.stdout.emit("data", "[]"); - child.closeWith(0); - }, 0); + emitAndClose(child, "stdout", "[]"); return child; } return createMockChild(); @@ -805,13 +809,11 @@ describe("QmdMemoryManager", () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { if (args[0] === "query") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { - child.stdout.emit( - "data", - JSON.stringify([{ docid: "abc123", score: 1, snippet: "@@ -1,1\nremember this" }]), - ); - child.closeWith(0); - }, 0); + emitAndClose( + child, + "stdout", + JSON.stringify([{ docid: "abc123", score: 1, snippet: "@@ -1,1\nremember this" }]), + ); return child; } return createMockChild(); @@ -844,10 +846,7 @@ describe("QmdMemoryManager", () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { if (args[0] === "query") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { - child.stdout.emit("data", "No results found."); - child.closeWith(0); - }, 0); + emitAndClose(child, "stdout", "No results found."); return child; } return createMockChild(); @@ -870,10 +869,7 @@ describe("QmdMemoryManager", () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { if (args[0] === "query") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { - child.stdout.emit("data", "No results found\n\n"); - child.closeWith(0); - }, 0); + emitAndClose(child, "stdout", "No results found\n\n"); return child; } return createMockChild(); @@ -896,10 +892,7 @@ describe("QmdMemoryManager", () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { if (args[0] === "query") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { - child.stderr.emit("data", "No results found.\n"); - child.closeWith(0); - }, 0); + emitAndClose(child, "stderr", "No results found.\n"); return child; } return createMockChild(); @@ -922,11 +915,11 @@ describe("QmdMemoryManager", () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { if (args[0] === "query") { const child = createMockChild({ autoClose: false }); - setTimeout(() => { + queueMicrotask(() => { child.stdout.emit("data", " \n"); child.stderr.emit("data", "unexpected parser error"); child.closeWith(0); - }, 0); + }); return child; } return createMockChild(); @@ -1034,7 +1027,7 @@ async function waitForCondition(check: () => boolean, timeoutMs: number): Promis if (check()) { return; } - await new Promise((resolve) => setTimeout(resolve, 5)); + await new Promise((resolve) => setTimeout(resolve, 2)); } throw new Error("condition was not met in time"); } diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 3c2c63a7d..cb919a023 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js"; import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js"; import { @@ -23,6 +23,13 @@ vi.mock("../auto-reply/skill-commands.js", () => ({ const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-bot-${Math.random().toString(16).slice(2)}.json`, })); +const tempDirs: string[] = []; + +function createTempDir(prefix: string): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} function resolveSkillCommands(config: Parameters[0]) { return listSkillCommandsForAgents({ cfg: config }); @@ -208,6 +215,13 @@ describe("createTelegramBot", () => { process.env.TZ = ORIGINAL_TZ; }); + afterAll(() => { + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } + tempDirs.length = 0; + }); + it("installs grammY throttler", () => { createTelegramBot({ token: "tok" }); expect(throttlerSpy).toHaveBeenCalledTimes(1); @@ -1214,7 +1228,7 @@ describe("createTelegramBot", () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); - const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-")); + const storeDir = createTempDir("openclaw-telegram-"); const storePath = path.join(storeDir, "sessions.json"); fs.writeFileSync( storePath, diff --git a/src/web/media.test.ts b/src/web/media.test.ts index 0dee4ac0c..b507b02c8 100644 --- a/src/web/media.test.ts +++ b/src/web/media.test.ts @@ -9,6 +9,8 @@ import { loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } from "./media.js"; let fixtureRoot = ""; let fixtureFileCount = 0; +let largeJpegBuffer: Buffer; +let tinyPngBuffer: Buffer; async function writeTempFile(buffer: Buffer, ext: string): Promise { const file = path.join(fixtureRoot, `media-${fixtureFileCount++}${ext}`); @@ -27,23 +29,27 @@ function buildDeterministicBytes(length: number): Buffer { } async function createLargeTestJpeg(): Promise<{ buffer: Buffer; file: string }> { - const buffer = await sharp({ + const file = await writeTempFile(largeJpegBuffer, ".jpg"); + return { buffer: largeJpegBuffer, file }; +} + +beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-test-")); + largeJpegBuffer = await sharp({ create: { - width: 1600, - height: 1600, + width: 1200, + height: 1200, channels: 3, background: "#ff0000", }, }) .jpeg({ quality: 95 }) .toBuffer(); - - const file = await writeTempFile(buffer, ".jpg"); - return { buffer, file }; -} - -beforeAll(async () => { - fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-test-")); + tinyPngBuffer = await sharp({ + create: { width: 10, height: 10, channels: 3, background: "#00ff00" }, + }) + .png() + .toBuffer(); }); afterAll(async () => { @@ -68,18 +74,7 @@ describe("web media loading", () => { }); it("compresses large local images under the provided cap", async () => { - const buffer = await sharp({ - create: { - width: 1200, - height: 1200, - channels: 3, - background: "#ff0000", - }, - }) - .jpeg({ quality: 95 }) - .toBuffer(); - - const file = await writeTempFile(buffer, ".jpg"); + const { buffer, file } = await createLargeTestJpeg(); const cap = Math.floor(buffer.length * 0.8); const result = await loadWebMedia(file, cap); @@ -109,12 +104,7 @@ describe("web media loading", () => { }); it("sniffs mime before extension when loading local files", async () => { - const pngBuffer = await sharp({ - create: { width: 2, height: 2, channels: 3, background: "#00ff00" }, - }) - .png() - .toBuffer(); - const wrongExt = await writeTempFile(pngBuffer, ".bin"); + const wrongExt = await writeTempFile(tinyPngBuffer, ".bin"); const result = await loadWebMedia(wrongExt, 1024 * 1024); @@ -292,7 +282,7 @@ describe("web media loading", () => { }); it("falls back to JPEG when PNG alpha cannot fit under cap", async () => { - const sizes = [320, 448, 640]; + const sizes = [256, 320, 448]; let pngBuffer: Buffer | null = null; let smallestPng: Awaited> | null = null; let jpegOptimized: Awaited> | null = null; @@ -333,12 +323,7 @@ describe("web media loading", () => { describe("local media root guard", () => { it("rejects local paths outside allowed roots", async () => { - const pngBuffer = await sharp({ - create: { width: 10, height: 10, channels: 3, background: "#00ff00" }, - }) - .png() - .toBuffer(); - const file = await writeTempFile(pngBuffer, ".png"); + const file = await writeTempFile(tinyPngBuffer, ".png"); // Explicit roots that don't contain the temp file. await expect( @@ -347,24 +332,14 @@ describe("local media root guard", () => { }); it("allows local paths under an explicit root", async () => { - const pngBuffer = await sharp({ - create: { width: 10, height: 10, channels: 3, background: "#00ff00" }, - }) - .png() - .toBuffer(); - const file = await writeTempFile(pngBuffer, ".png"); + const file = await writeTempFile(tinyPngBuffer, ".png"); const result = await loadWebMedia(file, 1024 * 1024, { localRoots: [os.tmpdir()] }); expect(result.kind).toBe("image"); }); it("allows any path when localRoots is 'any'", async () => { - const pngBuffer = await sharp({ - create: { width: 10, height: 10, channels: 3, background: "#00ff00" }, - }) - .png() - .toBuffer(); - const file = await writeTempFile(pngBuffer, ".png"); + const file = await writeTempFile(tinyPngBuffer, ".png"); const result = await loadWebMedia(file, 1024 * 1024, { localRoots: "any" }); expect(result.kind).toBe("image");