chore: Fix types in tests 23/N.

This commit is contained in:
cpojer
2026-02-17 12:23:01 +09:00
parent 8d6e345338
commit cf6cdc74d0
13 changed files with 166 additions and 116 deletions

View File

@@ -1,15 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { VoiceCallConfigSchema, type VoiceCallConfig } from "./config.js";
import type { CallManager } from "./manager.js";
import type { VoiceCallProvider } from "./providers/base.js";
import type { CallRecord } from "./types.js";
import { VoiceCallConfigSchema, type VoiceCallConfig } from "./config.js";
import { VoiceCallWebhookServer } from "./webhook.js";
const provider: VoiceCallProvider = {
name: "mock",
verifyWebhook: () => ({ ok: true }),
parseWebhookEvent: () => ({ events: [] }),
initiateCall: async () => ({ providerCallId: "provider-call" }),
initiateCall: async () => ({ providerCallId: "provider-call", status: "initiated" }),
hangupCall: async () => {},
playTts: async () => {},
startListening: async () => {},

View File

@@ -1,9 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js";
import {
handleToolExecutionEnd,
handleToolExecutionStart,
} from "./pi-embedded-subscribe.handlers.tools.js";
import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js";
// Minimal mock context factory. Only the fields needed for the media emission path.
function createMockContext(overrides?: {

View File

@@ -65,6 +65,8 @@ const { readAllowFromStoreMock, upsertPairingRequestMock } = vi.hoisted(() => ({
let handleLineWebhookEvents: typeof import("./bot-handlers.js").handleLineWebhookEvents;
const createRuntime = () => ({ log: vi.fn(), error: vi.fn(), exit: vi.fn() });
vi.mock("../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: readAllowFromStoreMock,
upsertChannelPairingRequest: upsertPairingRequestMock,
@@ -105,7 +107,7 @@ describe("handleLineWebhookEvents", () => {
tokenSource: "config",
config: { groupPolicy: "disabled" },
},
runtime: { error: vi.fn() },
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
});
@@ -137,7 +139,7 @@ describe("handleLineWebhookEvents", () => {
tokenSource: "config",
config: { groupPolicy: "allowlist" },
},
runtime: { error: vi.fn() },
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
});
@@ -171,7 +173,7 @@ describe("handleLineWebhookEvents", () => {
tokenSource: "config",
config: { groupPolicy: "allowlist", groupAllowFrom: ["user-3"] },
},
runtime: { error: vi.fn() },
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
});
@@ -203,7 +205,7 @@ describe("handleLineWebhookEvents", () => {
tokenSource: "config",
config: { groupPolicy: "open", groups: { "*": { enabled: false } } },
},
runtime: { error: vi.fn() },
runtime: createRuntime(),
mediaMaxBytes: 1,
processMessage,
});

View File

@@ -8,24 +8,26 @@ const sign = (body: string, secret: string) =>
function createRes() {
const headers: Record<string, string> = {};
const res = {
const resObj = {
statusCode: 0,
headersSent: false,
setHeader: (k: string, v: string) => {
headers[k.toLowerCase()] = v;
},
end: vi.fn((data?: unknown) => {
res.headersSent = true;
resObj.headersSent = true;
// Keep payload available for assertions
(res as { body?: unknown }).body = data;
resObj.body = data;
}),
} as unknown as ServerResponse & { body?: unknown };
body: undefined as unknown,
};
const res = resObj as unknown as ServerResponse & { body?: unknown };
return { res, headers };
}
function createPostWebhookTestHarness(rawBody: string, secret = "secret") {
const bot = { handleWebhook: vi.fn(async () => {}) };
const runtime = { error: vi.fn() };
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
const handler = createLineNodeWebhookHandler({
channelSecret: secret,
bot,
@@ -38,7 +40,7 @@ function createPostWebhookTestHarness(rawBody: string, secret = "secret") {
describe("createLineNodeWebhookHandler", () => {
it("returns 200 for GET", async () => {
const bot = { handleWebhook: vi.fn(async () => {}) };
const runtime = { error: vi.fn() };
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
const handler = createLineNodeWebhookHandler({
channelSecret: "secret",
bot,

View File

@@ -1,8 +1,9 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolveEntriesWithActiveFallback, resolveModelEntries } from "./resolve.js";
import type { MediaUnderstandingCapability } from "./types.js";
const providerRegistry = new Map([
const providerRegistry = new Map<string, { capabilities: MediaUnderstandingCapability[] }>([
["openai", { capabilities: ["image"] }],
["groq", { capabilities: ["audio"] }],
]);

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { PassThrough } from "node:stream";
import JSZip from "jszip";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createPinnedLookup } from "../infra/net/ssrf.js";
import { captureEnv } from "../test-utils/env.js";
import { saveMediaSource, setMediaStoreNetworkDepsForTest } from "./store.js";
@@ -24,8 +25,10 @@ describe("media store redirects", () => {
setMediaStoreNetworkDepsForTest({
httpRequest: (...args) => mockRequest(...args),
httpsRequest: (...args) => mockRequest(...args),
resolvePinnedHostname: async () => ({
lookup: async () => [{ address: "93.184.216.34", family: 4 }],
resolvePinnedHostname: async (hostname) => ({
hostname,
addresses: ["93.184.216.34"],
lookup: createPinnedLookup({ hostname, addresses: ["93.184.216.34"] }),
}),
});
});
@@ -41,7 +44,10 @@ describe("media store redirects", () => {
let call = 0;
mockRequest.mockImplementation((_url, _opts, cb) => {
call += 1;
const res = new PassThrough();
const res = Object.assign(new PassThrough(), {
statusCode: 0,
headers: {} as Record<string, string>,
});
const req = {
on: (event: string, handler: (...args: unknown[]) => void) => {
if (event === "error") {
@@ -83,7 +89,10 @@ describe("media store redirects", () => {
it("sniffs xlsx from zip content when headers and url extension are missing", async () => {
mockRequest.mockImplementationOnce((_url, _opts, cb) => {
const res = new PassThrough();
const res = Object.assign(new PassThrough(), {
statusCode: 0,
headers: {} as Record<string, string>,
});
const req = {
on: (event: string, handler: (...args: unknown[]) => void) => {
if (event === "error") {

View File

@@ -104,7 +104,17 @@ describe("after_tool_call hook wiring", () => {
expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1);
expect(hookMocks.runner.runBeforeToolCall).not.toHaveBeenCalled();
const [event, context] = hookMocks.runner.runAfterToolCall.mock.calls[0];
const firstCall = (hookMocks.runner.runAfterToolCall as ReturnType<typeof vi.fn>).mock.calls[0];
expect(firstCall).toBeDefined();
const event = firstCall?.[0] as
| { toolName?: string; params?: unknown; error?: unknown; durationMs?: unknown }
| undefined;
const context = firstCall?.[1] as { toolName?: string } | undefined;
expect(event).toBeDefined();
expect(context).toBeDefined();
if (!event || !context) {
throw new Error("missing hook call payload");
}
expect(event.toolName).toBe("read");
expect(event.params).toEqual({ path: "/tmp/file.txt" });
expect(event.error).toBeUndefined();
@@ -143,7 +153,13 @@ describe("after_tool_call hook wiring", () => {
expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1);
const [event] = hookMocks.runner.runAfterToolCall.mock.calls[0];
const firstCall = (hookMocks.runner.runAfterToolCall as ReturnType<typeof vi.fn>).mock.calls[0];
expect(firstCall).toBeDefined();
const event = firstCall?.[0] as { error?: unknown } | undefined;
expect(event).toBeDefined();
if (!event) {
throw new Error("missing hook call payload");
}
expect(event.error).toBeDefined();
});

View File

@@ -38,18 +38,26 @@ function createMonitorRuntime() {
}
function setSignalAutoStartConfig(overrides: Record<string, unknown> = {}) {
setSignalToolResultTestConfig({
...config,
setSignalToolResultTestConfig(createSignalConfig(overrides));
}
function createSignalConfig(overrides: Record<string, unknown> = {}): Record<string, unknown> {
const base = config as OpenClawConfig;
const channels = (base.channels ?? {}) as Record<string, unknown>;
const signal = (channels.signal ?? {}) as Record<string, unknown>;
return {
...base,
channels: {
...config.channels,
...channels,
signal: {
...signal,
autoStart: true,
dmPolicy: "open",
allowFrom: ["*"],
...overrides,
},
},
});
};
}
function createAutoAbortController() {
@@ -193,18 +201,9 @@ describe("monitorSignalProvider tool results", () => {
});
it("replies with pairing code when dmPolicy is pairing and no allowFrom is set", async () => {
setSignalToolResultTestConfig({
...config,
channels: {
...config.channels,
signal: {
...config.channels?.signal,
autoStart: false,
dmPolicy: "pairing",
allowFrom: [],
},
},
});
setSignalToolResultTestConfig(
createSignalConfig({ autoStart: false, dmPolicy: "pairing", allowFrom: [] }),
);
await receiveSignalPayloads({
payloads: [
{
@@ -277,19 +276,14 @@ describe("monitorSignalProvider tool results", () => {
});
it("enqueues system events for reaction notifications", async () => {
setSignalToolResultTestConfig({
...config,
channels: {
...config.channels,
signal: {
...config.channels?.signal,
autoStart: false,
dmPolicy: "open",
allowFrom: ["*"],
reactionNotifications: "all",
},
},
});
setSignalToolResultTestConfig(
createSignalConfig({
autoStart: false,
dmPolicy: "open",
allowFrom: ["*"],
reactionNotifications: "all",
}),
);
await receiveSignalPayloads({
payloads: [
{
@@ -312,20 +306,15 @@ describe("monitorSignalProvider tool results", () => {
});
it("notifies on own reactions when target includes uuid + phone", async () => {
setSignalToolResultTestConfig({
...config,
channels: {
...config.channels,
signal: {
...config.channels?.signal,
autoStart: false,
dmPolicy: "open",
allowFrom: ["*"],
account: "+15550002222",
reactionNotifications: "own",
},
},
});
setSignalToolResultTestConfig(
createSignalConfig({
autoStart: false,
dmPolicy: "open",
allowFrom: ["*"],
account: "+15550002222",
reactionNotifications: "own",
}),
);
await receiveSignalPayloads({
payloads: [
{
@@ -376,18 +365,9 @@ describe("monitorSignalProvider tool results", () => {
});
it("does not resend pairing code when a request is already pending", async () => {
setSignalToolResultTestConfig({
...config,
channels: {
...config.channels,
signal: {
...config.channels?.signal,
autoStart: false,
dmPolicy: "pairing",
allowFrom: [],
},
},
});
setSignalToolResultTestConfig(
createSignalConfig({ autoStart: false, dmPolicy: "pairing", allowFrom: [] }),
);
upsertPairingRequestMock
.mockResolvedValueOnce({ code: "PAIRCODE", created: true })
.mockResolvedValueOnce({ code: "PAIRCODE", created: false });

View File

@@ -6,12 +6,18 @@ import type {
} from "../channels/plugins/types.js";
import type { PluginRegistry } from "../plugins/registry.js";
export const createTestRegistry = (channels: PluginRegistry["channels"] = []): PluginRegistry => ({
type TestChannelRegistration = {
pluginId: string;
plugin: unknown;
source: string;
};
export const createTestRegistry = (channels: TestChannelRegistration[] = []): PluginRegistry => ({
plugins: [],
tools: [],
hooks: [],
typedHooks: [],
channels,
channels: channels as unknown as PluginRegistry["channels"],
providers: [],
gatewayHandlers: {},
httpHandlers: [],

View File

@@ -1,7 +1,8 @@
import { completeSimple } from "@mariozechner/pi-ai";
import { completeSimple, type AssistantMessage } from "@mariozechner/pi-ai";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { getApiKeyForModel } from "../agents/model-auth.js";
import { resolveModel } from "../agents/pi-embedded-runner/model.js";
import type { OpenClawConfig } from "../config/config.js";
import { withEnv } from "../test-utils/env.js";
import * as tts from "./tts.js";
@@ -54,12 +55,36 @@ const {
resolveEdgeOutputFormat,
} = _test;
const mockAssistantMessage = (content: AssistantMessage["content"]): AssistantMessage => ({
role: "assistant",
content,
api: "openai-completions",
provider: "openai",
model: "gpt-4o-mini",
usage: {
input: 1,
output: 1,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 2,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop",
timestamp: Date.now(),
});
describe("tts", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(completeSimple).mockResolvedValue({
content: [{ type: "text", text: "Summary" }],
});
vi.mocked(completeSimple).mockResolvedValue(
mockAssistantMessage([{ type: "text", text: "Summary" }]),
);
});
describe("isValidVoiceId", () => {
@@ -165,7 +190,7 @@ describe("tts", () => {
});
describe("resolveEdgeOutputFormat", () => {
const baseCfg = {
const baseCfg: OpenClawConfig = {
agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } },
messages: { tts: {} },
};
@@ -223,7 +248,7 @@ describe("tts", () => {
});
describe("summarizeText", () => {
const baseCfg = {
const baseCfg: OpenClawConfig = {
agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } },
messages: { tts: {} },
};
@@ -231,9 +256,9 @@ describe("tts", () => {
it("summarizes text and returns result with metrics", async () => {
const mockSummary = "This is a summarized version of the text.";
vi.mocked(completeSimple).mockResolvedValue({
content: [{ type: "text", text: mockSummary }],
});
vi.mocked(completeSimple).mockResolvedValue(
mockAssistantMessage([{ type: "text", text: mockSummary }]),
);
const longText = "A".repeat(2000);
const result = await summarizeText({
@@ -268,7 +293,7 @@ describe("tts", () => {
});
it("uses summaryModel override when configured", async () => {
const cfg = {
const cfg: OpenClawConfig = {
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } },
messages: { tts: { summaryModel: "openai/gpt-4.1-mini" } },
};
@@ -330,9 +355,7 @@ describe("tts", () => {
});
it("throws error when no summary is returned", async () => {
vi.mocked(completeSimple).mockResolvedValue({
content: [],
});
vi.mocked(completeSimple).mockResolvedValue(mockAssistantMessage([]));
await expect(
summarizeText({
@@ -346,9 +369,9 @@ describe("tts", () => {
});
it("throws error when summary content is empty", async () => {
vi.mocked(completeSimple).mockResolvedValue({
content: [{ type: "text", text: " " }],
});
vi.mocked(completeSimple).mockResolvedValue(
mockAssistantMessage([{ type: "text", text: " " }]),
);
await expect(
summarizeText({
@@ -363,7 +386,7 @@ describe("tts", () => {
});
describe("getTtsProvider", () => {
const baseCfg = {
const baseCfg: OpenClawConfig = {
agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } },
messages: { tts: {} },
};
@@ -415,7 +438,7 @@ describe("tts", () => {
});
describe("maybeApplyTtsToPayload", () => {
const baseCfg = {
const baseCfg: OpenClawConfig = {
agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } },
messages: {
tts: {
@@ -445,11 +468,11 @@ describe("tts", () => {
}
};
const taggedCfg = {
const taggedCfg: OpenClawConfig = {
...baseCfg,
messages: {
...baseCfg.messages,
tts: { ...baseCfg.messages.tts, auto: "tagged" },
...baseCfg.messages!,
tts: { ...baseCfg.messages!.tts, auto: "tagged" },
},
};

View File

@@ -37,6 +37,9 @@ vi.mock("./qr-image.js", () => ({
const { startWebLoginWithQr, waitForWebLogin } = await import("./login-qr.js");
const { createWaSocket, waitForWaConnection, logoutWeb } = await import("./session.js");
const createWaSocketMock = vi.mocked(createWaSocket);
const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
const logoutWebMock = vi.mocked(logoutWeb);
describe("login-qr", () => {
beforeEach(() => {
@@ -44,7 +47,7 @@ describe("login-qr", () => {
});
it("restarts login once on status 515 and completes", async () => {
waitForWaConnection
waitForWaConnectionMock
.mockRejectedValueOnce({ output: { statusCode: 515 } })
.mockResolvedValueOnce(undefined);
@@ -54,7 +57,7 @@ describe("login-qr", () => {
const result = await waitForWebLogin({ timeoutMs: 5000 });
expect(result.connected).toBe(true);
expect(createWaSocket).toHaveBeenCalledTimes(2);
expect(logoutWeb).not.toHaveBeenCalled();
expect(createWaSocketMock).toHaveBeenCalledTimes(2);
expect(logoutWebMock).not.toHaveBeenCalled();
});
});

View File

@@ -44,6 +44,9 @@ vi.mock("./session.js", () => {
});
const { createWaSocket, waitForWaConnection, formatError } = await import("./session.js");
const createWaSocketMock = vi.mocked(createWaSocket);
const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
const formatErrorMock = vi.mocked(formatError);
const { loginWeb } = await import("./login.js");
describe("loginWeb coverage", () => {
@@ -57,27 +60,29 @@ describe("loginWeb coverage", () => {
});
it("restarts once when WhatsApp requests code 515", async () => {
waitForWaConnection
waitForWaConnectionMock
.mockRejectedValueOnce({ output: { statusCode: 515 } })
.mockResolvedValueOnce(undefined);
const runtime = { log: vi.fn(), error: vi.fn() } as never;
await loginWeb(false, waitForWaConnection as never, runtime);
await loginWeb(false, waitForWaConnectionMock as never, runtime);
expect(createWaSocket).toHaveBeenCalledTimes(2);
const firstSock = await createWaSocket.mock.results[0].value;
expect(createWaSocketMock).toHaveBeenCalledTimes(2);
const firstSock = await createWaSocketMock.mock.results[0]?.value;
expect(firstSock.ws.close).toHaveBeenCalled();
vi.runAllTimers();
const secondSock = await createWaSocket.mock.results[1].value;
const secondSock = await createWaSocketMock.mock.results[1]?.value;
expect(secondSock.ws.close).toHaveBeenCalled();
});
it("clears creds and throws when logged out", async () => {
waitForWaConnection.mockRejectedValueOnce({
waitForWaConnectionMock.mockRejectedValueOnce({
output: { statusCode: DisconnectReason.loggedOut },
});
await expect(loginWeb(false, waitForWaConnection as never)).rejects.toThrow(/cache cleared/i);
await expect(loginWeb(false, waitForWaConnectionMock as never)).rejects.toThrow(
/cache cleared/i,
);
expect(rmMock).toHaveBeenCalledWith(authDir, {
recursive: true,
force: true,
@@ -85,10 +90,10 @@ describe("loginWeb coverage", () => {
});
it("formats and rethrows generic errors", async () => {
waitForWaConnection.mockRejectedValueOnce(new Error("boom"));
await expect(loginWeb(false, waitForWaConnection as never)).rejects.toThrow(
waitForWaConnectionMock.mockRejectedValueOnce(new Error("boom"));
await expect(loginWeb(false, waitForWaConnectionMock as never)).rejects.toThrow(
"formatted:Error: boom",
);
expect(formatError).toHaveBeenCalled();
expect(formatErrorMock).toHaveBeenCalled();
});
});

View File

@@ -7,6 +7,7 @@ import { baileys, getLastSocket, resetBaileysMocks, resetLoadConfigMock } from "
const { createWaSocket, formatError, logWebSelfId, waitForWaConnection } =
await import("./session.js");
const useMultiFileAuthStateMock = vi.mocked(baileys.useMultiFileAuthState);
function mockCredsJsonSpies(readContents: string) {
const credsSuffix = path.join(".openclaw", "credentials", "whatsapp", "default", "creds.json");
@@ -65,7 +66,7 @@ describe("web session", () => {
expect(passedLogger?.level).toBe("silent");
expect(typeof passedLogger?.trace).toBe("function");
const sock = getLastSocket();
const saveCreds = (await baileys.useMultiFileAuthState.mock.results[0].value).saveCreds;
const saveCreds = (await useMultiFileAuthStateMock.mock.results[0]?.value)?.saveCreds;
// trigger creds.update listener
sock.ev.emit("creds.update", {});
await new Promise<void>((resolve) => setImmediate(resolve));
@@ -145,7 +146,7 @@ describe("web session", () => {
await createWaSocket(false, false);
const sock = getLastSocket();
const saveCreds = (await baileys.useMultiFileAuthState.mock.results[0].value).saveCreds;
const saveCreds = (await useMultiFileAuthStateMock.mock.results[0]?.value)?.saveCreds;
sock.ev.emit("creds.update", {});
await new Promise<void>((resolve) => setImmediate(resolve));
@@ -170,7 +171,7 @@ describe("web session", () => {
await gate;
inFlight -= 1;
});
baileys.useMultiFileAuthState.mockResolvedValueOnce({
useMultiFileAuthStateMock.mockResolvedValueOnce({
state: { creds: {}, keys: {} },
saveCreds,
});
@@ -184,7 +185,9 @@ describe("web session", () => {
await new Promise<void>((resolve) => setImmediate(resolve));
expect(inFlight).toBe(1);
release?.();
if (release) {
release();
}
// let both queued saves complete
await new Promise<void>((resolve) => setImmediate(resolve));
@@ -207,7 +210,7 @@ describe("web session", () => {
await createWaSocket(false, false);
const sock = getLastSocket();
const saveCreds = (await baileys.useMultiFileAuthState.mock.results[0].value).saveCreds;
const saveCreds = (await useMultiFileAuthStateMock.mock.results[0]?.value)?.saveCreds;
sock.ev.emit("creds.update", {});
await new Promise<void>((resolve) => setImmediate(resolve));