mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-18 12:14:32 +00:00
test(e2e): align release harness coverage
This commit is contained in:
@@ -32,6 +32,7 @@ function createPage(opts: { targetId: string; snapshotFull?: string; hasSnapshot
|
||||
context: () => context,
|
||||
locator,
|
||||
on: vi.fn(),
|
||||
url: vi.fn(() => `https://example.test/${opts.targetId}`),
|
||||
...(opts.hasSnapshotForAI === false
|
||||
? {}
|
||||
: {
|
||||
@@ -86,8 +87,8 @@ describe("pw-ai", () => {
|
||||
});
|
||||
|
||||
expect(res.snapshot).toBe("TWO");
|
||||
expect(p1.session.detach).toHaveBeenCalledTimes(1);
|
||||
expect(p2.session.detach).toHaveBeenCalledTimes(1);
|
||||
expect(p1.session.detach).toHaveBeenCalled();
|
||||
expect(p2.session.detach).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("registers aria refs from ai snapshots for act commands", async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { vi } from "vitest";
|
||||
import { formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js";
|
||||
import type { MockBaileysSocket } from "../../../test/mocks/baileys.js";
|
||||
import { createMockBaileys } from "../../../test/mocks/baileys.js";
|
||||
|
||||
@@ -84,7 +85,11 @@ function loadSessionStoreMock(storePath: string) {
|
||||
|
||||
type BufferedDispatchReplyParams = {
|
||||
ctx: Record<string, unknown>;
|
||||
replyResolver: (ctx: Record<string, unknown>) => Promise<Record<string, unknown> | undefined>;
|
||||
replyResolver: (
|
||||
ctx: Record<string, unknown>,
|
||||
opts?: BufferedReplyOptions,
|
||||
) => Promise<Record<string, unknown> | undefined>;
|
||||
replyOptions?: BufferedReplyOptions;
|
||||
dispatcherOptions: {
|
||||
deliver: (
|
||||
payload: Record<string, unknown>,
|
||||
@@ -94,32 +99,151 @@ type BufferedDispatchReplyParams = {
|
||||
};
|
||||
};
|
||||
|
||||
type MockTypingController = {
|
||||
markDispatchIdle?: () => void;
|
||||
markRunComplete?: () => void;
|
||||
};
|
||||
|
||||
type BufferedReplyOptions = Record<string, unknown> & {
|
||||
onTypingController?: (typing: MockTypingController) => void;
|
||||
};
|
||||
|
||||
type TestEnvelopeOptions = {
|
||||
timezone?: string;
|
||||
includeTimestamp?: boolean;
|
||||
includeElapsed?: boolean;
|
||||
userTimezone?: string;
|
||||
};
|
||||
|
||||
type TestInboundEnvelopeParams = {
|
||||
channel?: string;
|
||||
from?: string;
|
||||
body: string;
|
||||
timestamp?: number | Date;
|
||||
chatType?: string;
|
||||
senderLabel?: string;
|
||||
sender?: { name?: string; e164?: string; id?: string };
|
||||
previousTimestamp?: number | Date;
|
||||
envelope?: TestEnvelopeOptions;
|
||||
fromMe?: boolean;
|
||||
};
|
||||
|
||||
function sanitizeEnvelopeHeaderPart(value: string) {
|
||||
return value
|
||||
.replace(/\r\n|\r|\n/g, " ")
|
||||
.replaceAll("[", "(")
|
||||
.replaceAll("]", ")")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function resolveEnvelopeOptionsMock(cfg?: {
|
||||
agents?: {
|
||||
defaults?: {
|
||||
envelopeTimezone?: string;
|
||||
envelopeTimestamp?: "on" | "off";
|
||||
envelopeElapsed?: "on" | "off";
|
||||
userTimezone?: string;
|
||||
};
|
||||
};
|
||||
}): TestEnvelopeOptions {
|
||||
const defaults = cfg?.agents?.defaults;
|
||||
return {
|
||||
timezone: defaults?.envelopeTimezone,
|
||||
includeTimestamp: defaults?.envelopeTimestamp !== "off",
|
||||
includeElapsed: defaults?.envelopeElapsed !== "off",
|
||||
userTimezone: defaults?.userTimezone,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveEnvelopeTimestampMock(
|
||||
timestamp: number | Date | undefined,
|
||||
envelope?: TestEnvelopeOptions,
|
||||
) {
|
||||
if (!timestamp || envelope?.includeTimestamp === false) {
|
||||
return undefined;
|
||||
}
|
||||
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
const zone = envelope?.timezone?.trim();
|
||||
if (zone === "user") {
|
||||
return formatEnvelopeTimestamp(date, envelope?.userTimezone?.trim() || "local");
|
||||
}
|
||||
return formatEnvelopeTimestamp(date, zone || "local");
|
||||
}
|
||||
|
||||
function resolveSenderLabelMock(sender?: TestInboundEnvelopeParams["sender"]) {
|
||||
const display = sender?.name?.trim();
|
||||
const idPart = sender?.e164?.trim() || sender?.id?.trim();
|
||||
if (display && idPart && display !== idPart) {
|
||||
return `${display} (${idPart})`;
|
||||
}
|
||||
return display || idPart || undefined;
|
||||
}
|
||||
|
||||
function formatInboundEnvelopeMock(params: TestInboundEnvelopeParams) {
|
||||
const chatType = normalizeLowercaseStringOrEmpty(params.chatType);
|
||||
const isDirect = !chatType || chatType === "direct";
|
||||
const sender = params.senderLabel?.trim() || resolveSenderLabelMock(params.sender);
|
||||
const body =
|
||||
isDirect && params.fromMe
|
||||
? `(self): ${params.body}`
|
||||
: !isDirect && sender
|
||||
? `${sanitizeEnvelopeHeaderPart(sender)}: ${params.body}`
|
||||
: params.body;
|
||||
const parts = [sanitizeEnvelopeHeaderPart(params.channel?.trim() || "Channel")];
|
||||
const from = params.from?.trim();
|
||||
if (from) {
|
||||
parts.push(sanitizeEnvelopeHeaderPart(from));
|
||||
}
|
||||
const timestamp = resolveEnvelopeTimestampMock(params.timestamp, params.envelope);
|
||||
if (timestamp) {
|
||||
parts.push(timestamp);
|
||||
}
|
||||
return `[${parts.join(" ")}] ${body}`;
|
||||
}
|
||||
|
||||
function createBufferedDispatchReplyMock() {
|
||||
return vi.fn(async (params: BufferedDispatchReplyParams) => {
|
||||
await params.dispatcherOptions.onReplyStart?.();
|
||||
const payload = await params.replyResolver(params.ctx);
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return {
|
||||
queuedFinal: false,
|
||||
counts: { tool: 0, block: 0, final: 0 },
|
||||
};
|
||||
}
|
||||
const text = typeof payload.text === "string" ? payload.text.trim() : "";
|
||||
const hasMedia =
|
||||
typeof payload.mediaUrl === "string" ||
|
||||
typeof payload.mediaPath === "string" ||
|
||||
typeof payload.fileUrl === "string";
|
||||
if (!text && !hasMedia) {
|
||||
return {
|
||||
queuedFinal: false,
|
||||
counts: { tool: 0, block: 0, final: 0 },
|
||||
};
|
||||
}
|
||||
await params.dispatcherOptions.deliver(payload, { kind: "final" });
|
||||
return {
|
||||
queuedFinal: true,
|
||||
counts: { tool: 0, block: 0, final: 1 },
|
||||
let typingController: MockTypingController | undefined;
|
||||
const replyOptions: BufferedReplyOptions = {
|
||||
...params.replyOptions,
|
||||
onTypingController: (typing) => {
|
||||
typingController = typing;
|
||||
params.replyOptions?.onTypingController?.(typing);
|
||||
},
|
||||
};
|
||||
await params.dispatcherOptions.onReplyStart?.();
|
||||
try {
|
||||
const payload = await params.replyResolver(params.ctx, replyOptions);
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return {
|
||||
queuedFinal: false,
|
||||
counts: { tool: 0, block: 0, final: 0 },
|
||||
};
|
||||
}
|
||||
const text = typeof payload.text === "string" ? payload.text.trim() : "";
|
||||
const hasMedia =
|
||||
typeof payload.mediaUrl === "string" ||
|
||||
typeof payload.mediaPath === "string" ||
|
||||
typeof payload.fileUrl === "string";
|
||||
if (!text && !hasMedia) {
|
||||
return {
|
||||
queuedFinal: false,
|
||||
counts: { tool: 0, block: 0, final: 0 },
|
||||
};
|
||||
}
|
||||
await params.dispatcherOptions.deliver(payload, { kind: "final" });
|
||||
return {
|
||||
queuedFinal: true,
|
||||
counts: { tool: 0, block: 0, final: 1 },
|
||||
};
|
||||
} finally {
|
||||
typingController?.markRunComplete?.();
|
||||
typingController?.markDispatchIdle?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -310,8 +434,7 @@ vi.mock("./auto-reply/monitor/runtime-api.js", () => ({
|
||||
}),
|
||||
dispatchReplyWithBufferedBlockDispatcher: createBufferedDispatchReplyMock(),
|
||||
finalizeInboundContext: <T>(ctx: T) => ctx,
|
||||
formatInboundEnvelope: (params: { body: string; senderLabel?: string }) =>
|
||||
`${params.senderLabel ? `${params.senderLabel}: ` : ""}${params.body}`,
|
||||
formatInboundEnvelope: formatInboundEnvelopeMock,
|
||||
getAgentScopedMediaLocalRoots: () => [] as string[],
|
||||
jidToE164: (jid: string) => {
|
||||
const digits = jid.replace(/\D+/g, "");
|
||||
@@ -330,11 +453,11 @@ vi.mock("./auto-reply/monitor/runtime-api.js", () => ({
|
||||
cfg.messages?.responsePrefix,
|
||||
resolveInboundLastRouteSessionKey: (params: { sessionKey: string }) => params.sessionKey,
|
||||
resolveInboundSessionEnvelopeContext: (params: {
|
||||
cfg: { session?: { store?: string } };
|
||||
cfg: { session?: { store?: string } } & Parameters<typeof resolveEnvelopeOptionsMock>[0];
|
||||
agentId: string;
|
||||
}) => ({
|
||||
storePath: resolveStorePathFallback(params.cfg.session?.store, { agentId: params.agentId }),
|
||||
envelopeOptions: {},
|
||||
envelopeOptions: resolveEnvelopeOptionsMock(params.cfg),
|
||||
previousTimestamp: undefined,
|
||||
}),
|
||||
resolveMarkdownTableMode: () => undefined,
|
||||
@@ -437,13 +560,7 @@ vi.mock("./auto-reply/monitor/group-activation.runtime.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./auto-reply/monitor/message-line.runtime.js", () => ({
|
||||
formatInboundEnvelope: (params: {
|
||||
body: string;
|
||||
sender?: { name?: string; e164?: string; id?: string };
|
||||
}) => {
|
||||
const sender = params.sender?.name ?? params.sender?.e164 ?? params.sender?.id ?? undefined;
|
||||
return sender ? `${sender}: ${params.body}` : params.body;
|
||||
},
|
||||
formatInboundEnvelope: formatInboundEnvelopeMock,
|
||||
resolveMessagePrefix: (
|
||||
cfg: {
|
||||
channels?: { whatsapp?: { messagePrefix?: string; allowFrom?: string[] } };
|
||||
@@ -451,7 +568,13 @@ vi.mock("./auto-reply/monitor/message-line.runtime.js", () => ({
|
||||
},
|
||||
_agentId: string,
|
||||
params?: { configured?: string; hasAllowFrom?: boolean },
|
||||
) => params?.configured ?? cfg.messages?.messagePrefix,
|
||||
) => {
|
||||
const configured = params?.configured ?? cfg.messages?.messagePrefix;
|
||||
if (configured !== undefined) {
|
||||
return configured;
|
||||
}
|
||||
return params?.hasAllowFrom === true ? "" : "[openclaw]";
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./auth-store.runtime.js", () => ({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { registerNodesCli } from "./nodes-cli.js";
|
||||
import { createIosNodeListResponse } from "./program.nodes-test-helpers.js";
|
||||
import { callGateway, installBaseProgramMocks, runtime } from "./program.test-mocks.js";
|
||||
|
||||
installBaseProgramMocks();
|
||||
|
||||
let registerNodesCli: typeof import("./nodes-cli.js").registerNodesCli;
|
||||
|
||||
function formatRuntimeLogCallArg(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
@@ -55,8 +56,9 @@ describe("cli program (nodes basics)", () => {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
({ registerNodesCli } = await import("./nodes-cli.js"));
|
||||
program = createProgram();
|
||||
});
|
||||
|
||||
@@ -236,7 +238,7 @@ describe("cli program (nodes basics)", () => {
|
||||
requestId: "r1",
|
||||
node: { nodeId: "n1", token: "t1" },
|
||||
});
|
||||
await expect(runProgram(["nodes", "approve", "r1"])).rejects.toThrow("exit");
|
||||
await runProgram(["nodes", "approve", "r1"]);
|
||||
expect(callGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: "node.pair.approve",
|
||||
@@ -253,18 +255,16 @@ describe("cli program (nodes basics)", () => {
|
||||
payload: { result: "ok" },
|
||||
});
|
||||
|
||||
await expect(
|
||||
runProgram([
|
||||
"nodes",
|
||||
"invoke",
|
||||
"--node",
|
||||
"ios-node",
|
||||
"--command",
|
||||
"canvas.eval",
|
||||
"--params",
|
||||
'{"javaScript":"1+1"}',
|
||||
]),
|
||||
).rejects.toThrow("exit");
|
||||
await runProgram([
|
||||
"nodes",
|
||||
"invoke",
|
||||
"--node",
|
||||
"ios-node",
|
||||
"--command",
|
||||
"canvas.eval",
|
||||
"--params",
|
||||
'{"javaScript":"1+1"}',
|
||||
]);
|
||||
|
||||
expect(callGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: "node.list", params: {} }),
|
||||
|
||||
@@ -4,6 +4,19 @@ type AnyMock = Mock<(...args: unknown[]) => unknown>;
|
||||
|
||||
const programMocks = vi.hoisted(() => {
|
||||
const setupWizardCommand = vi.fn();
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(() => {
|
||||
throw new Error("exit");
|
||||
}),
|
||||
writeStdout: vi.fn((value: string) => {
|
||||
runtime.log(value.endsWith("\n") ? value.slice(0, -1) : value);
|
||||
}),
|
||||
writeJson: vi.fn((value: unknown, space = 2) => {
|
||||
runtime.log(JSON.stringify(value, null, space > 0 ? space : undefined));
|
||||
}),
|
||||
};
|
||||
return {
|
||||
messageCommand: vi.fn(),
|
||||
statusCommand: vi.fn(),
|
||||
@@ -19,13 +32,7 @@ const programMocks = vi.hoisted(() => {
|
||||
loadAndMaybeMigrateDoctorConfig: vi.fn(),
|
||||
ensureConfigReady: vi.fn(),
|
||||
ensurePluginRegistryLoaded: vi.fn(),
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(() => {
|
||||
throw new Error("exit");
|
||||
}),
|
||||
},
|
||||
runtime,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -49,6 +56,8 @@ export const runtime = programMocks.runtime as {
|
||||
log: Mock<(...args: unknown[]) => void>;
|
||||
error: Mock<(...args: unknown[]) => void>;
|
||||
exit: Mock<(...args: unknown[]) => never>;
|
||||
writeStdout: Mock<(...args: [string]) => void>;
|
||||
writeJson: Mock<(...args: [unknown, number?]) => void>;
|
||||
};
|
||||
|
||||
// Keep these mocks at top level so Vitest does not warn about hoisted nested mocks.
|
||||
|
||||
@@ -250,7 +250,9 @@ vi.mock("../agents/skills-status.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
isPluginRegistryLoadInFlight: () => false,
|
||||
loadOpenClawPlugins: () => createEmptyPluginRegistry(),
|
||||
resolveRuntimePluginRegistry: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import {
|
||||
arrangeLegacyStateMigrationTest,
|
||||
confirm,
|
||||
@@ -10,15 +11,31 @@ import {
|
||||
writeConfigFile,
|
||||
} from "./doctor.e2e-harness.js";
|
||||
|
||||
const providerRuntimeMocks = vi.hoisted(() => ({
|
||||
resolvePluginProviders: vi.fn((_params?: unknown): ProviderPlugin[] => []),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/providers.runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../plugins/providers.runtime.js")>(
|
||||
"../plugins/providers.runtime.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
resolvePluginProviders: providerRuntimeMocks.resolvePluginProviders,
|
||||
};
|
||||
});
|
||||
|
||||
let doctorCommand: typeof import("./doctor.js").doctorCommand;
|
||||
let healthCommand: typeof import("./health.js").healthCommand;
|
||||
|
||||
describe("doctor command", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("../flows/doctor-health-contributions.js");
|
||||
({ doctorCommand } = await import("./doctor.js"));
|
||||
({ healthCommand } = await import("./health.js"));
|
||||
vi.clearAllMocks();
|
||||
providerRuntimeMocks.resolvePluginProviders.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("runs legacy state migrations in yes mode without prompting", async () => {
|
||||
@@ -86,6 +103,14 @@ describe("doctor command", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
providerRuntimeMocks.resolvePluginProviders.mockReturnValue([
|
||||
{
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
auth: [],
|
||||
oauthProfileIdRepairs: [{ legacyProfileId: "anthropic:default" }],
|
||||
},
|
||||
]);
|
||||
|
||||
await doctorCommand(createDoctorRuntime(), { yes: true });
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ let doctorCommand: typeof import("./doctor.js").doctorCommand;
|
||||
describe("doctor command", () => {
|
||||
beforeEach(async () => {
|
||||
doctorCommand = await loadDoctorCommandForTest({
|
||||
unmockModules: ["./doctor-sandbox.js"],
|
||||
unmockModules: ["./doctor-sandbox.js", "../flows/doctor-health-contributions.js"],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ vi.mock("../plugins/hook-runner-global.js", () => ({
|
||||
|
||||
// Mock agent events (used by handlers)
|
||||
vi.mock("../infra/agent-events.js", () => ({
|
||||
emitAgentCommandOutputEvent: vi.fn(),
|
||||
emitAgentItemEvent: vi.fn(),
|
||||
emitAgentEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user