refactor: move remaining SDK test helper files

This commit is contained in:
Peter Steinberger
2026-04-28 03:28:08 +01:00
parent e1acb61317
commit 7bf08e7344
12 changed files with 0 additions and 431 deletions

View File

@@ -1,38 +0,0 @@
import { expect, it } from "vitest";
type BundledChannelEntry = {
id: string;
kind?: string;
name: string;
};
type BundledChannelSetupEntry = {
kind?: string;
loadSetupPlugin?: unknown;
};
export function assertBundledChannelEntries(params: {
entry: BundledChannelEntry;
expectedId: string;
expectedName: string;
setupEntry: BundledChannelSetupEntry;
channelMessage?: string;
setupMessage?: string;
}) {
it(
params.channelMessage ?? "declares the channel plugin without importing the broad api barrel",
() => {
expect(params.entry.kind).toBe("bundled-channel-entry");
expect(params.entry.id).toBe(params.expectedId);
expect(params.entry.name).toBe(params.expectedName);
},
);
it(
params.setupMessage ?? "declares the setup plugin without importing the broad api barrel",
() => {
expect(params.setupEntry.kind).toBe("bundled-channel-setup-entry");
expect(typeof params.setupEntry.loadSetupPlugin).toBe("function");
},
);
}

View File

@@ -1,59 +0,0 @@
export const BUNDLED_PLUGIN_ROOT_DIR = "extensions";
export const BUNDLED_PLUGIN_PATH_PREFIX = `${BUNDLED_PLUGIN_ROOT_DIR}/`;
export const BUNDLED_PLUGIN_TEST_GLOB = `${BUNDLED_PLUGIN_ROOT_DIR}/**/*.test.ts`;
export function bundledPluginRoot(pluginId: string): string {
return `${BUNDLED_PLUGIN_PATH_PREFIX}${pluginId}`;
}
export function bundledPluginFile(pluginId: string, relativePath: string): string {
return `${bundledPluginRoot(pluginId)}/${relativePath}`;
}
function joinRoot(baseDir: string, relativePath: string): string {
return `${baseDir.replace(/\/$/, "")}/${relativePath}`;
}
export function bundledPluginDirPrefix(pluginId: string, relativeDir: string): string {
return `${bundledPluginRoot(pluginId)}/${relativeDir.replace(/\/$/, "")}/`;
}
export function bundledPluginRootAt(baseDir: string, pluginId: string): string {
return joinRoot(baseDir, bundledPluginRoot(pluginId));
}
export function bundledPluginFileAt(
baseDir: string,
pluginId: string,
relativePath: string,
): string {
return joinRoot(baseDir, bundledPluginFile(pluginId, relativePath));
}
export function bundledDistPluginRoot(pluginId: string): string {
return `dist/${bundledPluginRoot(pluginId)}`;
}
export function bundledDistPluginFile(pluginId: string, relativePath: string): string {
return `${bundledDistPluginRoot(pluginId)}/${relativePath}`;
}
export function bundledDistPluginRootAt(baseDir: string, pluginId: string): string {
return joinRoot(baseDir, bundledDistPluginRoot(pluginId));
}
export function bundledDistPluginFileAt(
baseDir: string,
pluginId: string,
relativePath: string,
): string {
return joinRoot(baseDir, bundledDistPluginFile(pluginId, relativePath));
}
export function installedPluginRoot(baseDir: string, pluginId: string): string {
return bundledPluginRootAt(baseDir, pluginId);
}
export function repoInstallSpec(pluginId: string): string {
return `./${bundledPluginRoot(pluginId)}`;
}

View File

@@ -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");
}

View File

@@ -1,18 +0,0 @@
import { createServer, type RequestListener } from "node:http";
import type { AddressInfo } from "node:net";
export async function withServer(handler: RequestListener, fn: (baseUrl: string) => Promise<void>) {
const server = createServer(handler);
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve());
});
const address = server.address() as AddressInfo | null;
if (!address) {
throw new Error("missing server address");
}
try {
await fn(`http://127.0.0.1:${address.port}`);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
}

View File

@@ -1,8 +0,0 @@
export async function importFreshModule<TModule>(
from: string,
specifier: string,
): Promise<TModule> {
// Vitest keys module instances by the full URL string, including the query
// suffix. These tests rely on that behavior to emulate code-split chunks.
return (await import(/* @vite-ignore */ new URL(specifier, from).href)) as TModule;
}

View File

@@ -1,27 +0,0 @@
import { EventEmitter } from "node:events";
import type { IncomingMessage } from "node:http";
export function createMockIncomingRequest(chunks: string[]): IncomingMessage {
const req = new EventEmitter() as IncomingMessage & {
destroyed?: boolean;
destroy: (error?: Error) => IncomingMessage;
};
req.destroyed = false;
req.headers = {};
req.destroy = () => {
req.destroyed = true;
return req;
};
void Promise.resolve().then(() => {
for (const chunk of chunks) {
req.emit("data", Buffer.from(chunk, "utf-8"));
if (req.destroyed) {
return;
}
}
req.emit("end");
});
return req;
}

View File

@@ -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),
});
});
});

View File

@@ -1,59 +0,0 @@
type MockFactory<TModule extends object> =
| Partial<TModule>
| ((actual: TModule) => Partial<TModule>);
function resolveMockOverrides<TModule extends object>(
actual: TModule,
factory: MockFactory<TModule>,
): Partial<TModule> {
return typeof factory === "function" ? factory(actual) : factory;
}
function resolveDefaultBase(actual: object): Record<string, unknown> {
const defaultExport = (actual as { default?: unknown }).default;
if (defaultExport && typeof defaultExport === "object") {
return defaultExport as Record<string, unknown>;
}
return actual as Record<string, unknown>;
}
export async function mockNodeBuiltinModule<TModule extends object>(
loadActual: () => Promise<TModule>,
factory: MockFactory<TModule>,
options?: { mirrorToDefault?: boolean },
): Promise<TModule> {
const actual = await loadActual();
const overrides = resolveMockOverrides(actual, factory);
const mocked = {
...actual,
...overrides,
} as TModule & { default?: Record<string, unknown> };
if (!options?.mirrorToDefault) {
return mocked;
}
return {
...mocked,
default: {
...resolveDefaultBase(actual),
...overrides,
},
} as TModule;
}
export async function mockNodeChildProcessSpawnSync(
spawnSync: (...args: unknown[]) => unknown,
): Promise<typeof import("node:child_process")> {
return mockNodeBuiltinModule(() => import("node:child_process"), {
spawnSync: (...args: unknown[]) => spawnSync(...args),
} as Partial<typeof import("node:child_process")>);
}
export async function mockNodeChildProcessExecFile(
execFile: typeof import("node:child_process").execFile,
): Promise<typeof import("node:child_process")> {
return mockNodeBuiltinModule(() => import("node:child_process"), {
execFile,
} as Partial<typeof import("node:child_process")>);
}

View File

@@ -1,24 +0,0 @@
import { expect } from "vitest";
export function extractPairingCode(text: string): string {
const code = text.match(/Pairing code:\s*```[\r\n]+([A-Z2-9]{6,})/)?.[1];
expect(code).toBeDefined();
return code ?? "";
}
export function expectPairingReplyText(
text: string,
params: {
channel: string;
idLine: string;
code?: string;
},
): string {
const code = params.code ?? extractPairingCode(text);
expect(text).toContain("OpenClaw: access not configured.");
expect(text).toContain(params.idLine);
expect(text).toContain("Pairing code:");
expect(text).toContain(`\n\`\`\`\n${code}\n\`\`\`\n`);
expect(text).toContain(`pairing approve ${params.channel} ${code}`);
return code;
}

View File

@@ -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;
}

View File

@@ -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<Buffer> {
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 };
}

View File

@@ -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<string>;
nextCaseId: number;
};
const SHARED_HOME_ROOTS = new Map<string, SharedHomeRootState>();
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<string, string | undefined> {
const snapshot: Record<string, string | undefined> = {};
for (const key of keys) {
snapshot[key] = process.env[key];
}
return snapshot;
}
function restoreExtraEnv(snapshot: Record<string, string | undefined>) {
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<string> {
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<T>(
fn: (home: string) => Promise<T>,
opts: {
env?: Record<string, EnvValue>;
prefix?: string;
skipSessionCleanup?: boolean;
} = {},
): Promise<T> {
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
}
}
}