refactor: move provider-specific tests to extensions

This commit is contained in:
Peter Steinberger
2026-04-18 19:15:42 +01:00
parent 7474b52584
commit 00e613f12d
7 changed files with 62 additions and 182 deletions

View File

@@ -1,24 +1,12 @@
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-onboard";
import { type OpenClawConfig, withFetchPreconnect } from "openclaw/plugin-sdk/testing";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { ModelDefinitionConfig } from "../config/types.models.js";
import {
normalizePluginDiscoveryResult,
runProviderCatalog,
} from "../plugins/provider-discovery.js";
import type { ProviderPlugin } from "../plugins/types.js";
import { resolveRelativeBundledPluginPublicModuleId } from "../test-utils/bundled-plugin-public-surface.js";
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
import { OLLAMA_LOCAL_AUTH_MARKER } from "./model-auth-markers.js";
import type { ProviderConfig } from "./models-config.providers.secrets.js";
import { ollamaProviderDiscovery } from "./provider-discovery.js";
const OLLAMA_PROVIDER_DISCOVERY_MODULE_ID = resolveRelativeBundledPluginPublicModuleId({
fromModuleUrl: import.meta.url,
pluginId: "ollama",
artifactBasename: "provider-discovery.js",
});
const OLLAMA_LOCAL_AUTH_MARKER = "ollama-local";
afterEach(() => {
vi.unstubAllEnvs();
@@ -54,35 +42,14 @@ describe("Ollama provider", () => {
}
}
let ollamaCatalogProvider: Promise<ProviderPlugin | undefined> | undefined;
function loadOllamaCatalogProvider(): Promise<ProviderPlugin | undefined> {
ollamaCatalogProvider ??= import(OLLAMA_PROVIDER_DISCOVERY_MODULE_ID).then((surface) => {
const typed = surface as {
default?: ProviderPlugin;
ollamaProviderDiscovery?: ProviderPlugin;
};
return typed.default ?? typed.ollamaProviderDiscovery;
});
return ollamaCatalogProvider;
}
async function runOllamaCatalog(params: {
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
}): Promise<ProviderConfig | undefined> {
const provider = await loadOllamaCatalogProvider();
if (!provider) {
return undefined;
}
async function runOllamaCatalog(params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv }) {
const env: NodeJS.ProcessEnv = {
...process.env,
VITEST: "1",
NODE_ENV: "test",
...params.env,
};
const result = await runProviderCatalog({
provider,
const result = await ollamaProviderDiscovery.discovery.run({
config: params.config ?? {},
agentDir: createAgentDir(),
env,
@@ -95,9 +62,7 @@ describe("Ollama provider", () => {
source: env.OLLAMA_API_KEY?.trim() ? "env" : "none",
}),
});
return normalizePluginDiscoveryResult({ provider, result }).ollama as
| ProviderConfig
| undefined;
return result && "provider" in result ? result.provider : undefined;
}
async function withoutAmbientOllamaEnv<T>(run: () => Promise<T>): Promise<T> {

View File

@@ -17,25 +17,23 @@ vi.mock("../plugins/provider-runtime.js", async () => {
...actual,
buildProviderMissingAuthMessageWithPlugin: () => undefined,
resolveExternalAuthProfilesWithPlugins: () => [],
shouldDeferProviderSyntheticProfileAuthWithPlugin: (params: {
provider: string;
context: { resolvedApiKey?: string };
}) => params.provider === "ollama" && params.context.resolvedApiKey?.trim() === "ollama-local",
shouldDeferProviderSyntheticProfileAuthWithPlugin: () => false,
resolveProviderSyntheticAuthWithPlugin: (params: {
provider: string;
config?: {
plugins?: {
enabled?: boolean;
entries?: {
xai?: {
entries?: Record<
string,
{
enabled?: boolean;
config?: {
webSearch?: {
apiKey?: unknown;
};
};
};
};
}
>;
};
tools?: {
web?: {
@@ -49,56 +47,39 @@ vi.mock("../plugins/provider-runtime.js", async () => {
};
context: { providerConfig?: { api?: string; baseUrl?: string; models?: unknown[] } };
}) => {
if (params.provider === "xai") {
if (params.provider === "plugin-web") {
if (
params.config?.plugins?.enabled === false ||
params.config?.plugins?.entries?.xai?.enabled === false
params.config?.plugins?.entries?.["plugin-web"]?.enabled === false
) {
return undefined;
}
const pluginApiKey = params.config?.plugins?.entries?.xai?.config?.webSearch?.apiKey;
const pluginApiKey =
params.config?.plugins?.entries?.["plugin-web"]?.config?.webSearch?.apiKey;
if (typeof pluginApiKey === "string" && pluginApiKey.trim()) {
return {
apiKey: pluginApiKey.trim(),
source: "plugins.entries.xai.config.webSearch.apiKey",
source: "plugins.entries.plugin-web.config.webSearch.apiKey",
mode: "api-key" as const,
};
}
if (pluginApiKey && typeof pluginApiKey === "object") {
return {
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
source: "plugins.entries.plugin-web.config.webSearch.apiKey",
mode: "api-key" as const,
};
}
return undefined;
}
if (params.provider === "claude-cli") {
if (params.provider === "native-cli") {
return {
apiKey: "claude-cli-access-token",
source: "Claude CLI native auth",
apiKey: "native-cli-access-token",
source: "Native CLI auth",
mode: "oauth" as const,
};
}
if (params.provider !== "ollama") {
return undefined;
}
const providerConfig = params.context.providerConfig;
const hasMeaningfulOllamaConfig =
(Array.isArray(providerConfig?.models) && providerConfig.models.length > 0) ||
Boolean(providerConfig?.api?.trim() && providerConfig.api.trim() !== "ollama") ||
Boolean(
providerConfig?.baseUrl?.trim() &&
providerConfig.baseUrl.trim().replace(/\/+$/, "") !== "http://127.0.0.1:11434",
);
if (!hasMeaningfulOllamaConfig) {
return undefined;
}
return {
apiKey: "ollama-local",
source: "models.providers.ollama (synthetic local key)",
mode: "api-key" as const,
};
return undefined;
},
};
});
@@ -626,17 +607,17 @@ describe("resolveUsableCustomProviderApiKey", () => {
});
describe("resolveApiKeyForProvider", () => {
it("reuses the xai plugin web search key without models.providers.xai", async () => {
const resolved = await withoutEnv("XAI_API_KEY", () =>
it("reuses plugin fallback auth without a models.providers entry", async () => {
const resolved = await withoutEnv("PLUGIN_WEB_API_KEY", () =>
resolveApiKeyForProvider({
provider: "xai",
provider: "plugin-web",
cfg: {
plugins: {
entries: {
xai: {
"plugin-web": {
config: {
webSearch: {
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
apiKey: "plugin-web-fallback-key", // pragma: allowlist secret
},
},
},
@@ -648,20 +629,20 @@ describe("resolveApiKeyForProvider", () => {
);
expect(resolved).toMatchObject({
apiKey: "xai-plugin-fallback-key",
source: "plugins.entries.xai.config.webSearch.apiKey",
apiKey: "plugin-web-fallback-key",
source: "plugins.entries.plugin-web.config.webSearch.apiKey",
mode: "api-key",
});
});
it("prefers the active runtime snapshot for SecretRef-backed xai fallback auth", async () => {
it("prefers the active runtime snapshot for SecretRef-backed plugin fallback auth", async () => {
const sourceConfig = {
plugins: {
entries: {
xai: {
"plugin-web": {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/api-key" },
apiKey: { source: "file", provider: "vault", id: "/plugin-web/api-key" },
},
},
},
@@ -671,10 +652,10 @@ describe("resolveApiKeyForProvider", () => {
const runtimeConfig = {
plugins: {
entries: {
xai: {
"plugin-web": {
config: {
webSearch: {
apiKey: "xai-runtime-key", // pragma: allowlist secret
apiKey: "plugin-web-runtime-key", // pragma: allowlist secret
},
},
},
@@ -683,34 +664,34 @@ describe("resolveApiKeyForProvider", () => {
};
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
const resolved = await withoutEnv("XAI_API_KEY", () =>
const resolved = await withoutEnv("PLUGIN_WEB_API_KEY", () =>
resolveApiKeyForProvider({
provider: "xai",
provider: "plugin-web",
cfg: sourceConfig,
store: { version: 1, profiles: {} },
}),
);
expect(resolved).toMatchObject({
apiKey: "xai-runtime-key",
source: "plugins.entries.xai.config.webSearch.apiKey",
apiKey: "plugin-web-runtime-key",
source: "plugins.entries.plugin-web.config.webSearch.apiKey",
mode: "api-key",
});
});
it("does not reuse xai fallback auth when the xai plugin is disabled", async () => {
it("does not reuse plugin fallback auth when the plugin is disabled", async () => {
await expect(
withoutEnv("XAI_API_KEY", () =>
withoutEnv("PLUGIN_WEB_API_KEY", () =>
resolveApiKeyForProvider({
provider: "xai",
provider: "plugin-web",
cfg: {
plugins: {
entries: {
xai: {
"plugin-web": {
enabled: false,
config: {
webSearch: {
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
apiKey: "plugin-web-fallback-key", // pragma: allowlist secret
},
},
},
@@ -720,17 +701,17 @@ describe("resolveApiKeyForProvider", () => {
store: { version: 1, profiles: {} },
}),
),
).rejects.toThrow('No API key found for provider "xai"');
).rejects.toThrow('No API key found for provider "plugin-web"');
});
it("reuses native Claude CLI auth for the claude-cli provider", async () => {
it("reuses plugin-owned native CLI auth", async () => {
const resolved = await resolveApiKeyForProvider({
provider: "claude-cli",
provider: "native-cli",
cfg: {
agents: {
defaults: {
model: {
primary: "claude-cli/claude-sonnet-4-6",
primary: "native-cli/demo-model",
},
},
},
@@ -739,8 +720,8 @@ describe("resolveApiKeyForProvider", () => {
});
expect(resolved).toEqual({
apiKey: "claude-cli-access-token",
source: "Claude CLI native auth",
apiKey: "native-cli-access-token",
source: "Native CLI auth",
mode: "oauth",
});
});

View File

@@ -1223,14 +1223,15 @@ describe("openai transport stream", () => {
expect(params.stream_options).toMatchObject({ include_usage: true });
});
it("enables streaming usage compat for Ollama OpenAI-compat endpoints", () => {
it("honors explicit streaming usage compat for configured custom providers", () => {
const params = buildOpenAICompletionsParams(
{
id: "qwen2.5:7b",
name: "Qwen 2.5 7B",
id: "custom-model",
name: "Custom Model",
api: "openai-completions",
provider: "ollama",
baseUrl: "http://127.0.0.1:11434/v1",
provider: "custom-cpa",
baseUrl: "https://proxy.example.com/v1",
compat: { supportsUsageInStreaming: true },
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },

View File

@@ -1,31 +0,0 @@
import { describe, expect, it } from "vitest";
import { shouldApplyMoonshotPayloadCompat } from "./moonshot-stream-wrappers.js";
describe("moonshot stream wrappers", () => {
it("keeps Moonshot compatibility on the lightweight provider-id path", () => {
expect(
shouldApplyMoonshotPayloadCompat({
provider: "moonshot",
modelId: "kimi-k2.5",
}),
).toBe(true);
expect(
shouldApplyMoonshotPayloadCompat({
provider: "kimi-coding",
modelId: "kimi-code",
}),
).toBe(true);
expect(
shouldApplyMoonshotPayloadCompat({
provider: "ollama",
modelId: "kimi-k2.5:cloud",
}),
).toBe(true);
expect(
shouldApplyMoonshotPayloadCompat({
provider: "openai",
modelId: "gpt-5.4",
}),
).toBe(false);
});
});

View File

@@ -1,8 +1,6 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { resolveProviderRequestCapabilities } from "../provider-attribution.js";
import { normalizeProviderId } from "../provider-id.js";
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
export {
@@ -22,21 +20,6 @@ export function shouldApplySiliconFlowThinkingOffCompat(params: {
);
}
export function shouldApplyMoonshotPayloadCompat(params: {
provider: string;
modelId: string;
}): boolean {
const normalizedProvider = normalizeProviderId(params.provider);
return (
resolveProviderRequestCapabilities({
provider: normalizedProvider,
modelId: params.modelId,
capability: "llm",
transport: "stream",
}).compatibilityFamily === "moonshot"
);
}
export function createSiliconFlowThinkingWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) =>

View File

@@ -445,20 +445,13 @@ describe("noteMemorySearchHealth", () => {
expect(message).toContain("openclaw configure --section model");
});
it("still warns in auto mode when only ollama credentials exist", async () => {
it("does not probe unrelated embedding providers in auto mode", async () => {
resolveMemorySearchConfig.mockReturnValue({
provider: "auto",
local: {},
remote: {},
});
resolveApiKeyForProvider.mockImplementation(async ({ provider }: { provider: string }) => {
if (provider === "ollama") {
return {
apiKey: "ollama-local", // pragma: allowlist secret
source: "env: OLLAMA_API_KEY",
mode: "api-key",
};
}
resolveApiKeyForProvider.mockImplementation(async () => {
throw new Error("missing key");
});

View File

@@ -1,14 +1,7 @@
import { hasNonEmptyString } from "../infra/outbound/channel-target.js";
import { getChannelEnvVars } from "../secrets/channel-env-vars.js";
import { isRecord } from "../utils.js";
import type { OpenClawConfig } from "./config.js";
const STATIC_ENV_RULES: Record<string, string[] | ((env: NodeJS.ProcessEnv) => boolean)> = {
discord: ["DISCORD_BOT_TOKEN"],
slack: ["SLACK_BOT_TOKEN"],
telegram: ["TELEGRAM_BOT_TOKEN"],
irc: (env) => hasNonEmptyString(env.IRC_HOST) && hasNonEmptyString(env.IRC_NICK),
};
export function resolveChannelConfigRecord(
cfg: OpenClawConfig,
channelId: string,
@@ -30,15 +23,10 @@ export function isStaticallyChannelConfigured(
channelId: string,
env: NodeJS.ProcessEnv = process.env,
): boolean {
const staticRule = STATIC_ENV_RULES[channelId];
if (Array.isArray(staticRule)) {
for (const envVar of staticRule) {
if (hasNonEmptyString(env[envVar])) {
return true;
}
for (const envVar of getChannelEnvVars(channelId, { config: cfg, env })) {
if (typeof env[envVar] === "string" && env[envVar].trim().length > 0) {
return true;
}
} else if (staticRule?.(env)) {
return true;
}
return hasMeaningfulChannelConfigShallow(resolveChannelConfigRecord(cfg, channelId));
}