From 7bf08e7344fe9e8f56f4de2117945cf3b6f6696b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 03:28:08 +0100 Subject: [PATCH] refactor: move remaining SDK test helper files --- .../test-helpers}/bundled-channel-entry.ts | 0 .../test-helpers}/bundled-plugin-paths.ts | 0 .../test-helpers}/http-test-server.ts | 0 .../plugin-sdk/test-helpers}/import-fresh.ts | 0 .../test-helpers}/mock-incoming-request.ts | 0 .../test-helpers}/node-builtin-mocks.ts | 0 .../plugin-sdk/test-helpers}/pairing-reply.ts | 0 test/helpers/envelope-timestamp.ts | 43 ----- test/helpers/node-builtin-mocks.test.ts | 54 ------ test/helpers/provider-replay-policy.ts | 35 ---- test/helpers/stt-live-audio.ts | 139 --------------- test/helpers/temp-home.ts | 160 ------------------ 12 files changed, 431 deletions(-) rename {test/helpers => src/plugin-sdk/test-helpers}/bundled-channel-entry.ts (100%) rename {test/helpers => src/plugin-sdk/test-helpers}/bundled-plugin-paths.ts (100%) rename {test/helpers => src/plugin-sdk/test-helpers}/http-test-server.ts (100%) rename {test/helpers => src/plugin-sdk/test-helpers}/import-fresh.ts (100%) rename {test/helpers => src/plugin-sdk/test-helpers}/mock-incoming-request.ts (100%) rename {test/helpers => src/plugin-sdk/test-helpers}/node-builtin-mocks.ts (100%) rename {test/helpers => src/plugin-sdk/test-helpers}/pairing-reply.ts (100%) delete mode 100644 test/helpers/envelope-timestamp.ts delete mode 100644 test/helpers/node-builtin-mocks.test.ts delete mode 100644 test/helpers/provider-replay-policy.ts delete mode 100644 test/helpers/stt-live-audio.ts delete mode 100644 test/helpers/temp-home.ts diff --git a/test/helpers/bundled-channel-entry.ts b/src/plugin-sdk/test-helpers/bundled-channel-entry.ts similarity index 100% rename from test/helpers/bundled-channel-entry.ts rename to src/plugin-sdk/test-helpers/bundled-channel-entry.ts diff --git a/test/helpers/bundled-plugin-paths.ts b/src/plugin-sdk/test-helpers/bundled-plugin-paths.ts similarity index 100% rename from test/helpers/bundled-plugin-paths.ts rename to src/plugin-sdk/test-helpers/bundled-plugin-paths.ts diff --git a/test/helpers/http-test-server.ts b/src/plugin-sdk/test-helpers/http-test-server.ts similarity index 100% rename from test/helpers/http-test-server.ts rename to src/plugin-sdk/test-helpers/http-test-server.ts diff --git a/test/helpers/import-fresh.ts b/src/plugin-sdk/test-helpers/import-fresh.ts similarity index 100% rename from test/helpers/import-fresh.ts rename to src/plugin-sdk/test-helpers/import-fresh.ts diff --git a/test/helpers/mock-incoming-request.ts b/src/plugin-sdk/test-helpers/mock-incoming-request.ts similarity index 100% rename from test/helpers/mock-incoming-request.ts rename to src/plugin-sdk/test-helpers/mock-incoming-request.ts diff --git a/test/helpers/node-builtin-mocks.ts b/src/plugin-sdk/test-helpers/node-builtin-mocks.ts similarity index 100% rename from test/helpers/node-builtin-mocks.ts rename to src/plugin-sdk/test-helpers/node-builtin-mocks.ts diff --git a/test/helpers/pairing-reply.ts b/src/plugin-sdk/test-helpers/pairing-reply.ts similarity index 100% rename from test/helpers/pairing-reply.ts rename to src/plugin-sdk/test-helpers/pairing-reply.ts diff --git a/test/helpers/envelope-timestamp.ts b/test/helpers/envelope-timestamp.ts deleted file mode 100644 index 70c6bbe58c2..00000000000 --- a/test/helpers/envelope-timestamp.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - formatUtcTimestamp, - formatZonedTimestamp, -} from "../../src/infra/format-time/format-datetime.js"; - -export { escapeRegExp } from "../../src/utils.js"; - -type EnvelopeTimestampZone = string; - -export function formatEnvelopeTimestamp(date: Date, zone: EnvelopeTimestampZone = "utc"): string { - const trimmedZone = zone.trim(); - const normalized = trimmedZone.toLowerCase(); - const weekday = (() => { - try { - if (normalized === "utc" || normalized === "gmt") { - return new Intl.DateTimeFormat("en-US", { timeZone: "UTC", weekday: "short" }).format(date); - } - if (normalized === "local" || normalized === "host") { - return new Intl.DateTimeFormat("en-US", { weekday: "short" }).format(date); - } - return new Intl.DateTimeFormat("en-US", { timeZone: trimmedZone, weekday: "short" }).format( - date, - ); - } catch { - return undefined; - } - })(); - - if (normalized === "utc" || normalized === "gmt") { - const ts = formatUtcTimestamp(date); - return weekday ? `${weekday} ${ts}` : ts; - } - if (normalized === "local" || normalized === "host") { - const ts = formatZonedTimestamp(date) ?? formatUtcTimestamp(date); - return weekday ? `${weekday} ${ts}` : ts; - } - const ts = formatZonedTimestamp(date, { timeZone: trimmedZone }) ?? formatUtcTimestamp(date); - return weekday ? `${weekday} ${ts}` : ts; -} - -export function formatLocalEnvelopeTimestamp(date: Date): string { - return formatEnvelopeTimestamp(date, "local"); -} diff --git a/test/helpers/node-builtin-mocks.test.ts b/test/helpers/node-builtin-mocks.test.ts deleted file mode 100644 index 8d6b7d156c3..00000000000 --- a/test/helpers/node-builtin-mocks.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { mockNodeBuiltinModule } from "./node-builtin-mocks.js"; - -describe("mockNodeBuiltinModule", () => { - it("merges partial overrides into the original module", async () => { - const actual = { readFileSync: () => "actual", watch: () => "watch" }; - const readFileSync = () => "mock"; - - const mocked = await mockNodeBuiltinModule(async () => actual, { - readFileSync, - }); - - expect(mocked.readFileSync).toBe(readFileSync); - expect(mocked.watch).toBe(actual.watch); - expect("default" in mocked).toBe(false); - }); - - it("mirrors overrides into the default export when requested", async () => { - const homedir = () => "/tmp/home"; - - const mocked = await mockNodeBuiltinModule( - async () => ({ tmpdir: () => "/tmp" }), - { homedir }, - { mirrorToDefault: true }, - ); - - expect(mocked.default).toMatchObject({ - homedir, - tmpdir: expect.any(Function), - }); - }); - - it("preserves existing default exports while overriding members", async () => { - const actual = { - readFileSync: () => "actual", - default: { - readFileSync: () => "actual", - statSync: () => "stat", - }, - }; - const readFileSync = () => "mock"; - - const mocked = await mockNodeBuiltinModule( - async () => actual, - { readFileSync }, - { mirrorToDefault: true }, - ); - - expect(mocked.default).toMatchObject({ - readFileSync, - statSync: expect.any(Function), - }); - }); -}); diff --git a/test/helpers/provider-replay-policy.ts b/test/helpers/provider-replay-policy.ts deleted file mode 100644 index 1d0902c1005..00000000000 --- a/test/helpers/provider-replay-policy.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { expect } from "vitest"; - -export async function expectPassthroughReplayPolicy(params: { - modelId: string; - plugin: unknown; - providerId: string; - sanitizeThoughtSignatures?: boolean; -}) { - const provider = await registerSingleProviderPlugin(params.plugin as never); - const policy = provider.buildReplayPolicy?.({ - provider: params.providerId, - modelApi: "openai-completions", - modelId: params.modelId, - } as never); - - expect(policy).toMatchObject({ - applyAssistantFirstOrderingFix: false, - validateGeminiTurns: false, - validateAnthropicTurns: false, - }); - - if (params.sanitizeThoughtSignatures) { - expect(policy).toMatchObject({ - sanitizeThoughtSignatures: { - allowBase64Only: true, - includeCamelCase: true, - }, - }); - } else { - expect(policy).not.toHaveProperty("sanitizeThoughtSignatures"); - } - - return provider; -} diff --git a/test/helpers/stt-live-audio.ts b/test/helpers/stt-live-audio.ts deleted file mode 100644 index 199967d587c..00000000000 --- a/test/helpers/stt-live-audio.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { - RealtimeTranscriptionProviderConfig, - RealtimeTranscriptionProviderPlugin, -} from "openclaw/plugin-sdk/realtime-transcription"; -import { expect } from "vitest"; - -const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io"; -const DEFAULT_ELEVENLABS_VOICE_ID = "pMsXgVXv3BLzUgSXRplE"; -const DEFAULT_ELEVENLABS_TTS_MODEL_ID = "eleven_multilingual_v2"; - -export function normalizeTranscriptForMatch(value: string): string { - return value.toLowerCase().replace(/[^a-z0-9]+/g, ""); -} - -type ExpectedTranscriptMatch = RegExp | string; - -const DEFAULT_OPENCLAW_TRANSCRIPT_MATCH = /open(?:claw|flaw|clar)/; - -export async function waitForLiveExpectation(expectation: () => void, timeoutMs = 30_000) { - const started = Date.now(); - let lastError: unknown; - while (Date.now() - started < timeoutMs) { - try { - expectation(); - return; - } catch (error) { - lastError = error; - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } - throw lastError; -} - -export async function synthesizeElevenLabsLiveSpeech(params: { - text: string; - apiKey: string; - outputFormat: "mp3_44100_128" | "ulaw_8000"; - timeoutMs?: number; -}): Promise { - const baseUrl = process.env.ELEVENLABS_BASE_URL?.trim() || DEFAULT_ELEVENLABS_BASE_URL; - const voiceId = process.env.ELEVENLABS_LIVE_VOICE_ID?.trim() || DEFAULT_ELEVENLABS_VOICE_ID; - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), params.timeoutMs ?? 30_000); - try { - const url = new URL(`${baseUrl.replace(/\/+$/, "")}/v1/text-to-speech/${voiceId}`); - url.searchParams.set("output_format", params.outputFormat); - const response = await fetch(url, { - method: "POST", - headers: { - "xi-api-key": params.apiKey, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - text: params.text, - model_id: DEFAULT_ELEVENLABS_TTS_MODEL_ID, - voice_settings: { - stability: 0.5, - similarity_boost: 0.75, - style: 0, - use_speaker_boost: true, - speed: 1, - }, - }), - signal: controller.signal, - }); - if (!response.ok) { - throw new Error(`ElevenLabs live TTS failed (${response.status})`); - } - return Buffer.from(await response.arrayBuffer()); - } finally { - clearTimeout(timeout); - } -} - -export async function streamAudioForLiveTest(params: { - audio: Buffer; - sendAudio: (chunk: Buffer) => void; - chunkSize?: number; - delayMs?: number; -}) { - const chunkSize = params.chunkSize ?? 160; - const delayMs = params.delayMs ?? 5; - for (let offset = 0; offset < params.audio.byteLength; offset += chunkSize) { - params.sendAudio(params.audio.subarray(offset, offset + chunkSize)); - await new Promise((resolve) => setTimeout(resolve, delayMs)); - } -} - -export async function runRealtimeSttLiveTest(params: { - provider: RealtimeTranscriptionProviderPlugin; - providerConfig: RealtimeTranscriptionProviderConfig; - audio: Buffer; - expectedNormalizedText?: ExpectedTranscriptMatch; - timeoutMs?: number; - closeBeforeWait?: boolean; - chunkSize?: number; - delayMs?: number; -}): Promise<{ transcripts: string[]; partials: string[]; errors: Error[] }> { - const transcripts: string[] = []; - const partials: string[] = []; - const errors: Error[] = []; - const expected = params.expectedNormalizedText ?? DEFAULT_OPENCLAW_TRANSCRIPT_MATCH; - const session = params.provider.createSession({ - providerConfig: params.providerConfig, - onPartial: (partial) => partials.push(partial), - onTranscript: (transcript) => transcripts.push(transcript), - onError: (error) => errors.push(error), - }); - - try { - await session.connect(); - await streamAudioForLiveTest({ - audio: params.audio, - sendAudio: (chunk) => session.sendAudio(chunk), - chunkSize: params.chunkSize, - delayMs: params.delayMs, - }); - if (params.closeBeforeWait) { - session.close(); - } - - await waitForLiveExpectation(() => { - if (errors[0]) { - throw errors[0]; - } - const normalized = normalizeTranscriptForMatch(transcripts.join(" ")); - if (typeof expected === "string") { - expect(normalized).toContain(expected); - } else { - expect(normalized).toMatch(expected); - } - }, params.timeoutMs ?? 60_000); - } finally { - session.close(); - } - - expect(partials.length + transcripts.length).toBeGreaterThan(0); - return { transcripts, partials, errors }; -} diff --git a/test/helpers/temp-home.ts b/test/helpers/temp-home.ts deleted file mode 100644 index 016c3368ed9..00000000000 --- a/test/helpers/temp-home.ts +++ /dev/null @@ -1,160 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { cleanupSessionStateForTest } from "../../src/test-utils/session-state-cleanup.js"; - -type EnvValue = string | undefined | ((home: string) => string | undefined); - -type EnvSnapshot = { - home: string | undefined; - userProfile: string | undefined; - homeDrive: string | undefined; - homePath: string | undefined; - openclawHome: string | undefined; - stateDir: string | undefined; -}; - -type SharedHomeRootState = { - rootPromise: Promise; - nextCaseId: number; -}; - -const SHARED_HOME_ROOTS = new Map(); - -function snapshotEnv(): EnvSnapshot { - return { - home: process.env.HOME, - userProfile: process.env.USERPROFILE, - homeDrive: process.env.HOMEDRIVE, - homePath: process.env.HOMEPATH, - openclawHome: process.env.OPENCLAW_HOME, - stateDir: process.env.OPENCLAW_STATE_DIR, - }; -} - -function restoreEnv(snapshot: EnvSnapshot) { - 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_HOME", snapshot.openclawHome); - restoreKey("OPENCLAW_STATE_DIR", snapshot.stateDir); -} - -function snapshotExtraEnv(keys: string[]): Record { - const snapshot: Record = {}; - for (const key of keys) { - snapshot[key] = process.env[key]; - } - return snapshot; -} - -function restoreExtraEnv(snapshot: Record) { - for (const [key, value] of Object.entries(snapshot)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } -} - -function setTempHome(base: string) { - process.env.HOME = base; - process.env.USERPROFILE = base; - // Ensure tests using HOME isolation aren't affected by leaked OPENCLAW_HOME. - delete process.env.OPENCLAW_HOME; - process.env.OPENCLAW_STATE_DIR = path.join(base, ".openclaw"); - - if (process.platform !== "win32") { - return; - } - const match = base.match(/^([A-Za-z]:)(.*)$/); - if (!match) { - return; - } - process.env.HOMEDRIVE = match[1]; - process.env.HOMEPATH = match[2] || "\\"; -} - -async function allocateTempHomeBase(prefix: string): Promise { - let state = SHARED_HOME_ROOTS.get(prefix); - if (!state) { - state = { - rootPromise: fs.mkdtemp(path.join(os.tmpdir(), prefix)), - nextCaseId: 0, - }; - SHARED_HOME_ROOTS.set(prefix, state); - } - const root = await state.rootPromise; - const base = path.join(root, `case-${state.nextCaseId++}`); - await fs.mkdir(base, { recursive: true }); - return base; -} - -export async function withTempHome( - fn: (home: string) => Promise, - opts: { - env?: Record; - prefix?: string; - skipSessionCleanup?: boolean; - } = {}, -): Promise { - const prefix = opts.prefix ?? "openclaw-test-home-"; - const base = await allocateTempHomeBase(prefix); - const snapshot = snapshotEnv(); - const envKeys = Object.keys(opts.env ?? {}); - for (const key of envKeys) { - if (key === "HOME" || key === "USERPROFILE" || key === "HOMEDRIVE" || key === "HOMEPATH") { - throw new Error(`withTempHome: use built-in home env (got ${key})`); - } - } - const envSnapshot = snapshotExtraEnv(envKeys); - - setTempHome(base); - await fs.mkdir(path.join(base, ".openclaw", "agents", "main", "sessions"), { recursive: true }); - if (opts.env) { - for (const [key, raw] of Object.entries(opts.env)) { - const value = typeof raw === "function" ? raw(base) : raw; - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - } - - try { - return await fn(base); - } finally { - if (!opts.skipSessionCleanup) { - await cleanupSessionStateForTest().catch(() => undefined); - } - restoreExtraEnv(envSnapshot); - restoreEnv(snapshot); - try { - if (process.platform === "win32") { - await fs.rm(base, { - recursive: true, - force: true, - maxRetries: 10, - retryDelay: 50, - }); - } else { - await fs.rm(base, { - recursive: true, - force: true, - }); - } - } catch { - // ignore cleanup failures in tests - } - } -}