test: harden ci isolated mocks

This commit is contained in:
Peter Steinberger
2026-03-23 08:51:45 +00:00
parent aa02b86a9e
commit fb602c9b02
16 changed files with 305 additions and 211 deletions

View File

@@ -4,50 +4,42 @@ import type { OpenClawConfig } from "../config/config.js";
const mockStore: Record<string, Record<string, unknown>> = {};
vi.mock("../config/sessions.js", () => ({
loadSessionStore: vi.fn((storePath: string) => mockStore[storePath] ?? {}),
resolveAgentMainSessionKey: vi.fn(({ agentId }: { agentId: string }) => `agent:${agentId}:main`),
resolveStorePath: vi.fn((_store: unknown, _opts: unknown) => "/mock/store.json"),
}));
vi.mock("../infra/outbound/channel-selection.js", () => ({
resolveMessageChannelSelection: vi.fn(async () => ({ channel: "telegram" })),
}));
vi.mock("../channels/plugins/index.js", () => ({
getChannelPlugin: vi.fn(() => ({
meta: { label: "Telegram" },
config: {},
messaging: {
parseExplicitTarget: ({ raw }: { raw: string }) => {
const target = parseTelegramTarget(raw);
return {
to: target.chatId,
threadId: target.messageThreadId,
chatType: target.chatType === "unknown" ? undefined : target.chatType,
};
},
},
outbound: {
resolveTarget: ({ to }: { to?: string }) =>
to ? { ok: true, to } : { ok: false, error: new Error("missing") },
},
})),
normalizeChannelId: vi.fn((id: string) => id),
}));
const mockedModuleIds = [
"../channels/plugins/index.js",
"../config/sessions.js",
"../infra/outbound/channel-selection.js",
];
let resolveDeliveryTarget: typeof import("./isolated-agent/delivery-target.js").resolveDeliveryTarget;
beforeEach(async () => {
vi.clearAllMocks();
vi.resetModules();
vi.doMock("../config/sessions.js", () => ({
loadSessionStore: vi.fn((storePath: string) => mockStore[storePath] ?? {}),
resolveAgentMainSessionKey: vi.fn(
({ agentId }: { agentId: string }) => `agent:${agentId}:main`,
),
resolveStorePath: vi.fn((_store: unknown, _opts: unknown) => "/mock/store.json"),
}));
vi.doMock("../infra/outbound/channel-selection.js", () => ({
resolveMessageChannelSelection: vi.fn(async () => ({ channel: "telegram" })),
}));
vi.doMock("../channels/plugins/index.js", () => ({
getChannelPlugin: vi.fn(() => ({
meta: { label: "Telegram" },
config: {},
messaging: {
parseExplicitTarget: ({ raw }: { raw: string }) => {
const target = parseTelegramTarget(raw);
return {
to: target.chatId,
threadId: target.messageThreadId,
chatType: target.chatType === "unknown" ? undefined : target.chatType,
};
},
},
outbound: {
resolveTarget: ({ to }: { to?: string }) =>
to ? { ok: true, to } : { ok: false, error: new Error("missing") },
},
})),
normalizeChannelId: vi.fn((id: string) => id),
}));
({ resolveDeliveryTarget } = await import("./isolated-agent/delivery-target.js"));
vi.clearAllMocks();
for (const key of Object.keys(mockStore)) {
delete mockStore[key];
}
@@ -55,9 +47,6 @@ beforeEach(async () => {
afterAll(() => {
vi.restoreAllMocks();
for (const id of mockedModuleIds) {
vi.doUnmock(id);
}
vi.resetModules();
});

View File

@@ -5,18 +5,25 @@ import { getActivePluginRegistry, getActivePluginRegistryKey } from "../plugins/
import type { ImageGenerationProviderPlugin } from "../plugins/types.js";
const BUILTIN_IMAGE_GENERATION_PROVIDERS: readonly ImageGenerationProviderPlugin[] = [];
const UNSAFE_PROVIDER_IDS = new Set(["__proto__", "constructor", "prototype"]);
function normalizeImageGenerationProviderId(id: string | undefined): string | undefined {
const normalized = normalizeProviderId(id ?? "");
return normalized || undefined;
}
function isSafeImageGenerationProviderId(id: string | undefined): id is string {
return Boolean(id && !UNSAFE_PROVIDER_IDS.has(id));
}
function resolvePluginImageGenerationProviders(
cfg?: OpenClawConfig,
): ImageGenerationProviderPlugin[] {
const active = getActivePluginRegistry();
const registry =
getActivePluginRegistryKey() || !cfg ? active : loadOpenClawPlugins({ config: cfg });
(active?.imageGenerationProviders?.length ?? 0) > 0 || getActivePluginRegistryKey() || !cfg
? active
: loadOpenClawPlugins({ config: cfg });
return registry?.imageGenerationProviders?.map((entry) => entry.provider) ?? [];
}
@@ -28,14 +35,14 @@ function buildProviderMaps(cfg?: OpenClawConfig): {
const aliases = new Map<string, ImageGenerationProviderPlugin>();
const register = (provider: ImageGenerationProviderPlugin) => {
const id = normalizeImageGenerationProviderId(provider.id);
if (!id) {
if (!isSafeImageGenerationProviderId(id)) {
return;
}
canonical.set(id, provider);
aliases.set(id, provider);
for (const alias of provider.aliases ?? []) {
const normalizedAlias = normalizeImageGenerationProviderId(alias);
if (normalizedAlias) {
if (isSafeImageGenerationProviderId(normalizedAlias)) {
aliases.set(normalizedAlias, provider);
}
}

View File

@@ -4,22 +4,11 @@ import type { OpenClawConfig } from "../../config/config.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
let runMessageAction: typeof import("./message-action-runner.js").runMessageAction;
const mocks = vi.hoisted(() => ({
executePollAction: vi.fn(),
}));
let runMessageAction: typeof import("./message-action-runner.js").runMessageAction;
vi.mock("./outbound-send-service.js", async () => {
const actual = await vi.importActual<typeof import("./outbound-send-service.js")>(
"./outbound-send-service.js",
);
return {
...actual,
executePollAction: mocks.executePollAction,
};
});
const telegramConfig = {
channels: {
telegram: {
@@ -43,6 +32,12 @@ const telegramPollTestPlugin: ChannelPlugin = {
resolveAccount: () => ({ botToken: "telegram-test" }),
isConfigured: () => true,
},
outbound: {
deliveryMode: "gateway",
sendPoll: async () => ({
messageId: "poll-test",
}),
},
messaging: {
targetResolver: {
looksLikeId: () => true,
@@ -99,7 +94,15 @@ async function runPollAction(params: {
describe("runMessageAction poll handling", () => {
beforeEach(async () => {
vi.resetModules();
({ runMessageAction } = await import("./message-action-runner.js"));
vi.doMock("./outbound-send-service.js", async () => {
const actual = await vi.importActual<typeof import("./outbound-send-service.js")>(
"./outbound-send-service.js",
);
return {
...actual,
executePollAction: mocks.executePollAction,
};
});
setActivePluginRegistry(
createTestRegistry([
{
@@ -115,6 +118,7 @@ describe("runMessageAction poll handling", () => {
payload: { ok: true, corePoll: input.resolveCorePoll() },
pollResult: { ok: true },
}));
({ runMessageAction } = await import("./message-action-runner.js"));
});
afterEach(() => {

View File

@@ -7,43 +7,6 @@ const mocks = vi.hoisted(() => ({
loadOpenClawPlugins: vi.fn(),
}));
vi.mock("../../channels/plugins/index.js", () => ({
normalizeChannelId: (channel?: string) => channel?.trim().toLowerCase() ?? undefined,
getChannelPlugin: mocks.getChannelPlugin,
listChannelPlugins: () => [],
}));
vi.mock("../../agents/agent-scope.js", () => ({
resolveDefaultAgentId: () => "main",
resolveSessionAgentId: ({
sessionKey,
}: {
sessionKey?: string;
config?: unknown;
agentId?: string;
}) => {
const match = sessionKey?.match(/^agent:([^:]+)/i);
return match?.[1] ?? "main";
},
resolveAgentWorkspaceDir: () => "/tmp/openclaw-test-workspace",
}));
vi.mock("../../config/plugin-auto-enable.js", () => ({
applyPluginAutoEnable: ({ config }: { config: unknown }) => ({ config, changes: [] }),
}));
vi.mock("../../plugins/loader.js", () => ({
loadOpenClawPlugins: mocks.loadOpenClawPlugins,
}));
vi.mock("./targets.js", () => ({
resolveOutboundTarget: mocks.resolveOutboundTarget,
}));
vi.mock("./deliver.js", () => ({
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
}));
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
@@ -52,7 +15,37 @@ let sendMessage: typeof import("./message.js").sendMessage;
describe("sendMessage", () => {
beforeEach(async () => {
vi.resetModules();
({ sendMessage } = await import("./message.js"));
vi.doMock("../../channels/plugins/index.js", () => ({
normalizeChannelId: (channel?: string) => channel?.trim().toLowerCase() ?? undefined,
getChannelPlugin: mocks.getChannelPlugin,
listChannelPlugins: () => [],
}));
vi.doMock("../../agents/agent-scope.js", () => ({
resolveDefaultAgentId: () => "main",
resolveSessionAgentId: ({
sessionKey,
}: {
sessionKey?: string;
config?: unknown;
agentId?: string;
}) => {
const match = sessionKey?.match(/^agent:([^:]+)/i);
return match?.[1] ?? "main";
},
resolveAgentWorkspaceDir: () => "/tmp/openclaw-test-workspace",
}));
vi.doMock("../../config/plugin-auto-enable.js", () => ({
applyPluginAutoEnable: ({ config }: { config: unknown }) => ({ config, changes: [] }),
}));
vi.doMock("../../plugins/loader.js", () => ({
loadOpenClawPlugins: mocks.loadOpenClawPlugins,
}));
vi.doMock("./targets.js", () => ({
resolveOutboundTarget: mocks.resolveOutboundTarget,
}));
vi.doMock("./deliver.js", () => ({
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
}));
setActivePluginRegistry(createTestRegistry([]));
mocks.getChannelPlugin.mockClear();
mocks.resolveOutboundTarget.mockClear();
@@ -64,6 +57,8 @@ describe("sendMessage", () => {
});
mocks.resolveOutboundTarget.mockImplementation(({ to }: { to: string }) => ({ ok: true, to }));
mocks.deliverOutboundPayloads.mockResolvedValue([{ channel: "mattermost", messageId: "m1" }]);
({ sendMessage } = await import("./message.js"));
});
it("passes explicit agentId to outbound delivery for scoped media roots", async () => {

View File

@@ -3,6 +3,11 @@ import { beforeEach, describe, expect, it } from "vitest";
import { vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
let applyCrossContextDecoration: typeof import("./outbound-policy.js").applyCrossContextDecoration;
let buildCrossContextDecoration: typeof import("./outbound-policy.js").buildCrossContextDecoration;
let enforceCrossContextPolicy: typeof import("./outbound-policy.js").enforceCrossContextPolicy;
let shouldApplyCrossContextMarker: typeof import("./outbound-policy.js").shouldApplyCrossContextMarker;
class TestDiscordUiContainer extends Container {}
const mocks = vi.hoisted(() => ({
@@ -47,19 +52,6 @@ const mocks = vi.hoisted(() => ({
),
}));
vi.mock("./channel-adapters.js", () => ({
getChannelMessageAdapter: mocks.getChannelMessageAdapter,
}));
vi.mock("./target-normalization.js", () => ({
normalizeTargetForProvider: mocks.normalizeTargetForProvider,
}));
vi.mock("./target-resolver.js", () => ({
formatTargetDisplay: mocks.formatTargetDisplay,
lookupDirectoryDisplay: mocks.lookupDirectoryDisplay,
}));
const slackConfig = {
channels: {
slack: {
@@ -75,15 +67,20 @@ const discordConfig = {
},
} as OpenClawConfig;
let applyCrossContextDecoration: typeof import("./outbound-policy.js").applyCrossContextDecoration;
let buildCrossContextDecoration: typeof import("./outbound-policy.js").buildCrossContextDecoration;
let enforceCrossContextPolicy: typeof import("./outbound-policy.js").enforceCrossContextPolicy;
let shouldApplyCrossContextMarker: typeof import("./outbound-policy.js").shouldApplyCrossContextMarker;
describe("outbound policy helpers", () => {
beforeEach(async () => {
vi.clearAllMocks();
vi.resetModules();
vi.clearAllMocks();
vi.doMock("./channel-adapters.js", () => ({
getChannelMessageAdapter: mocks.getChannelMessageAdapter,
}));
vi.doMock("./target-normalization.js", () => ({
normalizeTargetForProvider: mocks.normalizeTargetForProvider,
}));
vi.doMock("./target-resolver.js", () => ({
formatTargetDisplay: mocks.formatTargetDisplay,
lookupDirectoryDisplay: mocks.lookupDirectoryDisplay,
}));
({
applyCrossContextDecoration,
buildCrossContextDecoration,

View File

@@ -16,15 +16,25 @@ vi.mock("../config/paths.js", () => ({
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
}));
import {
__testing,
cleanStaleGatewayProcessesSync,
findGatewayPidsOnPortSync,
} from "./restart-stale-pids.js";
let __testing: typeof import("./restart-stale-pids.js").__testing;
let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync;
let findGatewayPidsOnPortSync: typeof import("./restart-stale-pids.js").findGatewayPidsOnPortSync;
let currentTimeMs = 0;
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("node:child_process", () => ({
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
}));
vi.doMock("./ports-lsof.js", () => ({
resolveLsofCommandSync: (...args: unknown[]) => resolveLsofCommandSyncMock(...args),
}));
vi.doMock("../config/paths.js", () => ({
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
}));
({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } =
await import("./restart-stale-pids.js"));
spawnSyncMock.mockReset();
resolveLsofCommandSyncMock.mockReset();
resolveGatewayPortMock.mockReset();

View File

@@ -29,43 +29,40 @@ const {
discoverModelsMock,
} = hoisted;
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
const actual = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...actual,
complete: completeMock,
};
});
vi.mock("../agents/minimax-vlm.js", () => ({
isMinimaxVlmProvider: (provider: string) =>
provider === "minimax" || provider === "minimax-portal",
isMinimaxVlmModel: (provider: string, modelId: string) =>
(provider === "minimax" || provider === "minimax-portal") && modelId === "MiniMax-VL-01",
minimaxUnderstandImage: minimaxUnderstandImageMock,
}));
vi.mock("../agents/models-config.js", () => ({
ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock,
}));
vi.mock("../agents/model-auth.js", () => ({
getApiKeyForModel: getApiKeyForModelMock,
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
requireApiKey: requireApiKeyMock,
}));
vi.mock("../agents/pi-model-discovery-runtime.js", () => ({
discoverAuthStorage: () => ({
setRuntimeApiKey: setRuntimeApiKeyMock,
}),
discoverModels: discoverModelsMock,
}));
const { describeImageWithModel } = await import("./image.js");
let describeImageWithModel: typeof import("./image.js").describeImageWithModel;
describe("describeImageWithModel", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("@mariozechner/pi-ai", async (importOriginal) => {
const actual = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...actual,
complete: completeMock,
};
});
vi.doMock("../agents/minimax-vlm.js", () => ({
isMinimaxVlmProvider: (provider: string) =>
provider === "minimax" || provider === "minimax-portal",
isMinimaxVlmModel: (provider: string, modelId: string) =>
(provider === "minimax" || provider === "minimax-portal") && modelId === "MiniMax-VL-01",
minimaxUnderstandImage: minimaxUnderstandImageMock,
}));
vi.doMock("../agents/models-config.js", () => ({
ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock,
}));
vi.doMock("../agents/model-auth.js", () => ({
getApiKeyForModel: getApiKeyForModelMock,
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
requireApiKey: requireApiKeyMock,
}));
vi.doMock("../agents/pi-model-discovery-runtime.js", () => ({
discoverAuthStorage: () => ({
setRuntimeApiKey: setRuntimeApiKeyMock,
}),
discoverModels: discoverModelsMock,
}));
({ describeImageWithModel } = await import("./image.js"));
vi.clearAllMocks();
minimaxUnderstandImageMock.mockResolvedValue("portal ok");
discoverModelsMock.mockReturnValue({

View File

@@ -1,12 +1,11 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { MsgContext } from "../auto-reply/templating.js";
import type { OpenClawConfig } from "../config/config.js";
import {
buildProviderRegistry,
createMediaAttachmentCache,
normalizeMediaAttachments,
runCapability,
} from "./runner.js";
let buildProviderRegistry: typeof import("./runner.js").buildProviderRegistry;
let createMediaAttachmentCache: typeof import("./runner.js").createMediaAttachmentCache;
let normalizeMediaAttachments: typeof import("./runner.js").normalizeMediaAttachments;
let runCapability: typeof import("./runner.js").runCapability;
const catalog = [
{
@@ -30,7 +29,23 @@ vi.mock("../agents/model-catalog.js", async () => {
});
describe("runCapability image skip", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("../agents/model-catalog.js", async () => {
const actual = await vi.importActual<typeof import("../agents/model-catalog.js")>(
"../agents/model-catalog.js",
);
return {
...actual,
loadModelCatalog,
};
});
({
buildProviderRegistry,
createMediaAttachmentCache,
normalizeMediaAttachments,
runCapability,
} = await import("./runner.js"));
loadModelCatalog.mockClear();
});

View File

@@ -14,6 +14,7 @@ describe("postJsonWithRetry", () => {
let postJsonWithRetry: typeof import("./batch-http.js").postJsonWithRetry;
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
vi.resetModules();
({ postJsonWithRetry } = await import("./batch-http.js"));

View File

@@ -1,20 +1,19 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { postJson } from "./post-json.js";
vi.mock("./post-json.js", () => ({
postJson: vi.fn(),
}));
const postJsonMock = vi.hoisted(() => vi.fn());
type EmbeddingsRemoteFetchModule = typeof import("./embeddings-remote-fetch.js");
let fetchRemoteEmbeddingVectors: EmbeddingsRemoteFetchModule["fetchRemoteEmbeddingVectors"];
describe("fetchRemoteEmbeddingVectors", () => {
const postJsonMock = vi.mocked(postJson);
beforeEach(async () => {
vi.resetModules();
vi.doMock("./post-json.js", () => ({
postJson: postJsonMock,
}));
({ fetchRemoteEmbeddingVectors } = await import("./embeddings-remote-fetch.js"));
vi.clearAllMocks();
postJsonMock.mockReset();
});
it("maps remote embedding response data to vectors", async () => {

View File

@@ -11,6 +11,7 @@ describe("postJson", () => {
let remoteHttpMock: ReturnType<typeof vi.mocked<typeof withRemoteHttpResponse>>;
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
vi.resetModules();
({ postJson } = await import("./post-json.js"));

View File

@@ -111,18 +111,16 @@ vi.mock("./runtime.js", () => ({
},
}));
const {
__testing,
buildPluginBindingApprovalCustomId,
detachPluginConversationBinding,
getCurrentPluginConversationBinding,
parsePluginBindingApprovalCustomId,
requestPluginConversationBinding,
resolvePluginConversationBindingApproval,
} = await import("./conversation-binding.js");
const { registerSessionBindingAdapter, unregisterSessionBindingAdapter } =
await import("../infra/outbound/session-binding-service.js");
const { setActivePluginRegistry } = await import("./runtime.js");
let __testing: typeof import("./conversation-binding.js").__testing;
let buildPluginBindingApprovalCustomId: typeof import("./conversation-binding.js").buildPluginBindingApprovalCustomId;
let detachPluginConversationBinding: typeof import("./conversation-binding.js").detachPluginConversationBinding;
let getCurrentPluginConversationBinding: typeof import("./conversation-binding.js").getCurrentPluginConversationBinding;
let parsePluginBindingApprovalCustomId: typeof import("./conversation-binding.js").parsePluginBindingApprovalCustomId;
let requestPluginConversationBinding: typeof import("./conversation-binding.js").requestPluginConversationBinding;
let resolvePluginConversationBindingApproval: typeof import("./conversation-binding.js").resolvePluginConversationBindingApproval;
let registerSessionBindingAdapter: typeof import("../infra/outbound/session-binding-service.js").registerSessionBindingAdapter;
let unregisterSessionBindingAdapter: typeof import("../infra/outbound/session-binding-service.js").unregisterSessionBindingAdapter;
let setActivePluginRegistry: typeof import("./runtime.js").setActivePluginRegistry;
type PluginBindingRequest = Awaited<ReturnType<typeof requestPluginConversationBinding>>;
type ConversationBindingModule = typeof import("./conversation-binding.js");
@@ -187,7 +185,38 @@ function createDeferredVoid(): { promise: Promise<void>; resolve: () => void } {
}
describe("plugin conversation binding approvals", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("../infra/home-dir.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../infra/home-dir.js")>();
return {
...actual,
expandHomePrefix: (value: string) => {
if (value === "~/.openclaw/plugin-binding-approvals.json") {
return approvalsPath;
}
return actual.expandHomePrefix(value);
},
};
});
vi.doMock("./runtime.js", () => ({
getActivePluginRegistry: () => pluginRuntimeState.registry,
setActivePluginRegistry: (registry: PluginRegistry) => {
pluginRuntimeState.registry = registry;
},
}));
({
__testing,
buildPluginBindingApprovalCustomId,
detachPluginConversationBinding,
getCurrentPluginConversationBinding,
parsePluginBindingApprovalCustomId,
requestPluginConversationBinding,
resolvePluginConversationBindingApproval,
} = await import("./conversation-binding.js"));
({ registerSessionBindingAdapter, unregisterSessionBindingAdapter } =
await import("../infra/outbound/session-binding-service.js"));
({ setActivePluginRegistry } = await import("./runtime.js"));
sessionBindingState.reset();
__testing.reset();
setActivePluginRegistry(createEmptyPluginRegistry());

View File

@@ -20,17 +20,6 @@ const resolveOwningPluginIdsForProviderMock = vi.fn<ResolveOwningPluginIdsForPro
(_) => undefined as string[] | undefined,
);
vi.mock("./providers.js", () => ({
resolveNonBundledProviderPluginIds: (params: unknown) =>
resolveNonBundledProviderPluginIdsMock(params as never),
resolveOwningPluginIdsForProvider: (params: unknown) =>
resolveOwningPluginIdsForProviderMock(params as never),
}));
vi.mock("./providers.runtime.js", () => ({
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
}));
let augmentModelCatalogWithProviderPlugins: typeof import("./provider-runtime.js").augmentModelCatalogWithProviderPlugins;
let buildProviderAuthDoctorHintWithPlugin: typeof import("./provider-runtime.js").buildProviderAuthDoctorHintWithPlugin;
let buildProviderMissingAuthMessageWithPlugin: typeof import("./provider-runtime.js").buildProviderMissingAuthMessageWithPlugin;
@@ -70,6 +59,15 @@ const MODEL: ProviderRuntimeModel = {
describe("provider-runtime", () => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("./providers.js", () => ({
resolveNonBundledProviderPluginIds: (params: unknown) =>
resolveNonBundledProviderPluginIdsMock(params as never),
resolveOwningPluginIdsForProvider: (params: unknown) =>
resolveOwningPluginIdsForProviderMock(params as never),
}));
vi.doMock("./providers.runtime.js", () => ({
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
}));
({
augmentModelCatalogWithProviderPlugins,
buildProviderAuthDoctorHintWithPlugin,

View File

@@ -1,8 +1,9 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { edgeTTS } from "./tts-core.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let edgeTTS: typeof import("./tts-core.js").edgeTTS;
let mockTtsPromise = vi.fn<(text: string, filePath: string) => Promise<void>>();
@@ -24,10 +25,25 @@ const baseEdgeConfig = {
};
describe("edgeTTS empty audio validation", () => {
let tempDir: string;
let tempDir: string | undefined;
beforeEach(async () => {
vi.resetModules();
vi.doMock("node-edge-tts", () => ({
EdgeTTS: class {
ttsPromise(text: string, filePath: string) {
return mockTtsPromise(text, filePath);
}
},
}));
({ edgeTTS } = await import("./tts-core.js"));
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
tempDir = undefined;
}
});
it("throws when the output file is 0 bytes", async () => {

View File

@@ -3,11 +3,6 @@ import type { OpenClawConfig } from "../config/config.js";
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
import type { SpeechProviderPlugin } from "../plugins/types.js";
import {
getSpeechProvider,
listSpeechProviders,
normalizeSpeechProviderId,
} from "./provider-registry.js";
const loadOpenClawPluginsMock = vi.fn();
@@ -16,6 +11,10 @@ vi.mock("../plugins/loader.js", () => ({
loadOpenClawPluginsMock(...args),
}));
let getSpeechProvider: typeof import("./provider-registry.js").getSpeechProvider;
let listSpeechProviders: typeof import("./provider-registry.js").listSpeechProviders;
let normalizeSpeechProviderId: typeof import("./provider-registry.js").normalizeSpeechProviderId;
function createSpeechProvider(id: string, aliases?: string[]): SpeechProviderPlugin {
return {
id,
@@ -32,10 +31,13 @@ function createSpeechProvider(id: string, aliases?: string[]): SpeechProviderPlu
}
describe("speech provider registry", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
resetPluginRuntimeStateForTest();
loadOpenClawPluginsMock.mockReset();
loadOpenClawPluginsMock.mockReturnValue(createEmptyPluginRegistry());
({ getSpeechProvider, listSpeechProviders, normalizeSpeechProviderId } =
await import("./provider-registry.js"));
});
afterEach(() => {

View File

@@ -1,5 +1,5 @@
import { completeSimple, type AssistantMessage } from "@mariozechner/pi-ai";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildElevenLabsSpeechProvider } from "../../extensions/elevenlabs/speech-provider.ts";
import { buildMicrosoftSpeechProvider } from "../../extensions/microsoft/speech-provider.ts";
import { buildOpenAISpeechProvider } from "../../extensions/openai/speech-provider.ts";
@@ -401,7 +401,44 @@ describe("tts", () => {
messages: { tts: {} },
};
beforeAll(async () => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("@mariozechner/pi-ai", async (importOriginal) => {
const original = await importOriginal<typeof import("@mariozechner/pi-ai")>();
return {
...original,
completeSimple: vi.fn(),
};
});
vi.doMock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
"@mariozechner/pi-ai/oauth",
);
return {
...actual,
getOAuthProviders: () => [],
getOAuthApiKey: vi.fn(async () => null),
};
});
vi.doMock("../agents/pi-embedded-runner/model.js", () => ({
resolveModel: vi.fn((provider: string, modelId: string) =>
createResolvedModel(provider, modelId),
),
resolveModelAsync: vi.fn(async (provider: string, modelId: string) =>
createResolvedModel(provider, modelId),
),
}));
vi.doMock("../agents/model-auth.js", () => ({
getApiKeyForModel: vi.fn(async () => ({
apiKey: "test-api-key",
source: "test",
mode: "api-key",
})),
requireApiKey: vi.fn((auth: { apiKey?: string }) => auth.apiKey ?? ""),
}));
vi.doMock("../agents/custom-api-registry.js", () => ({
ensureCustomApiRegistered: vi.fn(),
}));
({ completeSimple: completeSimpleForTest } = await import("@mariozechner/pi-ai"));
({ getApiKeyForModel: getApiKeyForModelForTest } = await import("../agents/model-auth.js"));
({ resolveModelAsync: resolveModelAsyncForTest } =
@@ -411,9 +448,6 @@ describe("tts", () => {
const ttsModule = await import("./tts.js");
summarizeTextForTest = ttsModule._test.summarizeText;
resolveTtsConfigForTest = ttsModule.resolveTtsConfig;
});
beforeEach(() => {
vi.mocked(completeSimpleForTest).mockResolvedValue(
mockAssistantMessage([{ type: "text", text: "Summary" }]),
);