test: dedupe plugin provider runtime suites

This commit is contained in:
Peter Steinberger
2026-03-28 04:02:02 +00:00
parent 708ff9145e
commit e74f206a68
23 changed files with 509 additions and 295 deletions

View File

@@ -29,6 +29,19 @@ function expectGeneratedAuthEnvVarModuleState(params: {
expect(result.wrote).toBe(params.expectedWrote);
}
function expectBundledProviderEnvVars(expected: Record<string, readonly string[]>) {
expect(
Object.fromEntries(
Object.keys(expected).map((providerId) => [
providerId,
BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES[
providerId as keyof typeof BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES
],
]),
),
).toEqual(expected);
}
describe("bundled provider auth env vars", () => {
it("matches the generated manifest snapshot", () => {
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES).toEqual(
@@ -37,25 +50,7 @@ describe("bundled provider auth env vars", () => {
});
it("reads bundled provider auth env vars from plugin manifests", () => {
expect(
Object.fromEntries(
[
["brave", ["BRAVE_API_KEY"]],
["firecrawl", ["FIRECRAWL_API_KEY"]],
["github-copilot", ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]],
["perplexity", ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"]],
["tavily", ["TAVILY_API_KEY"]],
["minimax-portal", ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"]],
["openai", ["OPENAI_API_KEY"]],
["fal", ["FAL_KEY"]],
].map(([providerId]) => [
providerId,
BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES[
providerId as keyof typeof BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES
],
]),
),
).toEqual({
expectBundledProviderEnvVars({
brave: ["BRAVE_API_KEY"],
firecrawl: ["FIRECRAWL_API_KEY"],
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],

View File

@@ -38,6 +38,10 @@ vi.mock("./bundled-compat.js", () => ({
let resolvePluginCapabilityProviders: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProviders;
function expectResolvedCapabilityProviderIds(providers: Array<{ id: string }>, expected: string[]) {
expect(providers.map((provider) => provider.id)).toEqual(expected);
}
function expectBundledCompatLoadPath(params: {
cfg: OpenClawConfig;
allowlistCompat: { plugins: { allow: string[] } };
@@ -100,6 +104,27 @@ function setBundledCapabilityFixture(contractKey: string) {
});
}
function setActiveSpeechCapabilityRegistry(providerId: string) {
const active = createEmptyPluginRegistry();
active.speechProviders.push({
pluginId: providerId,
pluginName: "OpenAI",
source: "test",
provider: {
id: providerId,
label: "OpenAI",
isConfigured: () => true,
synthesize: async () => ({
audioBuffer: Buffer.from("x"),
outputFormat: "mp3",
voiceCompatible: false,
fileExtension: ".mp3",
}),
},
});
setActivePluginRegistry(active);
}
describe("resolvePluginCapabilityProviders", () => {
beforeEach(async () => {
vi.resetModules();
@@ -118,28 +143,11 @@ describe("resolvePluginCapabilityProviders", () => {
});
it("uses the active registry when capability providers are already loaded", () => {
const active = createEmptyPluginRegistry();
active.speechProviders.push({
pluginId: "openai",
pluginName: "OpenAI",
source: "test",
provider: {
id: "openai",
label: "OpenAI",
isConfigured: () => true,
synthesize: async () => ({
audioBuffer: Buffer.from("x"),
outputFormat: "mp3",
voiceCompatible: false,
fileExtension: ".mp3",
}),
},
});
setActivePluginRegistry(active);
setActiveSpeechCapabilityRegistry("openai");
const providers = resolvePluginCapabilityProviders({ key: "speechProviders" });
expect(providers.map((provider) => provider.id)).toEqual(["openai"]);
expectResolvedCapabilityProviderIds(providers, ["openai"]);
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
});

View File

@@ -37,6 +37,10 @@ function createOwnedAdapterEntry(id: string) {
};
}
function expectMemoryEmbeddingProviderIds(expectedIds: readonly string[]) {
expect(listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual([...expectedIds]);
}
afterEach(() => {
clearMemoryEmbeddingProviders();
});
@@ -47,7 +51,7 @@ describe("memory embedding provider registry", () => {
registerMemoryEmbeddingProvider(createAdapter("beta"));
expect(getMemoryEmbeddingProvider("alpha")?.id).toBe("alpha");
expect(listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual(["alpha", "beta"]);
expectMemoryEmbeddingProviderIds(["alpha", "beta"]);
});
it("restores a previous snapshot", () => {
@@ -92,7 +96,7 @@ describe("memory embedding provider registry", () => {
clearMemoryEmbeddingProviders();
expect(listMemoryEmbeddingProviders()).toEqual([]);
expectMemoryEmbeddingProviderIds([]);
});
it("stores adapters in a process-global singleton map", () => {

View File

@@ -11,16 +11,18 @@ const MIN_HOST_REQUIREMENT = {
minimumLabel: "2026.3.22",
};
function expectValidHostCheck(currentVersion: string, minHostVersion?: string) {
expect(checkMinHostVersion({ currentVersion, minHostVersion })).toEqual({
ok: true,
requirement: minHostVersion ? MIN_HOST_REQUIREMENT : null,
});
}
describe("min-host-version", () => {
it("accepts empty metadata", () => {
expect(validateMinHostVersion(undefined)).toBeNull();
expect(parseMinHostVersionRequirement(undefined)).toBeNull();
expect(checkMinHostVersion({ currentVersion: "2026.3.22", minHostVersion: undefined })).toEqual(
{
ok: true,
requirement: null,
},
);
expectValidHostCheck("2026.3.22");
});
it("parses semver floors", () => {
@@ -69,10 +71,7 @@ describe("min-host-version", () => {
it.each(["2026.3.22", "2026.4.0"] as const)(
"accepts equal or newer hosts: %s",
(currentVersion) => {
expect(checkMinHostVersion({ currentVersion, minHostVersion: ">=2026.3.22" })).toEqual({
ok: true,
requirement: MIN_HOST_REQUIREMENT,
});
expectValidHostCheck(currentVersion, ">=2026.3.22");
},
);
});

View File

@@ -13,6 +13,13 @@ import {
resolveManifestProviderOnboardAuthFlags,
} from "./provider-auth-choices.js";
function createManifestPlugin(id: string, providerAuthChoices: Array<Record<string, unknown>>) {
return {
id,
providerAuthChoices,
};
}
function setManifestPlugins(plugins: Array<Record<string, unknown>>) {
loadPluginManifestRegistry.mockReturnValue({
plugins,
@@ -23,24 +30,27 @@ function expectAuthChoice(choiceId: string, providerId: string) {
expect(resolveManifestProviderAuthChoice(choiceId)?.providerId).toBe(providerId);
}
function expectDeprecatedAuthChoice(choiceIds: string[], expectedChoiceId?: string) {
for (const choiceId of choiceIds) {
expect(resolveManifestDeprecatedProviderAuthChoice(choiceId)?.choiceId).toBe(expectedChoiceId);
}
}
describe("provider auth choice manifest helpers", () => {
it("flattens manifest auth choices", () => {
setManifestPlugins([
{
id: "openai",
providerAuthChoices: [
{
provider: "openai",
method: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
onboardingScopes: ["text-inference"],
optionKey: "openaiApiKey",
cliFlag: "--openai-api-key",
cliOption: "--openai-api-key <key>",
},
],
},
createManifestPlugin("openai", [
{
provider: "openai",
method: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
onboardingScopes: ["text-inference"],
optionKey: "openaiApiKey",
cliFlag: "--openai-api-key",
cliOption: "--openai-api-key <key>",
},
]),
]);
expect(resolveManifestProviderAuthChoices()).toEqual([
@@ -63,31 +73,28 @@ describe("provider auth choice manifest helpers", () => {
{
name: "deduplicates flag metadata by option key + flag",
plugins: [
{
id: "moonshot",
providerAuthChoices: [
{
provider: "moonshot",
method: "api-key",
choiceId: "moonshot-api-key",
choiceLabel: "Kimi API key (.ai)",
optionKey: "moonshotApiKey",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
cliDescription: "Moonshot API key",
},
{
provider: "moonshot",
method: "api-key-cn",
choiceId: "moonshot-api-key-cn",
choiceLabel: "Kimi API key (.cn)",
optionKey: "moonshotApiKey",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
cliDescription: "Moonshot API key",
},
],
},
createManifestPlugin("moonshot", [
{
provider: "moonshot",
method: "api-key",
choiceId: "moonshot-api-key",
choiceLabel: "Kimi API key (.ai)",
optionKey: "moonshotApiKey",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
cliDescription: "Moonshot API key",
},
{
provider: "moonshot",
method: "api-key-cn",
choiceId: "moonshot-api-key-cn",
choiceLabel: "Kimi API key (.cn)",
optionKey: "moonshotApiKey",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
cliDescription: "Moonshot API key",
},
]),
],
run: () =>
expect(resolveManifestProviderOnboardAuthFlags()).toEqual([
@@ -103,26 +110,18 @@ describe("provider auth choice manifest helpers", () => {
{
name: "resolves deprecated auth-choice aliases through manifest metadata",
plugins: [
{
id: "minimax",
providerAuthChoices: [
{
provider: "minimax",
method: "api-global",
choiceId: "minimax-global-api",
deprecatedChoiceIds: ["minimax", "minimax-api"],
},
],
},
createManifestPlugin("minimax", [
{
provider: "minimax",
method: "api-global",
choiceId: "minimax-global-api",
deprecatedChoiceIds: ["minimax", "minimax-api"],
},
]),
],
run: () => {
expect(resolveManifestDeprecatedProviderAuthChoice("minimax")?.choiceId).toBe(
"minimax-global-api",
);
expect(resolveManifestDeprecatedProviderAuthChoice("minimax-api")?.choiceId).toBe(
"minimax-global-api",
);
expect(resolveManifestDeprecatedProviderAuthChoice("openai")).toBeUndefined();
expectDeprecatedAuthChoice(["minimax", "minimax-api"], "minimax-global-api");
expectDeprecatedAuthChoice(["openai"]);
},
},
])("$name", ({ plugins, run }) => {

View File

@@ -53,6 +53,47 @@ function expectPairedCatalogProviders(
});
}
function createSingleCatalogProvider(overrides: Partial<ModelProviderConfig> & { apiKey: string }) {
return {
provider: {
...createProviderConfig(overrides),
apiKey: overrides.apiKey,
},
};
}
async function expectSingleCatalogResult(params: {
ctx: ProviderCatalogContext;
allowExplicitBaseUrl?: boolean;
buildProvider?: () => ModelProviderConfig;
expected: Awaited<ReturnType<typeof buildSingleProviderApiKeyCatalog>>;
}) {
const result = await buildSingleProviderApiKeyCatalog({
ctx: params.ctx,
providerId: "test-provider",
buildProvider: params.buildProvider ?? (() => createProviderConfig()),
allowExplicitBaseUrl: params.allowExplicitBaseUrl,
});
expect(result).toEqual(params.expected);
}
async function expectPairedCatalogResult(params: {
ctx: ProviderCatalogContext;
expected: Record<string, ModelProviderConfig & { apiKey: string }>;
}) {
const result = await buildPairedProviderApiKeyCatalog({
ctx: params.ctx,
providerId: "test-provider",
buildProviders: async () => ({
alpha: createProviderConfig(),
beta: createProviderConfig(),
}),
});
expectPairedCatalogProviders(result, params.expected);
}
describe("buildSingleProviderApiKeyCatalog", () => {
it.each([
{
@@ -92,14 +133,9 @@ describe("buildSingleProviderApiKeyCatalog", () => {
apiKeys: { "test-provider": "secret-key" },
}),
undefined,
{
provider: {
api: "openai-completions",
baseUrl: "https://default.example/v1",
models: [],
apiKey: "secret-key",
},
},
createSingleCatalogProvider({
apiKey: "secret-key",
}),
],
[
"prefers explicit base url when allowed",
@@ -117,24 +153,17 @@ describe("buildSingleProviderApiKeyCatalog", () => {
},
}),
true,
{
provider: {
api: "openai-completions",
baseUrl: "https://override.example/v1/",
models: [],
apiKey: "secret-key",
},
},
createSingleCatalogProvider({
baseUrl: "https://override.example/v1/",
apiKey: "secret-key",
}),
],
] as const)("%s", async (_name, ctx, allowExplicitBaseUrl, expected) => {
const result = await buildSingleProviderApiKeyCatalog({
await expectSingleCatalogResult({
ctx,
providerId: "test-provider",
buildProvider: () => createProviderConfig(),
allowExplicitBaseUrl,
expected,
});
expect(result).toEqual(expected);
});
it("matches explicit base url config across canonical provider aliases", async () => {
@@ -166,29 +195,23 @@ describe("buildSingleProviderApiKeyCatalog", () => {
});
it("adds api key to each paired provider", async () => {
const result = await buildPairedProviderApiKeyCatalog({
await expectPairedCatalogResult({
ctx: createCatalogContext({
apiKeys: { "test-provider": "secret-key" },
}),
providerId: "test-provider",
buildProviders: async () => ({
alpha: createProviderConfig(),
beta: createProviderConfig(),
}),
});
expectPairedCatalogProviders(result, {
alpha: {
api: "openai-completions",
baseUrl: "https://default.example/v1",
models: [],
apiKey: "secret-key",
},
beta: {
api: "openai-completions",
baseUrl: "https://default.example/v1",
models: [],
apiKey: "secret-key",
expected: {
alpha: {
api: "openai-completions",
baseUrl: "https://default.example/v1",
models: [],
apiKey: "secret-key",
},
beta: {
api: "openai-completions",
baseUrl: "https://default.example/v1",
models: [],
apiKey: "secret-key",
},
},
});
});

View File

@@ -38,11 +38,13 @@ function expectGroupedProviderIds(
expected: Record<ProviderDiscoveryOrder | "late", readonly string[]>,
) {
const grouped = groupPluginDiscoveryProvidersByOrder([...providers]);
expect(grouped.simple.map((provider) => provider.id)).toEqual(expected.simple);
expect(grouped.profile.map((provider) => provider.id)).toEqual(expected.profile);
expect(grouped.paired.map((provider) => provider.id)).toEqual(expected.paired);
expect(grouped.late.map((provider) => provider.id)).toEqual(expected.late);
const actual = {
simple: grouped.simple.map((provider) => provider.id),
profile: grouped.profile.map((provider) => provider.id),
paired: grouped.paired.map((provider) => provider.id),
late: grouped.late.map((provider) => provider.id),
};
expect(actual).toEqual(expected);
}
function createCatalogRuntimeContext() {

View File

@@ -30,6 +30,18 @@ function createTemplateModel(
} as ProviderRuntimeModel;
}
function expectClonedTemplateModel(
params: Parameters<typeof cloneFirstTemplateModel>[0],
expected: Record<string, unknown> | undefined,
) {
const model = cloneFirstTemplateModel(params);
if (expected == null) {
expect(model).toBeUndefined();
return;
}
expect(model).toMatchObject(expected);
}
describe("cloneFirstTemplateModel", () => {
it.each([
{
@@ -60,12 +72,7 @@ describe("cloneFirstTemplateModel", () => {
expected: undefined,
},
] as const)("$name", ({ params, expected }) => {
const model = cloneFirstTemplateModel(params);
if (expected == null) {
expect(model).toBeUndefined();
return;
}
expect(model).toMatchObject(expected);
expectClonedTemplateModel(params, expected);
});
});

View File

@@ -36,6 +36,15 @@ function expectCatalogPrimaryModel(cfg: OpenClawConfig) {
});
}
function expectProviderModels(
cfg: OpenClawConfig,
providerId: string,
expected: Record<string, unknown>,
) {
const providers = cfg.models?.providers as Record<string, unknown> | undefined;
expect(providers?.[providerId]).toMatchObject(expected);
}
function createDemoProviderParams(params?: {
providerId?: string;
baseUrl?: string;
@@ -119,7 +128,7 @@ describe("provider onboarding preset appliers", () => {
});
const cfg = appliers.applyConfig({}, "https://alt.test/v1");
expect(cfg.models?.providers?.demo).toMatchObject({
expectProviderModels(cfg, "demo", {
baseUrl: "https://alt.test/v1",
models: [
{ id: "a", name: "Model A" },

View File

@@ -60,6 +60,7 @@ const MODEL: ProviderRuntimeModel = {
maxTokens: 8_192,
};
const DEMO_PROVIDER_ID = "demo";
const EMPTY_MODEL_REGISTRY = { find: () => null } as never;
function createOpenAiCatalogProviderPlugin(
overrides: Partial<ProviderPlugin> = {},
@@ -134,6 +135,12 @@ function createDemoResolvedModelContext<TContext extends Record<string, unknown>
});
}
function expectCalledOnce(...mocks: Array<{ mock: { calls: unknown[] } }>) {
for (const mockFn of mocks) {
expect(mockFn).toHaveBeenCalledTimes(1);
}
}
describe("provider-runtime", () => {
beforeEach(async () => {
vi.resetModules();
@@ -307,7 +314,7 @@ describe("provider-runtime", () => {
runProviderDynamicModel({
provider: DEMO_PROVIDER_ID,
context: createDemoRuntimeContext({
modelRegistry: { find: () => null } as never,
modelRegistry: EMPTY_MODEL_REGISTRY,
}),
}),
).toMatchObject(MODEL);
@@ -315,7 +322,7 @@ describe("provider-runtime", () => {
await prepareProviderDynamicModel({
provider: DEMO_PROVIDER_ID,
context: createDemoRuntimeContext({
modelRegistry: { find: () => null } as never,
modelRegistry: EMPTY_MODEL_REGISTRY,
}),
});
@@ -555,13 +562,15 @@ describe("provider-runtime", () => {
expectCodexBuiltInSuppression(resolveProviderBuiltInModelSuppression);
await expectAugmentedCodexCatalog(augmentModelCatalogWithProviderPlugins);
expect(prepareDynamicModel).toHaveBeenCalledTimes(1);
expect(refreshOAuth).toHaveBeenCalledTimes(1);
expect(resolveSyntheticAuth).toHaveBeenCalledTimes(1);
expect(buildUnknownModelHint).toHaveBeenCalledTimes(1);
expect(prepareRuntimeAuth).toHaveBeenCalledTimes(1);
expect(resolveUsageAuth).toHaveBeenCalledTimes(1);
expect(fetchUsageSnapshot).toHaveBeenCalledTimes(1);
expectCalledOnce(
prepareDynamicModel,
refreshOAuth,
resolveSyntheticAuth,
buildUnknownModelHint,
prepareRuntimeAuth,
resolveUsageAuth,
fetchUsageSnapshot,
);
});
it("resolves bundled catalog hooks through provider plugins", async () => {

View File

@@ -48,6 +48,25 @@ function normalizeProviderFixture(provider: ProviderPlugin) {
};
}
function expectNormalizedProviderFixture(params: {
provider: ProviderPlugin;
expectedProvider?: Record<string, unknown>;
expectedDiagnostics?: ReadonlyArray<{ level: PluginDiagnostic["level"]; message: string }>;
expectedDiagnosticText?: readonly string[];
}) {
const result = normalizeProviderFixture(params.provider);
if (params.expectedProvider) {
expect(result.provider).toMatchObject(params.expectedProvider);
}
if (params.expectedDiagnostics) {
expectDiagnosticMessages(result.diagnostics, params.expectedDiagnostics);
}
if (params.expectedDiagnosticText) {
expectDiagnosticText(result.diagnostics, params.expectedDiagnosticText);
}
return result;
}
describe("normalizeRegisteredProvider", () => {
it.each([
{
@@ -168,21 +187,22 @@ describe("normalizeRegisteredProvider", () => {
] as const)(
"$name",
({ provider: inputProvider, expectedProvider, expectedDiagnostics, assert }) => {
const { diagnostics, provider } = normalizeProviderFixture(inputProvider);
const { diagnostics, provider } = expectNormalizedProviderFixture({
provider: inputProvider,
...(expectedProvider ? { expectedProvider } : {}),
...(expectedDiagnostics ? { expectedDiagnostics } : {}),
});
if (assert) {
assert(provider, diagnostics);
return;
}
expect(provider).toMatchObject(expectedProvider);
expectDiagnosticMessages(diagnostics, expectedDiagnostics);
},
);
it("prefers catalog when a provider registers both catalog and discovery", () => {
const { diagnostics, provider } = normalizeProviderFixture(
makeProvider({
const { provider } = expectNormalizedProviderFixture({
provider: makeProvider({
catalog: {
run: async () => null,
},
@@ -195,12 +215,12 @@ describe("normalizeRegisteredProvider", () => {
}),
},
}),
);
expectedDiagnosticText: [
'provider "demo" registered both catalog and discovery; using catalog',
],
});
expect(provider?.catalog).toBeDefined();
expect(provider?.discovery).toBeUndefined();
expectDiagnosticText(diagnostics, [
'provider "demo" registered both catalog and discovery; using catalog',
]);
});
});

View File

@@ -98,7 +98,9 @@ function expectProviderResolutionCall(params?: {
config?: object;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
count?: number;
}) {
expect(resolvePluginProviders).toHaveBeenCalledTimes(params?.count ?? 1);
expect(resolvePluginProviders).toHaveBeenCalledWith({
...createWizardRuntimeParams(params),
bundledProviderAllowlistCompat: true,
@@ -106,6 +108,10 @@ function expectProviderResolutionCall(params?: {
});
}
function setResolvedProviders(...providers: ProviderPlugin[]) {
resolvePluginProviders.mockReturnValue(providers);
}
function resolveWizardOptionsTwice(params: {
config?: object;
env: NodeJS.ProcessEnv;
@@ -118,7 +124,7 @@ function resolveWizardOptionsTwice(params: {
function expectWizardProviderCacheMiss(params: { config?: object; env: NodeJS.ProcessEnv }) {
resolveWizardOptionsTwice(params);
expect(resolvePluginProviders).toHaveBeenCalledTimes(2);
expectProviderResolutionCall({ ...params, count: 2 });
}
function expectSingleWizardChoice(params: {
@@ -127,7 +133,7 @@ function expectSingleWizardChoice(params: {
expectedOption: Record<string, unknown>;
expectedWizard: unknown;
}) {
resolvePluginProviders.mockReturnValue([params.provider]);
setResolvedProviders(params.provider);
expect(resolveProviderWizardOptions({})).toEqual([params.expectedOption]);
expect(
resolveProviderPluginChoice({
@@ -236,7 +242,7 @@ describe("provider wizard boundaries", () => {
},
],
});
resolvePluginProviders.mockReturnValue([provider]);
setResolvedProviders(provider);
expect(resolveProviderWizardOptions({})).toEqual([
{
@@ -299,7 +305,7 @@ describe("provider wizard boundaries", () => {
},
},
});
resolvePluginProviders.mockReturnValue([provider]);
setResolvedProviders(provider);
expect(resolveProviderModelPickerEntries({})).toEqual([
{
@@ -314,13 +320,12 @@ describe("provider wizard boundaries", () => {
const provider = createSglangWizardProvider({ includeModelPicker: true });
const config = {};
const env = createHomeEnv();
resolvePluginProviders.mockReturnValue([provider]);
setResolvedProviders(provider);
const runtimeParams = createWizardRuntimeParams({ config, env });
expect(resolveProviderWizardOptions(runtimeParams)).toHaveLength(1);
expect(resolveProviderModelPickerEntries(runtimeParams)).toHaveLength(1);
expect(resolvePluginProviders).toHaveBeenCalledTimes(1);
expectProviderResolutionCall({ config, env });
});
@@ -328,7 +333,7 @@ describe("provider wizard boundaries", () => {
const provider = createSglangSetupProvider();
const config = createSglangConfig();
const env = createHomeEnv("-a");
resolvePluginProviders.mockReturnValue([provider]);
setResolvedProviders(provider);
expect(resolveProviderWizardOptions(createWizardRuntimeParams({ config, env }))).toHaveLength(
1,
@@ -341,7 +346,7 @@ describe("provider wizard boundaries", () => {
1,
);
expect(resolvePluginProviders).toHaveBeenCalledTimes(2);
expectProviderResolutionCall({ config, env, count: 2 });
});
it.each([
@@ -360,7 +365,7 @@ describe("provider wizard boundaries", () => {
] as const)("$name", ({ env }) => {
const provider = createSglangSetupProvider();
const config = createSglangConfig();
resolvePluginProviders.mockReturnValue([provider]);
setResolvedProviders(provider);
expectWizardProviderCacheMiss({ config, env });
});
@@ -373,7 +378,7 @@ describe("provider wizard boundaries", () => {
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "5",
OPENCLAW_PLUGIN_MANIFEST_CACHE_MS: "20",
});
resolvePluginProviders.mockReturnValue([provider]);
setResolvedProviders(provider);
const runtimeParams = createWizardRuntimeParams({ config, env });
resolveProviderWizardOptions(runtimeParams);
@@ -382,7 +387,7 @@ describe("provider wizard boundaries", () => {
vi.advanceTimersByTime(2);
resolveProviderWizardOptions(runtimeParams);
expect(resolvePluginProviders).toHaveBeenCalledTimes(2);
expectProviderResolutionCall({ config, env, count: 2 });
});
it("invalidates provider-wizard snapshots when cache-control env values change in place", () => {
@@ -391,7 +396,7 @@ describe("provider wizard boundaries", () => {
const env = createHomeEnv("", {
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "1000",
});
resolvePluginProviders.mockReturnValue([provider]);
setResolvedProviders(provider);
resolveProviderWizardOptions(createWizardRuntimeParams({ config, env }));
@@ -399,13 +404,13 @@ describe("provider wizard boundaries", () => {
resolveProviderWizardOptions(createWizardRuntimeParams({ config, env }));
expect(resolvePluginProviders).toHaveBeenCalledTimes(2);
expectProviderResolutionCall({ config, env, count: 2 });
});
it("routes model-selected hooks only to the matching provider", async () => {
const matchingHook = vi.fn(async () => {});
const otherHook = vi.fn(async () => {});
resolvePluginProviders.mockReturnValue([
setResolvedProviders(
makeProvider({
id: "ollama",
label: "Ollama",
@@ -416,7 +421,7 @@ describe("provider wizard boundaries", () => {
label: "vLLM",
onModelSelected: matchingHook,
}),
]);
);
const env = createHomeEnv();
await runProviderModelSelectedHook({

View File

@@ -19,6 +19,13 @@ vi.mock("./manifest-registry.js", () => ({
let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider;
let resolvePluginProviders: typeof import("./providers.runtime.js").resolvePluginProviders;
function setManifestPlugins(plugins: Array<Record<string, unknown>>) {
loadPluginManifestRegistryMock.mockReturnValue({
plugins,
diagnostics: [],
});
}
function getLastLoadPluginsCall(): Record<string, unknown> {
const call = loadOpenClawPluginsMock.mock.calls.at(-1)?.[0];
expect(call).toBeDefined();
@@ -29,6 +36,10 @@ function cloneOptions<T>(value: T): T {
return structuredClone(value);
}
function expectResolvedProviders(providers: unknown, expected: unknown[]) {
expect(providers).toEqual(expected);
}
function expectLastLoadPluginsCall(params?: {
env?: NodeJS.ProcessEnv;
onlyPluginIds?: readonly string[];
@@ -90,6 +101,10 @@ function expectResolvedAllowlistState(params?: {
});
}
function expectOwningPluginIds(provider: string, expectedPluginIds?: string[]) {
expect(resolveOwningPluginIdsForProvider({ provider })).toEqual(expectedPluginIds);
}
describe("resolvePluginProviders", () => {
beforeEach(async () => {
vi.resetModules();
@@ -103,17 +118,14 @@ describe("resolvePluginProviders", () => {
config: params.config,
changes: [],
}));
loadPluginManifestRegistryMock.mockReturnValue({
plugins: [
{ id: "google", providers: ["google"], origin: "bundled" },
{ id: "browser", providers: [], origin: "bundled" },
{ id: "kilocode", providers: ["kilocode"], origin: "bundled" },
{ id: "moonshot", providers: ["moonshot"], origin: "bundled" },
{ id: "google-gemini-cli-auth", providers: [], origin: "bundled" },
{ id: "workspace-provider", providers: ["workspace-provider"], origin: "workspace" },
],
diagnostics: [],
});
setManifestPlugins([
{ id: "google", providers: ["google"], origin: "bundled" },
{ id: "browser", providers: [], origin: "bundled" },
{ id: "kilocode", providers: ["kilocode"], origin: "bundled" },
{ id: "moonshot", providers: ["moonshot"], origin: "bundled" },
{ id: "google-gemini-cli-auth", providers: [], origin: "bundled" },
{ id: "workspace-provider", providers: ["workspace-provider"], origin: "workspace" },
]);
({ resolveOwningPluginIdsForProvider } = await import("./providers.js"));
({ resolvePluginProviders } = await import("./providers.runtime.js"));
});
@@ -126,7 +138,7 @@ describe("resolvePluginProviders", () => {
env,
});
expect(providers).toEqual([{ id: "demo-provider", pluginId: "google" }]);
expectResolvedProviders(providers, [{ id: "demo-provider", pluginId: "google" }]);
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
workspaceDir: "/workspace/explicit",
@@ -267,16 +279,13 @@ describe("resolvePluginProviders", () => {
});
it("maps provider ids to owning plugin ids via manifests", () => {
loadPluginManifestRegistryMock.mockReturnValue({
plugins: [
{ id: "minimax", providers: ["minimax", "minimax-portal"] },
{ id: "openai", providers: ["openai", "openai-codex"] },
],
diagnostics: [],
});
setManifestPlugins([
{ id: "minimax", providers: ["minimax", "minimax-portal"] },
{ id: "openai", providers: ["openai", "openai-codex"] },
]);
expect(resolveOwningPluginIdsForProvider({ provider: "minimax-portal" })).toEqual(["minimax"]);
expect(resolveOwningPluginIdsForProvider({ provider: "openai-codex" })).toEqual(["openai"]);
expect(resolveOwningPluginIdsForProvider({ provider: "gemini-cli" })).toBeUndefined();
expectOwningPluginIds("minimax-portal", ["minimax"]);
expectOwningPluginIds("openai-codex", ["openai"]);
expectOwningPluginIds("gemini-cli");
});
});

View File

@@ -35,17 +35,26 @@ function guardAssertions() {
]);
}
function expectGuardState(params: {
source: string;
type: "required" | "forbidden";
needle: string;
message: string;
}) {
if (params.type === "required") {
expect(params.source, params.message).toContain(params.needle);
return;
}
expect(params.source, params.message).not.toContain(params.needle);
}
describe("runtime live state guardrails", () => {
it.each(guardAssertions())(
"keeps split-runtime state holders on explicit direct globals: $relativePath $type $needle",
({ relativePath, type, needle, message }) => {
const source = readFileSync(resolve(repoRoot, relativePath), "utf8");
if (type === "required") {
expect(source, message).toContain(needle);
} else {
expect(source, message).not.toContain(needle);
}
expectGuardState({ source, type, needle, message });
},
);
});

View File

@@ -34,6 +34,10 @@ function createRegistrySet() {
};
}
function expectActiveChannelRegistry(registry: ReturnType<typeof createEmptyPluginRegistry>) {
expect(getActivePluginChannelRegistry()).toBe(registry);
}
describe("channel registry pinning", () => {
afterEach(() => {
resetPluginRuntimeStateForTest();
@@ -42,7 +46,7 @@ describe("channel registry pinning", () => {
it("returns the active registry when not pinned", () => {
const registry = createEmptyPluginRegistry();
setActivePluginRegistry(registry);
expect(getActivePluginChannelRegistry()).toBe(registry);
expectActiveChannelRegistry(registry);
});
it("preserves pinned channel registry across setActivePluginRegistry calls", () => {
@@ -54,7 +58,7 @@ describe("channel registry pinning", () => {
const replacement = createEmptyPluginRegistry();
setActivePluginRegistry(replacement);
expect(getActivePluginChannelRegistry()).toBe(startup);
expectActiveChannelRegistry(startup);
expect(getActivePluginChannelRegistry()!.channels).toHaveLength(1);
});
@@ -110,15 +114,13 @@ describe("channel registry pinning", () => {
}
setActivePluginRegistry(replacement);
expect(getActivePluginChannelRegistry()).toBe(expectDuringPin ? startup : replacement);
expectActiveChannelRegistry(expectDuringPin ? startup : replacement);
if (pin) {
releasePinnedPluginChannelRegistry(releasePinnedRegistry ? startup : unrelated);
}
expect(getActivePluginChannelRegistry()).toBe(
expectAfterSwap === "second" ? replacement : startup,
);
expectActiveChannelRegistry(expectAfterSwap === "second" ? replacement : startup);
});
it("requireActivePluginChannelRegistry creates a registry when none exists", () => {
@@ -136,6 +138,6 @@ describe("channel registry pinning", () => {
resetPluginRuntimeStateForTest();
setActivePluginRegistry(fresh);
expect(getActivePluginChannelRegistry()).toBe(fresh);
expectActiveChannelRegistry(fresh);
});
});

View File

@@ -31,6 +31,11 @@ function createRuntimeRegistryPair() {
};
}
function expectRegistryVersions(params: { active: number; routes: number }) {
expect(getActivePluginRegistryVersion()).toBe(params.active);
expect(getActivePluginHttpRouteRegistryVersion()).toBe(params.routes);
}
function expectActiveRouteRegistryResolution(params: {
pinnedRegistry: ReturnType<typeof createEmptyPluginRegistry>;
explicitRegistry: ReturnType<typeof createEmptyPluginRegistry>;
@@ -78,8 +83,10 @@ describe("plugin runtime route registry", () => {
pinActivePluginHttpRouteRegistry(repinnedRegistry);
expect(getActivePluginRegistryVersion()).toBe(activeVersionBeforeRepin);
expect(getActivePluginHttpRouteRegistryVersion()).toBe(routeVersionBeforeRepin + 1);
expectRegistryVersions({
active: activeVersionBeforeRepin,
routes: routeVersionBeforeRepin + 1,
});
});
it.each([

View File

@@ -24,20 +24,27 @@ describe("gateway request scope", () => {
});
}
function expectGatewayScope(
runtimeScope: Awaited<ReturnType<typeof importGatewayRequestScopeModule>>,
expected: PluginRuntimeGatewayRequestScope,
) {
expect(runtimeScope.getPluginRuntimeGatewayRequestScope()).toEqual(expected);
}
it("reuses AsyncLocalStorage across reloaded module instances", async () => {
const first = await importGatewayRequestScopeModule();
await first.withPluginRuntimeGatewayRequestScope(TEST_SCOPE, async () => {
vi.resetModules();
const second = await importGatewayRequestScopeModule();
expect(second.getPluginRuntimeGatewayRequestScope()).toEqual(TEST_SCOPE);
expectGatewayScope(second, TEST_SCOPE);
});
});
it("attaches plugin id to the active scope", async () => {
await withTestGatewayScope(async (runtimeScope) => {
await runtimeScope.withPluginRuntimePluginIdScope("voice-call", async () => {
expect(runtimeScope.getPluginRuntimeGatewayRequestScope()).toEqual({
expectGatewayScope(runtimeScope, {
...TEST_SCOPE,
pluginId: "voice-call",
});

View File

@@ -50,6 +50,12 @@ function expectGatewaySubagentRunFailure(
);
}
function expectFunctionKeys(value: Record<string, unknown>, keys: readonly string[]) {
keys.forEach((key) => {
expect(typeof value[key]).toBe("function");
});
}
describe("plugin runtime command execution", () => {
beforeEach(() => {
vi.restoreAllMocks();
@@ -96,10 +102,12 @@ describe("plugin runtime command execution", () => {
{
name: "exposes runtime.mediaUnderstanding helpers and keeps stt as an alias",
assert: (runtime: ReturnType<typeof createPluginRuntime>) => {
expect(typeof runtime.mediaUnderstanding.runFile).toBe("function");
expect(typeof runtime.mediaUnderstanding.describeImageFile).toBe("function");
expect(typeof runtime.mediaUnderstanding.describeImageFileWithModel).toBe("function");
expect(typeof runtime.mediaUnderstanding.describeVideoFile).toBe("function");
expectFunctionKeys(runtime.mediaUnderstanding as Record<string, unknown>, [
"runFile",
"describeImageFile",
"describeImageFileWithModel",
"describeVideoFile",
]);
expect(runtime.mediaUnderstanding.transcribeAudioFile).toBe(
runtime.stt.transcribeAudioFile,
);
@@ -108,15 +116,19 @@ describe("plugin runtime command execution", () => {
{
name: "exposes runtime.imageGeneration helpers",
assert: (runtime: ReturnType<typeof createPluginRuntime>) => {
expect(typeof runtime.imageGeneration.generate).toBe("function");
expect(typeof runtime.imageGeneration.listProviders).toBe("function");
expectFunctionKeys(runtime.imageGeneration as Record<string, unknown>, [
"generate",
"listProviders",
]);
},
},
{
name: "exposes runtime.webSearch helpers",
assert: (runtime: ReturnType<typeof createPluginRuntime>) => {
expect(typeof runtime.webSearch.listProviders).toBe("function");
expect(typeof runtime.webSearch.search).toBe("function");
expectFunctionKeys(runtime.webSearch as Record<string, unknown>, [
"listProviders",
"search",
]);
},
},
{
@@ -126,17 +138,23 @@ describe("plugin runtime command execution", () => {
model: DEFAULT_MODEL,
provider: DEFAULT_PROVIDER,
});
expect(typeof runtime.agent.runEmbeddedPiAgent).toBe("function");
expect(typeof runtime.agent.resolveAgentDir).toBe("function");
expect(typeof runtime.agent.session.resolveSessionFilePath).toBe("function");
expectFunctionKeys(runtime.agent as Record<string, unknown>, [
"runEmbeddedPiAgent",
"resolveAgentDir",
]);
expectFunctionKeys(runtime.agent.session as Record<string, unknown>, [
"resolveSessionFilePath",
]);
},
},
{
name: "exposes runtime.modelAuth with getApiKeyForModel and resolveApiKeyForProvider",
assert: (runtime: ReturnType<typeof createPluginRuntime>) => {
expect(runtime.modelAuth).toBeDefined();
expect(typeof runtime.modelAuth.getApiKeyForModel).toBe("function");
expect(typeof runtime.modelAuth.resolveApiKeyForProvider).toBe("function");
expectFunctionKeys(runtime.modelAuth as Record<string, unknown>, [
"getApiKeyForModel",
"resolveApiKeyForProvider",
]);
},
},
] as const)("$name", ({ assert }) => {

View File

@@ -23,6 +23,13 @@ function buildTelegramTypingParams(
};
}
function expectTelegramPulseCount(
pulse: ReturnType<typeof vi.fn<() => Promise<unknown>>>,
expected: number,
) {
expect(pulse).toHaveBeenCalledTimes(expected);
}
describe("createTelegramTypingLease", () => {
afterEach(() => {
vi.useRealTimers();
@@ -65,11 +72,11 @@ describe("createTelegramTypingLease", () => {
pulse,
});
expect(pulse).toHaveBeenCalledTimes(1);
expectTelegramPulseCount(pulse, 1);
await vi.advanceTimersByTimeAsync(TELEGRAM_TYPING_DEFAULT_INTERVAL_MS - 1);
expect(pulse).toHaveBeenCalledTimes(1);
expectTelegramPulseCount(pulse, 1);
await vi.advanceTimersByTimeAsync(1);
expect(pulse).toHaveBeenCalledTimes(2);
expectTelegramPulseCount(pulse, 2);
lease.stop();
});

View File

@@ -1,5 +1,9 @@
import { expect, vi } from "vitest";
function expectPulseCount(pulse: { mock: { calls: unknown[] } }, expected: number) {
expect(pulse.mock.calls).toHaveLength(expected);
}
export async function expectIndependentTypingLeases<
TParams extends { intervalMs?: number; pulse: (...args: never[]) => Promise<unknown> },
TLease extends { refresh: () => Promise<void>; stop: () => void },
@@ -13,17 +17,17 @@ export async function expectIndependentTypingLeases<
const leaseA = await params.createLease(params.buildParams(pulse));
const leaseB = await params.createLease(params.buildParams(pulse));
expect(pulse).toHaveBeenCalledTimes(2);
expectPulseCount(pulse as unknown as { mock: { calls: unknown[] } }, 2);
await vi.advanceTimersByTimeAsync(2_000);
expect(pulse).toHaveBeenCalledTimes(4);
expectPulseCount(pulse as unknown as { mock: { calls: unknown[] } }, 4);
leaseA.stop();
await vi.advanceTimersByTimeAsync(2_000);
expect(pulse).toHaveBeenCalledTimes(5);
expectPulseCount(pulse as unknown as { mock: { calls: unknown[] } }, 5);
await leaseB.refresh();
expect(pulse).toHaveBeenCalledTimes(6);
expectPulseCount(pulse as unknown as { mock: { calls: unknown[] } }, 6);
leaseB.stop();
}
@@ -41,7 +45,7 @@ export async function expectBackgroundTypingPulseFailuresAreSwallowed<
const lease = await params.createLease(params.buildParams(params.pulse));
await expect(vi.advanceTimersByTimeAsync(2_000)).resolves.toBe(vi);
expect(params.pulse).toHaveBeenCalledTimes(2);
expectPulseCount(params.pulse as unknown as { mock: { calls: unknown[] } }, 2);
lease.stop();
}

View File

@@ -75,9 +75,14 @@ function setSinglePluginLoadResult(
});
}
function expectInspectReport(pluginId: string) {
function expectInspectReport(
pluginId: string,
): NonNullable<ReturnType<typeof buildPluginInspectReport>> {
const inspect = buildPluginInspectReport({ id: pluginId });
expect(inspect).not.toBeNull();
if (!inspect) {
throw new Error(`expected inspect report for ${pluginId}`);
}
return inspect;
}
@@ -100,6 +105,15 @@ function expectNoCompatibilityWarnings() {
expect(buildPluginCompatibilityWarnings()).toEqual([]);
}
function expectCompatibilityOutput(params: { notices?: unknown[]; warnings?: string[] }) {
if (params.notices) {
expect(buildPluginCompatibilityNotices()).toEqual(params.notices);
}
if (params.warnings) {
expect(buildPluginCompatibilityWarnings()).toEqual(params.warnings);
}
}
function expectCapabilityKinds(
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>,
kinds: readonly string[],
@@ -107,6 +121,31 @@ function expectCapabilityKinds(
expect(inspect.capabilities.map((entry) => entry.kind)).toEqual(kinds);
}
function expectInspectShape(
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>,
params: {
shape: string;
capabilityMode: string;
capabilityKinds: readonly string[];
},
) {
expect(inspect.shape).toBe(params.shape);
expect(inspect.capabilityMode).toBe(params.capabilityMode);
expectCapabilityKinds(inspect, params.capabilityKinds);
}
function expectBundleInspectState(
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>,
params: {
bundleCapabilities: readonly string[];
shape: string;
},
) {
expect(inspect.bundleCapabilities).toEqual(params.bundleCapabilities);
expect(inspect.mcpServers).toEqual([]);
expect(inspect.shape).toBe(params.shape);
}
describe("buildPluginStatusReport", () => {
beforeEach(async () => {
vi.resetModules();
@@ -313,15 +352,17 @@ describe("buildPluginStatusReport", () => {
const inspect = buildPluginInspectReport({ id: "google" });
expect(inspect).not.toBeNull();
expect(inspect?.shape).toBe("hybrid-capability");
expect(inspect?.capabilityMode).toBe("hybrid");
expectCapabilityKinds(inspect!, [
"cli-backend",
"text-inference",
"media-understanding",
"image-generation",
"web-search",
]);
expectInspectShape(inspect!, {
shape: "hybrid-capability",
capabilityMode: "hybrid",
capabilityKinds: [
"cli-backend",
"text-inference",
"media-understanding",
"image-generation",
"web-search",
],
});
expect(inspect?.usesLegacyBeforeAgentStart).toBe(true);
expect(inspect?.compatibility).toEqual([
createCompatibilityNotice({ pluginId: "google", code: "legacy-before-agent-start" }),
@@ -378,10 +419,12 @@ describe("buildPluginStatusReport", () => {
const inspect = expectInspectReport("anthropic");
expect(inspect?.shape).toBe("plain-capability");
expect(inspect?.capabilityMode).toBe("plain");
expectCapabilityKinds(inspect!, ["cli-backend"]);
expect(inspect?.capabilities).toEqual([{ kind: "cli-backend", ids: ["claude-cli"] }]);
expectInspectShape(inspect, {
shape: "plain-capability",
capabilityMode: "plain",
capabilityKinds: ["cli-backend"],
});
expect(inspect.capabilities).toEqual([{ kind: "cli-backend", ids: ["claude-cli"] }]);
});
it("builds compatibility warnings for legacy compatibility paths", () => {
@@ -397,10 +440,9 @@ describe("buildPluginStatusReport", () => {
typedHooks: [createTypedHook({ pluginId: "lca", hookName: "before_agent_start" })],
});
expect(buildPluginCompatibilityWarnings()).toEqual([
`lca ${LEGACY_BEFORE_AGENT_START_MESSAGE}`,
`lca ${HOOK_ONLY_MESSAGE}`,
]);
expectCompatibilityOutput({
warnings: [`lca ${LEGACY_BEFORE_AGENT_START_MESSAGE}`, `lca ${HOOK_ONLY_MESSAGE}`],
});
});
it("builds structured compatibility notices with deterministic ordering", () => {
@@ -422,10 +464,12 @@ describe("buildPluginStatusReport", () => {
typedHooks: [createTypedHook({ pluginId: "legacy-only", hookName: "before_agent_start" })],
});
expect(buildPluginCompatibilityNotices()).toEqual([
createCompatibilityNotice({ pluginId: "hook-only", code: "hook-only" }),
createCompatibilityNotice({ pluginId: "legacy-only", code: "legacy-before-agent-start" }),
]);
expectCompatibilityOutput({
notices: [
createCompatibilityNotice({ pluginId: "hook-only", code: "hook-only" }),
createCompatibilityNotice({ pluginId: "legacy-only", code: "legacy-before-agent-start" }),
],
});
});
it("returns no compatibility warnings for modern capability plugins", () => {
@@ -474,9 +518,10 @@ describe("buildPluginStatusReport", () => {
const inspect = expectInspectReport(expectedId);
expect(inspect?.bundleCapabilities).toEqual(expectedBundleCapabilities);
expect(inspect?.mcpServers).toEqual([]);
expect(inspect?.shape).toBe(expectedShape);
expectBundleInspectState(inspect, {
bundleCapabilities: expectedBundleCapabilities,
shape: expectedShape,
});
});
it("formats and summarizes compatibility notices", () => {

View File

@@ -179,6 +179,27 @@ function expectScopedWebSearchCandidates(pluginIds: readonly string[]) {
);
}
function expectSnapshotMemoization(params: {
config: { plugins?: Record<string, unknown> };
env: NodeJS.ProcessEnv;
expectedLoaderCalls: number;
}) {
const runtimeParams = createSnapshotParams({
config: params.config,
env: params.env,
});
const first = resolvePluginWebSearchProviders(runtimeParams);
const second = resolvePluginWebSearchProviders(runtimeParams);
if (params.expectedLoaderCalls === 1) {
expect(second).toBe(first);
} else {
expect(second).not.toBe(first);
}
expectLoaderCallCount(params.expectedLoaderCalls);
}
describe("resolvePluginWebSearchProviders", () => {
beforeAll(async () => {
({ createEmptyPluginRegistry } = await import("./registry.js"));
@@ -270,15 +291,11 @@ describe("resolvePluginWebSearchProviders", () => {
});
it("memoizes snapshot provider resolution for the same config and env", () => {
const config = createBraveAllowConfig();
const env = createWebSearchEnv();
const runtimeParams = createSnapshotParams({ config, env });
const first = resolvePluginWebSearchProviders(runtimeParams);
const second = resolvePluginWebSearchProviders(runtimeParams);
expect(second).toBe(first);
expectLoaderCallCount(1);
expectSnapshotMemoization({
config: createBraveAllowConfig(),
env: createWebSearchEnv(),
expectedLoaderCalls: 1,
});
});
it("invalidates the snapshot cache when config or env contents change in place", () => {
@@ -307,12 +324,11 @@ describe("resolvePluginWebSearchProviders", () => {
},
},
])("$title", ({ env }) => {
const config = createBraveAllowConfig();
resolvePluginWebSearchProviders(createSnapshotParams({ config, env: createWebSearchEnv(env) }));
resolvePluginWebSearchProviders(createSnapshotParams({ config, env: createWebSearchEnv(env) }));
expectLoaderCallCount(2);
expectSnapshotMemoization({
config: createBraveAllowConfig(),
env: createWebSearchEnv(env),
expectedLoaderCalls: 2,
});
});
it("does not leak host Vitest env into an explicit non-Vitest cache key", () => {

View File

@@ -58,6 +58,16 @@ function expectResolvedPluginIds(
expect(providers.map((provider) => provider.pluginId)).toEqual(expectedPluginIds);
}
function expectResolvedPluginIdsExcluding(
providers: ReturnType<typeof resolveBundledPluginWebSearchProviders>,
unexpectedPluginIds: readonly string[],
) {
const pluginIds = providers.map((provider) => provider.pluginId);
for (const pluginId of unexpectedPluginIds) {
expect(pluginIds).not.toContain(pluginId);
}
}
describe("resolveBundledPluginWebSearchProviders", () => {
it(
"returns bundled providers in alphabetical order",
@@ -144,7 +154,7 @@ describe("resolveBundledPluginWebSearchProviders", () => {
},
});
expect(providers.map((provider) => provider.pluginId)).not.toContain("perplexity");
expectResolvedPluginIdsExcluding(providers, ["perplexity"]);
});
it("can resolve bundled providers through the manifest-scoped loader path", () => {