mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
refactor: move remaining SDK test helper files
This commit is contained in:
@@ -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");
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user