Files
openclaw/src/plugins/provider-discovery.runtime.test.ts
Peter Steinberger bb46b79d3c refactor: internalize OpenClaw agent runtime (#85341)
* refactor: extract agent core package

Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts.

* refactor: extract shared llm runtime

Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout.

* refactor: remove pi runtime internals

Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code.

* refactor: tighten agent session runtime

Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts.

* refactor: remove static model and pi auth paths

Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities.

* refactor: remove legacy provider compat paths

* docs: remove agent parity notes

* fix: skip provider wildcard metadata parsing

* refactor: share session extension sdk loading

* refactor: inline acpx proxy error formatter

* refactor: fold edit recovery into edit tool

* fix: accept extension batch separator

* test: align startup provider plugin expectations

* fix: restore provider-scoped release discovery

* test: align static asset packaging expectations

* fix: run static provider catalogs during scoped discovery

* fix: add provider entry catalogs for scoped live discovery

* fix: load lightweight provider catalog entries

* fix: refresh provider-scoped plugin metadata

* fix: keep provider catalog entries on release live path

* fix: keep static manifest models in release live checks

* fix: harden release model discovery

* fix: reduce OpenAI live cache probe reasoning

* fix: disable OpenAI cache probe reasoning

* ci: extend OpenAI gateway live timeout

* fix: extend live gateway model budget

* fix: stabilize release validation regressions

* fix: honor provider aliases in model rows

* fix: stabilize release validation lanes

* fix: stabilize release memory qa

* ci: stabilize release validation lanes

* ci: prefer ipv4 for live docker node calls

* fix: restore shared tool-call stream wrapper

* ci: remove legacy pi test shard alias

* fix: clean up embedded agent test drift

* fix: stabilize runtime alias status

* fix: clean up embedded agent ci drift

* fix: restore release ci invariants

* fix: clean up post-rebase runtime drift

* fix: restore release ci checks

* fix: restore release ci after rebase

* fix: remove stale pi runtime path

* test: align compaction runtime expectations

* test: update plugin prerelease expectations

* fix: handle claude live tool approvals

* fix: stabilize release validation gates

* fix: finish agent runtime import

* test: finish post-rebase agent runtime mocks

* fix: keep codex compaction native

* fix: stabilize codex app-server hook tests

* test: isolate codex diagnostic active run

* test: remove codex diagnostic completion race

# Conflicts:
#	extensions/codex/src/app-server/run-attempt.test.ts

* ci: fix full release manifest performance run id

* refactor: narrow llm plugin sdk boundary

* chore: drop generated google boundary stamps

* fix: repair rebase fallout

* fix: clean up rebased runtime references

* fix: decode codex jwt payloads as base64url

* fix: preserve shipped pi runtime alias

* fix: add scoped sdk virtual modules

* fix: decode llm codex oauth jwt as base64url

* fix: avoid stale vertex adc negative cache

* fix: harden tool arg decoding and codeql path

* fix: keep vertex adc negative checks live

* refactor: consolidate codex jwt and edit helpers

* fix: await codex oauth node runtime imports

* fix: preserve sdk tool and notice contracts

* fix: preserve shipped compat config boundaries

* fix: align codex oauth callback host

* fix: terminate agent-core loop streams on failure

* fix: keep codex oauth callback alive during fallback

* ci: include session tools in critical codeql scans

* fix: keep Cloudflare Anthropic provider auth header

* docs: redirect legacy pi runtime pages

* fix: honor bundled web provider compat discovery

* fix: protect session output spill files

* fix: keep legacy agent dir env blocked

* fix: contain auto-discovered skill symlinks

* fix: harden agent core sdk proxy surfaces

* fix: restore approval reaction sdk compat

* fix: keep live docker runs bounded

* fix: keep codex oauth redirect host aligned

* fix: resolve post-rebase agent runtime drift

* fix: redact anthropic oauth parse failures

* fix: preserve responses strict tool shaping

* fix: repair agent runtime rebase cleanup

* docs: redirect retired parity pages

* fix: bound auto-discovered resources to roots

* fix: repair post-rebase agent test drift

* fix: preserve bundled provider allowlist migration

* fix: preserve manifest-owned provider aliases

* fix: declare photon image dependency

* fix: keep provider headers out of proxy body

* fix: preserve shipped env aliases

* fix: refresh control ui i18n generated state

* fix: quote read fallback paths

* fix: preview edits through configured backend

* test: satisfy core test typecheck

* fix: preserve ZAI usage auth fallback

* test: repair codex diagnostic test

* fix: repair agent runtime rebase drift

* test: finish embedded runner import rename

* fix: repair agent runtime rebase integrations

* test: align compaction oauth fallback expectations

* fix: allow sdk-auth session models

* fix: update doctor tool schema import

* fix: preserve bedrock plugin region

* fix: stream harmony-like prose immediately

* ci: include session runtime in codeql shards

* fix: repair latest rebase integrations

* fix: honor explicit codex websocket transport

* fix: keep openai-compatible credentials provider-scoped

* fix: refresh sdk api baseline after rebase

* fix: route cli runtime aliases through openclaw harness

* test: rename stale harness mock expectation

* test: rename embedded agent overflow calls

* test: clean embedded auth test wording

* test: use openclaw stream types in deepinfra cache test

* fix: refresh sdk api baseline on latest main

* fix: honor bundled discovery compat allowlists

* fix: refresh sdk api baseline after latest rebase

* fix: remove stale rebase imports

* test: rename stale model catalog mock

* test: mock renamed doctor runtime modules

* fix: map canonical kimi env auth

* fix: use internal model registry in bench script

* fix: migrate deepinfra provider catalog entry

* fix: enforce builtin tool suppression

* fix: route compaction auth and proxy payloads safely

* refactor: prune unused llm registry leftovers

* test: update codex hooks session import

* test: fix model picker ci coverage

* test: align model picker auth mock types
2026-05-27 19:24:04 +01:00

500 lines
17 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginManifestRecord } from "./manifest-registry.js";
import type { ProviderPlugin } from "./types.js";
const mocks = vi.hoisted(() => ({
loadPluginMetadataSnapshot: vi.fn(),
resolvePluginMetadataSnapshot: vi.fn(),
resolveDiscoveredProviderPluginIds: vi.fn(),
resolvePluginProviders: vi.fn(),
loadSource: vi.fn(),
}));
vi.mock("./plugin-metadata-snapshot.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./plugin-metadata-snapshot.js")>();
return {
...actual,
loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot,
resolvePluginMetadataSnapshot: mocks.resolvePluginMetadataSnapshot,
};
});
vi.mock("./providers.js", () => ({
resolveDiscoveredProviderPluginIds: mocks.resolveDiscoveredProviderPluginIds,
}));
vi.mock("./providers.runtime.js", () => ({
resolvePluginProviders: mocks.resolvePluginProviders,
}));
vi.mock("./source-loader.js", () => ({
createPluginSourceLoader: () => mocks.loadSource,
}));
import { resolvePluginDiscoveryProvidersRuntime } from "./provider-discovery.runtime.js";
function createManifestPlugin(id: string): PluginManifestRecord {
return {
id,
enabledByDefault: true,
channels: [],
providers: [id],
cliBackends: [],
skills: [],
hooks: [],
origin: "bundled",
rootDir: `/tmp/${id}`,
source: "bundled",
manifestPath: `/tmp/${id}/openclaw.plugin.json`,
providerDiscoverySource: `/tmp/${id}/provider-discovery.ts`,
};
}
function createManifestPluginWithModelCatalog(id: string): PluginManifestRecord {
return {
...createManifestPluginWithoutDiscovery({ id }),
modelCatalog: {
providers: {
[id]: {
baseUrl: "https://catalog.example.test/v1",
api: "openai-responses",
models: [
{
id: "catalog-model",
name: "Catalog Model",
reasoning: true,
input: ["text"],
contextWindow: 128000,
maxTokens: 4096,
cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 0 },
},
],
},
},
discovery: { [id]: "static" },
},
};
}
function createManifestPluginWithoutDiscovery(params: {
id: string;
providerAuthEnvVars?: Record<string, string[]>;
setupProviders?: NonNullable<PluginManifestRecord["setup"]>["providers"];
}): PluginManifestRecord {
const { providerDiscoverySource: _providerDiscoverySource, ...plugin } = createManifestPlugin(
params.id,
);
return {
...plugin,
...(params.setupProviders ? { setup: { providers: params.setupProviders } } : {}),
...(params.providerAuthEnvVars ? { providerAuthEnvVars: params.providerAuthEnvVars } : {}),
};
}
function createProvider(params: { id: string; mode: "static" | "catalog" }): ProviderPlugin {
const hook = {
run: async () => ({
provider: {
baseUrl: "https://example.test/v1",
models: [],
},
}),
};
return {
id: params.id,
label: params.id,
auth: [],
...(params.mode === "static" ? { staticCatalog: hook } : { catalog: hook }),
};
}
function requireResolvePluginProvidersParams(index = 0): {
onlyPluginIds?: string[];
} {
const params = (mocks.resolvePluginProviders.mock.calls[index] as [unknown] | undefined)?.[0] as
| {
onlyPluginIds?: string[];
}
| undefined;
if (!params) {
throw new Error(`resolvePluginProviders call ${index} missing`);
}
return params;
}
function requireDiscoveredProviderIdsParams(index = 0): {
registry?: unknown;
manifestRegistry?: unknown;
} {
const params = (
mocks.resolveDiscoveredProviderPluginIds.mock.calls[index] as [unknown] | undefined
)?.[0] as
| {
registry?: unknown;
manifestRegistry?: unknown;
}
| undefined;
if (!params) {
throw new Error(`resolveDiscoveredProviderPluginIds call ${index} missing`);
}
return params;
}
describe("resolvePluginDiscoveryProvidersRuntime", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["deepseek"]);
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: { plugins: [] },
manifestRegistry: {
plugins: [createManifestPlugin("deepseek")],
diagnostics: [],
},
});
mocks.resolvePluginMetadataSnapshot.mockImplementation(
(params?: { pluginMetadataSnapshot?: unknown }) =>
params?.pluginMetadataSnapshot ?? mocks.loadPluginMetadataSnapshot(params),
);
});
it("uses static provider catalog entries without loading the full plugin", () => {
const staticProvider = createProvider({ id: "deepseek", mode: "static" });
mocks.loadSource.mockReturnValue(staticProvider);
expect(resolvePluginDiscoveryProvidersRuntime({})).toEqual([
{ ...staticProvider, pluginId: "deepseek" },
]);
expect(mocks.resolvePluginProviders).not.toHaveBeenCalled();
});
it("keeps unscoped discovery bounded for mixed live and static-only entries", () => {
const codexEntryProvider = createProvider({ id: "codex", mode: "catalog" });
const deepseekEntryProvider = createProvider({ id: "deepseek", mode: "static" });
const fullProviders = [createProvider({ id: "kilocode", mode: "catalog" })];
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue([
"codex",
"deepseek",
"kilocode",
"unused",
]);
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: { plugins: [] },
manifestRegistry: {
plugins: [
createManifestPlugin("codex"),
createManifestPlugin("deepseek"),
createManifestPluginWithoutDiscovery({
id: "kilocode",
providerAuthEnvVars: { kilocode: ["KILOCODE_API_KEY"] },
}),
createManifestPluginWithoutDiscovery({
id: "unused",
providerAuthEnvVars: { unused: ["UNUSED_API_KEY"] },
}),
],
diagnostics: [],
},
});
mocks.loadSource.mockImplementation((modulePath: string) =>
modulePath.includes("/codex/") ? codexEntryProvider : deepseekEntryProvider,
);
mocks.resolvePluginProviders.mockReturnValue(fullProviders);
expect(
resolvePluginDiscoveryProvidersRuntime({
env: { KILOCODE_API_KEY: "sk-test" } as NodeJS.ProcessEnv,
}),
).toEqual([
{ ...codexEntryProvider, pluginId: "codex" },
{ ...deepseekEntryProvider, pluginId: "deepseek" },
...fullProviders,
]);
expect(mocks.resolvePluginProviders).toHaveBeenCalledTimes(1);
const params = requireResolvePluginProvidersParams();
expect(params.onlyPluginIds).toEqual(["kilocode"]);
});
it("falls back to full provider plugins when setup provider env vars are configured", () => {
const codexEntryProvider = createProvider({ id: "codex", mode: "catalog" });
const fullProviders = [createProvider({ id: "kilocode", mode: "catalog" })];
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["codex", "kilocode"]);
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: { plugins: [] },
manifestRegistry: {
plugins: [
createManifestPlugin("codex"),
createManifestPluginWithoutDiscovery({
id: "kilocode",
setupProviders: [{ id: "kilocode", envVars: ["KILOCODE_API_KEY"] }],
}),
],
diagnostics: [],
},
});
mocks.loadSource.mockReturnValue(codexEntryProvider);
mocks.resolvePluginProviders.mockReturnValue(fullProviders);
expect(
resolvePluginDiscoveryProvidersRuntime({
env: { KILOCODE_API_KEY: "sk-test" } as NodeJS.ProcessEnv,
}),
).toEqual([{ ...codexEntryProvider, pluginId: "codex" }, ...fullProviders]);
expect(mocks.resolvePluginProviders).toHaveBeenCalledTimes(1);
const params = requireResolvePluginProvidersParams();
expect(params.onlyPluginIds).toEqual(["kilocode"]);
});
it("shares one metadata snapshot between provider id discovery and entry loading", () => {
const registry = { plugins: [] };
const manifestRegistry = {
plugins: [createManifestPlugin("deepseek")],
diagnostics: [],
};
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: registry,
manifestRegistry,
});
mocks.loadSource.mockReturnValue(createProvider({ id: "deepseek", mode: "catalog" }));
resolvePluginDiscoveryProvidersRuntime({ config: {}, env: {} as NodeJS.ProcessEnv });
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith(
expect.objectContaining({
config: {},
env: {},
}),
);
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce();
expect(mocks.resolveDiscoveredProviderPluginIds).toHaveBeenCalledTimes(1);
const params = requireDiscoveredProviderIdsParams();
expect(params.registry).toBe(registry);
expect(params.manifestRegistry).toBe(manifestRegistry);
});
it("uses a provided plugin metadata snapshot without rebuilding registry metadata", () => {
const registry = { plugins: [] };
const manifestRegistry = {
plugins: [createManifestPlugin("deepseek")],
diagnostics: [],
};
mocks.loadSource.mockReturnValue(createProvider({ id: "deepseek", mode: "catalog" }));
const providers = resolvePluginDiscoveryProvidersRuntime({
config: {},
env: {} as NodeJS.ProcessEnv,
pluginMetadataSnapshot: {
index: registry as never,
manifestRegistry,
},
});
expect(providers).toHaveLength(1);
expect(providers[0]?.id).toBe("deepseek");
expect(providers[0]?.pluginId).toBe("deepseek");
expect(mocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
expect(mocks.resolveDiscoveredProviderPluginIds).toHaveBeenCalledTimes(1);
const params = requireDiscoveredProviderIdsParams();
expect(params.registry).toBe(registry);
expect(params.manifestRegistry).toBe(manifestRegistry);
});
it("returns static-only discovery entries for callers that explicitly request them", () => {
const staticProvider = createProvider({ id: "deepseek", mode: "static" });
mocks.loadSource.mockReturnValue(staticProvider);
const providers = resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true });
expect(providers).toHaveLength(1);
expect(providers[0]?.id).toBe("deepseek");
expect(providers[0]?.pluginId).toBe("deepseek");
expect(providers[0]?.staticCatalog).toBe(staticProvider.staticCatalog);
expect(mocks.resolvePluginProviders).not.toHaveBeenCalled();
});
it("returns manifest model catalogs as static discovery entries", async () => {
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["openai"]);
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: { plugins: [] },
manifestRegistry: {
plugins: [createManifestPluginWithModelCatalog("openai")],
diagnostics: [],
},
});
const providers = resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true });
expect(providers.map((provider) => provider.id)).toEqual(["openai"]);
expect(providers[0]?.pluginId).toBe("openai");
expect(mocks.resolvePluginProviders).not.toHaveBeenCalled();
await expect(
providers[0]?.staticCatalog?.run({
config: {},
env: {},
resolveProviderApiKey: () => ({ apiKey: undefined }),
resolveProviderAuth: () => ({ apiKey: undefined, mode: "none", source: "none" }),
}),
).resolves.toEqual({
providers: {
openai: {
baseUrl: "https://catalog.example.test/v1",
api: "openai-responses",
models: [
expect.objectContaining({
id: "catalog-model",
name: "Catalog Model",
reasoning: true,
}),
],
},
},
});
});
it("defaults missing manifest model costs for static discovery entries", async () => {
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["anthropic"]);
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: { plugins: [] },
manifestRegistry: {
plugins: [
{
...createManifestPluginWithModelCatalog("anthropic"),
modelCatalog: {
providers: {
anthropic: {
baseUrl: "https://api.anthropic.com",
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
reasoning: true,
input: ["text"],
contextWindow: 200000,
maxTokens: 64000,
},
],
},
},
discovery: { anthropic: "static" },
},
},
],
diagnostics: [],
},
});
const providers = resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true });
await expect(
providers[0]?.staticCatalog?.run({
config: {},
env: {},
resolveProviderApiKey: () => ({ apiKey: undefined }),
resolveProviderAuth: () => ({ apiKey: undefined, mode: "none", source: "none" }),
}),
).resolves.toEqual({
providers: {
anthropic: expect.objectContaining({
models: [
expect.objectContaining({
id: "claude-sonnet-4-6",
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
}),
],
}),
},
});
});
it("ignores manifest model catalogs that cannot form valid models.json providers", () => {
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["anthropic"]);
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: { plugins: [] },
manifestRegistry: {
plugins: [
{
...createManifestPluginWithModelCatalog("anthropic"),
modelCatalog: {
providers: {
"claude-cli": {
models: [
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
reasoning: true,
input: ["text"],
contextWindow: 200000,
maxTokens: 64000,
},
],
},
anthropic: {
baseUrl: "https://api.anthropic.com",
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
reasoning: true,
input: ["text"],
contextWindow: 200000,
maxTokens: 64000,
},
],
},
},
discovery: { "claude-cli": "static", anthropic: "static" },
},
},
],
diagnostics: [],
},
});
const providers = resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true });
expect(providers.map((provider) => provider.id)).toEqual(["anthropic"]);
});
it("keeps manifest catalogs and loads only scoped plugins that have no entry", () => {
const dynamicProvider = createProvider({ id: "minimax", mode: "catalog" });
mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["minimax", "openai"]);
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: { plugins: [] },
manifestRegistry: {
plugins: [
createManifestPluginWithoutDiscovery({ id: "minimax" }),
createManifestPluginWithModelCatalog("openai"),
],
diagnostics: [],
},
});
mocks.resolvePluginProviders.mockReturnValue([dynamicProvider]);
const providers = resolvePluginDiscoveryProvidersRuntime({
onlyPluginIds: ["minimax", "openai"],
});
expect(providers.map((provider) => provider.id)).toEqual(["openai", "minimax"]);
expect(mocks.resolvePluginProviders).toHaveBeenCalledTimes(1);
expect(requireResolvePluginProvidersParams().onlyPluginIds).toEqual(["minimax"]);
});
it("does not fall back to full plugin loading when discovery entries are requested only", () => {
mocks.loadPluginMetadataSnapshot.mockReturnValue({
index: { plugins: [] },
manifestRegistry: {
plugins: [createManifestPluginWithoutDiscovery({ id: "deepseek" })],
diagnostics: [],
},
});
expect(resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true })).toStrictEqual(
[],
);
expect(resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true })).toStrictEqual(
[],
);
expect(mocks.resolvePluginProviders).not.toHaveBeenCalled();
});
});