Files
openclaw/src/auto-reply/reply.test-harness.ts
2026-04-06 19:23:17 +01:00

247 lines
7.9 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, vi, type Mock } from "vitest";
import { withFastReplyConfig } from "./reply/get-reply-fast-path.js";
export type ReplyRuntimeMocks = {
runEmbeddedPiAgent: Mock;
loadModelCatalog: Mock;
webAuthExists: Mock;
getWebAuthAgeMs: Mock;
readWebSelfId: Mock;
};
const replyRuntimeMockState = vi.hoisted(() => ({
mocks: {
runEmbeddedPiAgent: vi.fn(),
loadModelCatalog: vi.fn(),
webAuthExists: vi.fn().mockResolvedValue(true),
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
} as ReplyRuntimeMocks,
}));
vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
runEmbeddedPiAgent: (...args: unknown[]) =>
replyRuntimeMockState.mocks.runEmbeddedPiAgent(...args),
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
}));
vi.mock("../agents/model-catalog.runtime.js", () => ({
loadModelCatalog: (...args: unknown[]) => replyRuntimeMockState.mocks.loadModelCatalog(...args),
}));
vi.mock("../agents/auth-profiles/session-override.js", () => ({
clearSessionAuthProfileOverride: vi.fn(),
resolveSessionAuthProfileOverride: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../commands-registry.runtime.js", () => ({
listChatCommands: () => [],
}));
vi.mock("../skill-commands.runtime.js", () => ({
listSkillCommandsForWorkspace: () => [],
}));
vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({
webAuthExists: (...args: unknown[]) => replyRuntimeMockState.mocks.webAuthExists(...args),
getWebAuthAgeMs: (...args: unknown[]) => replyRuntimeMockState.mocks.getWebAuthAgeMs(...args),
readWebSelfId: (...args: unknown[]) => replyRuntimeMockState.mocks.readWebSelfId(...args),
}));
vi.mock("../agents/pi-embedded.runtime.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
resolveActiveEmbeddedRunSessionId: vi.fn().mockReturnValue(undefined),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
waitForEmbeddedPiRunEnd: vi.fn(async () => undefined),
}));
vi.mock("./reply/agent-runner.runtime.js", () => ({
runReplyAgent: async (params: {
commandBody: string;
followupRun: {
prompt: string;
run: {
agentDir: string;
agentId: string;
config: unknown;
execOverrides?: unknown;
inputProvenance?: unknown;
messageProvider?: string;
model: string;
ownerNumbers?: string[];
provider: string;
reasoningLevel?: unknown;
senderIsOwner?: boolean;
sessionFile: string;
sessionId: string;
sessionKey: string;
skillsSnapshot?: unknown;
thinkLevel?: unknown;
timeoutMs?: number;
verboseLevel?: unknown;
workspaceDir: string;
bashElevated?: unknown;
};
};
}) => {
const result = await replyRuntimeMockState.mocks.runEmbeddedPiAgent({
prompt: params.followupRun.prompt || params.commandBody,
agentDir: params.followupRun.run.agentDir,
agentId: params.followupRun.run.agentId,
config: params.followupRun.run.config,
execOverrides: params.followupRun.run.execOverrides,
inputProvenance: params.followupRun.run.inputProvenance,
messageProvider: params.followupRun.run.messageProvider,
model: params.followupRun.run.model,
ownerNumbers: params.followupRun.run.ownerNumbers,
provider: params.followupRun.run.provider,
reasoningLevel: params.followupRun.run.reasoningLevel,
senderIsOwner: params.followupRun.run.senderIsOwner,
sessionFile: params.followupRun.run.sessionFile,
sessionId: params.followupRun.run.sessionId,
sessionKey: params.followupRun.run.sessionKey,
skillsSnapshot: params.followupRun.run.skillsSnapshot,
thinkLevel: params.followupRun.run.thinkLevel,
timeoutMs: params.followupRun.run.timeoutMs,
verboseLevel: params.followupRun.run.verboseLevel,
workspaceDir: params.followupRun.run.workspaceDir,
bashElevated: params.followupRun.run.bashElevated,
});
return result?.payloads?.[0];
},
}));
type HomeEnvSnapshot = {
HOME: string | undefined;
USERPROFILE: string | undefined;
HOMEDRIVE: string | undefined;
HOMEPATH: string | undefined;
OPENCLAW_STATE_DIR: string | undefined;
OPENCLAW_AGENT_DIR: string | undefined;
PI_CODING_AGENT_DIR: string | undefined;
};
function snapshotHomeEnv(): HomeEnvSnapshot {
return {
HOME: process.env.HOME,
USERPROFILE: process.env.USERPROFILE,
HOMEDRIVE: process.env.HOMEDRIVE,
HOMEPATH: process.env.HOMEPATH,
OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR,
OPENCLAW_AGENT_DIR: process.env.OPENCLAW_AGENT_DIR,
PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR,
};
}
function restoreHomeEnv(snapshot: HomeEnvSnapshot) {
for (const [key, value] of Object.entries(snapshot)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
export function createTempHomeHarness(options: { prefix: string; beforeEachCase?: () => void }) {
let fixtureRoot = "";
let caseId = 0;
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), options.prefix));
});
afterAll(async () => {
if (!fixtureRoot) {
return;
}
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const home = path.join(fixtureRoot, `case-${++caseId}`);
await fs.mkdir(path.join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true });
const envSnapshot = snapshotHomeEnv();
process.env.HOME = home;
process.env.USERPROFILE = home;
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
process.env.OPENCLAW_AGENT_DIR = path.join(home, ".openclaw", "agent");
process.env.PI_CODING_AGENT_DIR = path.join(home, ".openclaw", "agent");
if (process.platform === "win32") {
const match = home.match(/^([A-Za-z]:)(.*)$/);
if (match) {
process.env.HOMEDRIVE = match[1];
process.env.HOMEPATH = match[2] || "\\";
}
}
try {
options.beforeEachCase?.();
return await fn(home);
} finally {
restoreHomeEnv(envSnapshot);
}
}
return { withTempHome };
}
export function makeReplyConfig(home: string) {
return withFastReplyConfig({
agents: {
defaults: {
model: "anthropic/claude-opus-4-6",
workspace: path.join(home, "openclaw"),
},
},
channels: {
whatsapp: {
allowFrom: ["*"],
},
},
session: { store: path.join(home, "sessions.json") },
});
}
export function createReplyRuntimeMocks(): ReplyRuntimeMocks {
return {
runEmbeddedPiAgent: vi.fn(),
loadModelCatalog: vi.fn(),
webAuthExists: vi.fn().mockResolvedValue(true),
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }),
};
}
export function installReplyRuntimeMocks(mocks: ReplyRuntimeMocks) {
replyRuntimeMockState.mocks = mocks;
}
export function resetReplyRuntimeMocks(mocks: ReplyRuntimeMocks) {
mocks.runEmbeddedPiAgent.mockClear();
mocks.loadModelCatalog.mockClear();
mocks.loadModelCatalog.mockResolvedValue([
{ id: "claude-opus-4-6", name: "Opus 4.5", provider: "anthropic" },
]);
}
export function makeEmbeddedTextResult(text: string) {
return {
payloads: [{ text }],
meta: {
durationMs: 5,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
};
}