mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 20:40:45 +00:00
test: cover provider plugin boundaries
This commit is contained in:
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/subagents: add `sessions_yield` so orchestrators can end the current turn immediately, skip queued tool work, and carry a hidden follow-up payload into the next session turn. (#36537) thanks @jriff
|
||||
- Docs/Kubernetes: Add a starter K8s install path with raw manifests, Kind setup, and deployment docs. Thanks @sallyom @dzianisv @egkristi
|
||||
- Control UI/dashboard-v2: refresh the gateway dashboard with modular overview, chat, config, agent, and session views, plus a command palette, mobile bottom tabs, and richer chat tools like slash commands, search, export, and pinned messages. (#41503) Thanks @BunsDev.
|
||||
- Models/plugins: move Ollama, vLLM, and SGLang onto the provider-plugin architecture, with provider-owned onboarding, discovery, model-picker setup, and post-selection hooks so core provider wiring is more modular.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import type { ProviderAuthMethod } from "../plugins/types.js";
|
||||
import type { ApplyAuthChoiceParams } from "./auth-choice.apply.js";
|
||||
import { applyAuthChoiceLoadedPluginProvider } from "./auth-choice.apply.plugin-provider.js";
|
||||
import {
|
||||
applyAuthChoiceLoadedPluginProvider,
|
||||
applyAuthChoicePluginProvider,
|
||||
runProviderPluginAuthMethod,
|
||||
} from "./auth-choice.apply.plugin-provider.js";
|
||||
|
||||
const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => []));
|
||||
vi.mock("../plugins/providers.js", () => ({
|
||||
@@ -105,6 +109,7 @@ function buildParams(overrides: Partial<ApplyAuthChoiceParams> = {}): ApplyAuthC
|
||||
describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
applyAuthProfileConfig.mockImplementation((config) => config);
|
||||
});
|
||||
|
||||
it("returns an agent model override when default model application is deferred", async () => {
|
||||
@@ -158,4 +163,159 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
});
|
||||
|
||||
it("merges provider config patches and emits provider notes", async () => {
|
||||
applyAuthProfileConfig.mockImplementation(((
|
||||
config: {
|
||||
auth?: {
|
||||
profiles?: Record<string, { provider: string; mode: string }>;
|
||||
};
|
||||
},
|
||||
profile: { profileId: string; provider: string; mode: string },
|
||||
) => ({
|
||||
...config,
|
||||
auth: {
|
||||
profiles: {
|
||||
...config.auth?.profiles,
|
||||
[profile.profileId]: {
|
||||
provider: profile.provider,
|
||||
mode: profile.mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as never);
|
||||
|
||||
const note = vi.fn(async () => {});
|
||||
const method: ProviderAuthMethod = {
|
||||
id: "local",
|
||||
label: "Local",
|
||||
kind: "custom",
|
||||
run: async () => ({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "ollama:default",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "ollama",
|
||||
key: "ollama-local",
|
||||
},
|
||||
},
|
||||
],
|
||||
configPatch: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
api: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultModel: "ollama/qwen3:4b",
|
||||
notes: ["Detected local Ollama runtime.", "Pulled model metadata."],
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await runProviderPluginAuthMethod({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "anthropic/claude-sonnet-4-5" },
|
||||
},
|
||||
},
|
||||
},
|
||||
runtime: {} as ApplyAuthChoiceParams["runtime"],
|
||||
prompter: {
|
||||
note,
|
||||
} as unknown as ApplyAuthChoiceParams["prompter"],
|
||||
method,
|
||||
});
|
||||
|
||||
expect(result.defaultModel).toBe("ollama/qwen3:4b");
|
||||
expect(result.config.models?.providers?.ollama).toEqual({
|
||||
api: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
models: [],
|
||||
});
|
||||
expect(result.config.auth?.profiles?.["ollama:default"]).toEqual({
|
||||
provider: "ollama",
|
||||
mode: "api_key",
|
||||
});
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"Detected local Ollama runtime.\nPulled model metadata.",
|
||||
"Provider notes",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an agent-scoped override for plugin auth choices when default model application is deferred", async () => {
|
||||
const provider = buildProvider();
|
||||
resolvePluginProviders.mockReturnValue([provider]);
|
||||
|
||||
const note = vi.fn(async () => {});
|
||||
const result = await applyAuthChoicePluginProvider(
|
||||
buildParams({
|
||||
authChoice: "provider-plugin:ollama:local",
|
||||
agentId: "worker",
|
||||
setDefaultModel: false,
|
||||
prompter: {
|
||||
note,
|
||||
} as unknown as ApplyAuthChoiceParams["prompter"],
|
||||
}),
|
||||
{
|
||||
authChoice: "provider-plugin:ollama:local",
|
||||
pluginId: "ollama",
|
||||
providerId: "ollama",
|
||||
methodId: "local",
|
||||
label: "Ollama",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result?.agentModelOverride).toBe("ollama/qwen3:4b");
|
||||
expect(result?.config.plugins).toEqual({
|
||||
entries: {
|
||||
ollama: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(runProviderModelSelectedHook).not.toHaveBeenCalled();
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
'Default model set to ollama/qwen3:4b for agent "worker".',
|
||||
"Model configured",
|
||||
);
|
||||
});
|
||||
|
||||
it("stops early when the plugin is disabled in config", async () => {
|
||||
const note = vi.fn(async () => {});
|
||||
|
||||
const result = await applyAuthChoicePluginProvider(
|
||||
buildParams({
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
prompter: {
|
||||
note,
|
||||
} as unknown as ApplyAuthChoiceParams["prompter"],
|
||||
}),
|
||||
{
|
||||
authChoice: "ollama",
|
||||
pluginId: "ollama",
|
||||
providerId: "ollama",
|
||||
label: "Ollama",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(resolvePluginProviders).not.toHaveBeenCalled();
|
||||
expect(note).toHaveBeenCalledWith("Ollama plugin is disabled (plugins disabled).", "Ollama");
|
||||
});
|
||||
});
|
||||
|
||||
134
src/plugins/provider-wizard.test.ts
Normal file
134
src/plugins/provider-wizard.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildProviderPluginMethodChoice,
|
||||
resolveProviderModelPickerEntries,
|
||||
resolveProviderPluginChoice,
|
||||
resolveProviderWizardOptions,
|
||||
runProviderModelSelectedHook,
|
||||
} from "./provider-wizard.js";
|
||||
import type { ProviderPlugin } from "./types.js";
|
||||
|
||||
const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => []));
|
||||
vi.mock("./providers.js", () => ({
|
||||
resolvePluginProviders,
|
||||
}));
|
||||
|
||||
function makeProvider(overrides: Partial<ProviderPlugin> & Pick<ProviderPlugin, "id" | "label">) {
|
||||
return {
|
||||
auth: [],
|
||||
...overrides,
|
||||
} satisfies ProviderPlugin;
|
||||
}
|
||||
|
||||
describe("provider wizard boundaries", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("uses explicit onboarding choice ids and bound method ids", () => {
|
||||
const provider = makeProvider({
|
||||
id: "vllm",
|
||||
label: "vLLM",
|
||||
auth: [
|
||||
{ id: "local", label: "Local", kind: "custom", run: vi.fn() },
|
||||
{ id: "cloud", label: "Cloud", kind: "custom", run: vi.fn() },
|
||||
],
|
||||
wizard: {
|
||||
onboarding: {
|
||||
choiceId: "self-hosted-vllm",
|
||||
methodId: "local",
|
||||
choiceLabel: "vLLM local",
|
||||
groupId: "local-runtimes",
|
||||
groupLabel: "Local runtimes",
|
||||
},
|
||||
},
|
||||
});
|
||||
resolvePluginProviders.mockReturnValue([provider]);
|
||||
|
||||
expect(resolveProviderWizardOptions({})).toEqual([
|
||||
{
|
||||
value: "self-hosted-vllm",
|
||||
label: "vLLM local",
|
||||
groupId: "local-runtimes",
|
||||
groupLabel: "Local runtimes",
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
resolveProviderPluginChoice({
|
||||
providers: [provider],
|
||||
choice: "self-hosted-vllm",
|
||||
}),
|
||||
).toEqual({
|
||||
provider,
|
||||
method: provider.auth[0],
|
||||
});
|
||||
});
|
||||
|
||||
it("builds model-picker entries from plugin metadata and provider-method choices", () => {
|
||||
const provider = makeProvider({
|
||||
id: "sglang",
|
||||
label: "SGLang",
|
||||
auth: [
|
||||
{ id: "server", label: "Server", kind: "custom", run: vi.fn() },
|
||||
{ id: "cloud", label: "Cloud", kind: "custom", run: vi.fn() },
|
||||
],
|
||||
wizard: {
|
||||
modelPicker: {
|
||||
label: "SGLang server",
|
||||
hint: "OpenAI-compatible local runtime",
|
||||
methodId: "server",
|
||||
},
|
||||
},
|
||||
});
|
||||
resolvePluginProviders.mockReturnValue([provider]);
|
||||
|
||||
expect(resolveProviderModelPickerEntries({})).toEqual([
|
||||
{
|
||||
value: buildProviderPluginMethodChoice("sglang", "server"),
|
||||
label: "SGLang server",
|
||||
hint: "OpenAI-compatible local runtime",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("routes model-selected hooks only to the matching provider", async () => {
|
||||
const matchingHook = vi.fn(async () => {});
|
||||
const otherHook = vi.fn(async () => {});
|
||||
resolvePluginProviders.mockReturnValue([
|
||||
makeProvider({
|
||||
id: "ollama",
|
||||
label: "Ollama",
|
||||
onModelSelected: otherHook,
|
||||
}),
|
||||
makeProvider({
|
||||
id: "vllm",
|
||||
label: "vLLM",
|
||||
onModelSelected: matchingHook,
|
||||
}),
|
||||
]);
|
||||
|
||||
const env = { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv;
|
||||
await runProviderModelSelectedHook({
|
||||
config: {},
|
||||
model: "vllm/qwen3-coder",
|
||||
prompter: {} as never,
|
||||
agentDir: "/tmp/agent",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env,
|
||||
});
|
||||
|
||||
expect(resolvePluginProviders).toHaveBeenCalledWith({
|
||||
config: {},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env,
|
||||
});
|
||||
expect(matchingHook).toHaveBeenCalledWith({
|
||||
config: {},
|
||||
model: "vllm/qwen3-coder",
|
||||
prompter: {},
|
||||
agentDir: "/tmp/agent",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
expect(otherHook).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user