diff --git a/src/plugins/bundle-manifest.test.ts b/src/plugins/bundle-manifest.test.ts index 40bbf85152e..d01a2603a52 100644 --- a/src/plugins/bundle-manifest.test.ts +++ b/src/plugins/bundle-manifest.test.ts @@ -22,6 +22,15 @@ function makeTempDir() { const mkdirSafe = mkdirSafeDir; +function expectLoadedManifest(rootDir: string, bundleFormat: "codex" | "claude" | "cursor") { + const result = loadBundleManifest({ rootDir, bundleFormat }); + expect(result.ok).toBe(true); + if (!result.ok) { + throw new Error("expected bundle manifest to load"); + } + return result.manifest; +} + afterEach(() => { cleanupTrackedTempDirs(tempDirs); }); @@ -55,12 +64,7 @@ describe("bundle manifest parsing", () => { ); expect(detectBundleManifestFormat(rootDir)).toBe("codex"); - const result = loadBundleManifest({ rootDir, bundleFormat: "codex" }); - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } - expect(result.manifest).toMatchObject({ + expect(expectLoadedManifest(rootDir, "codex")).toMatchObject({ id: "sample-bundle", name: "Sample Bundle", description: "Codex fixture", @@ -101,12 +105,7 @@ describe("bundle manifest parsing", () => { ); expect(detectBundleManifestFormat(rootDir)).toBe("claude"); - const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } - expect(result.manifest).toMatchObject({ + expect(expectLoadedManifest(rootDir, "claude")).toMatchObject({ id: "claude-sample", name: "Claude Sample", description: "Claude fixture", @@ -147,12 +146,7 @@ describe("bundle manifest parsing", () => { fs.writeFileSync(path.join(rootDir, ".mcp.json"), '{"servers":{}}', "utf-8"); expect(detectBundleManifestFormat(rootDir)).toBe("cursor"); - const result = loadBundleManifest({ rootDir, bundleFormat: "cursor" }); - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } - expect(result.manifest).toMatchObject({ + expect(expectLoadedManifest(rootDir, "cursor")).toMatchObject({ id: "cursor-sample", name: "Cursor Sample", description: "Cursor fixture", @@ -177,82 +171,69 @@ describe("bundle manifest parsing", () => { fs.writeFileSync(path.join(rootDir, "settings.json"), '{"hideThinkingBlock":true}', "utf-8"); expect(detectBundleManifestFormat(rootDir)).toBe("claude"); - const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } - - expect(result.manifest.id).toBe(path.basename(rootDir).toLowerCase()); - expect(result.manifest.skills).toEqual(["skills", "commands"]); - expect(result.manifest.settingsFiles).toEqual(["settings.json"]); - expect(result.manifest.capabilities).toEqual( + const manifest = expectLoadedManifest(rootDir, "claude"); + expect(manifest.id).toBe(path.basename(rootDir).toLowerCase()); + expect(manifest.skills).toEqual(["skills", "commands"]); + expect(manifest.settingsFiles).toEqual(["settings.json"]); + expect(manifest.capabilities).toEqual( expect.arrayContaining(["skills", "commands", "settings"]), ); }); - it("resolves Claude bundle hooks from default and declared paths", () => { + it.each([ + { + name: "resolves Claude bundle hooks from default and declared paths", + setupKind: "default-hooks", + expectedHooks: ["hooks/hooks.json"], + hasHooksCapability: true, + }, + { + name: "resolves Claude bundle hooks from manifest-declared paths only", + setupKind: "custom-hooks", + expectedHooks: ["custom-hooks"], + hasHooksCapability: true, + }, + { + name: "returns empty hooks for Claude bundles with no hooks directory", + setupKind: "no-hooks", + expectedHooks: [], + hasHooksCapability: false, + }, + ] as const)("$name", ({ setupKind, expectedHooks, hasHooksCapability }) => { const rootDir = makeTempDir(); mkdirSafe(path.join(rootDir, ".claude-plugin")); - mkdirSafe(path.join(rootDir, "hooks")); - fs.writeFileSync(path.join(rootDir, "hooks", "hooks.json"), '{"hooks":[]}', "utf-8"); - fs.writeFileSync( - path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH), - JSON.stringify({ - name: "Hook Plugin", - description: "Claude hooks fixture", - }), - "utf-8", - ); - - const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); - expect(result.ok).toBe(true); - if (!result.ok) { - return; + if (setupKind === "default-hooks") { + mkdirSafe(path.join(rootDir, "hooks")); + fs.writeFileSync(path.join(rootDir, "hooks", "hooks.json"), '{"hooks":[]}', "utf-8"); + fs.writeFileSync( + path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH), + JSON.stringify({ + name: "Hook Plugin", + description: "Claude hooks fixture", + }), + "utf-8", + ); + } else if (setupKind === "custom-hooks") { + mkdirSafe(path.join(rootDir, "custom-hooks")); + fs.writeFileSync( + path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH), + JSON.stringify({ + name: "Custom Hook Plugin", + hooks: "custom-hooks", + }), + "utf-8", + ); + } else { + mkdirSafe(path.join(rootDir, "skills")); + fs.writeFileSync( + path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH), + JSON.stringify({ name: "No Hooks" }), + "utf-8", + ); } - expect(result.manifest.hooks).toEqual(["hooks/hooks.json"]); - expect(result.manifest.capabilities).toContain("hooks"); - }); - - it("resolves Claude bundle hooks from manifest-declared paths only", () => { - const rootDir = makeTempDir(); - mkdirSafe(path.join(rootDir, ".claude-plugin")); - mkdirSafe(path.join(rootDir, "custom-hooks")); - fs.writeFileSync( - path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH), - JSON.stringify({ - name: "Custom Hook Plugin", - hooks: "custom-hooks", - }), - "utf-8", - ); - - const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } - expect(result.manifest.hooks).toEqual(["custom-hooks"]); - expect(result.manifest.capabilities).toContain("hooks"); - }); - - it("returns empty hooks for Claude bundles with no hooks directory", () => { - const rootDir = makeTempDir(); - mkdirSafe(path.join(rootDir, ".claude-plugin")); - mkdirSafe(path.join(rootDir, "skills")); - fs.writeFileSync( - path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH), - JSON.stringify({ name: "No Hooks" }), - "utf-8", - ); - - const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } - expect(result.manifest.hooks).toEqual([]); - expect(result.manifest.capabilities).not.toContain("hooks"); + const manifest = expectLoadedManifest(rootDir, "claude"); + expect(manifest.hooks).toEqual(expectedHooks); + expect(manifest.capabilities.includes("hooks")).toBe(hasHooksCapability); }); it("does not misclassify native index plugins as manifestless Claude bundles", () => { diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index 3d11078f035..e1b764b705e 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -20,6 +20,14 @@ const BUNDLED_PLUGIN_METADATA_TEST_TIMEOUT_MS = 300_000; installGeneratedPluginTempRootCleanup(); +function expectTestOnlyArtifactsExcluded(artifacts: readonly string[]) { + for (const artifact of artifacts) { + expect(artifact).not.toMatch(/^test-/); + expect(artifact).not.toContain(".test-"); + expect(artifact).not.toMatch(/\.test\.js$/); + } +} + describe("bundled plugin metadata", () => { it( "matches the generated metadata snapshot", @@ -49,13 +57,9 @@ describe("bundled plugin metadata", () => { }); it("excludes test-only public surface artifacts", () => { - for (const entry of BUNDLED_PLUGIN_METADATA) { - for (const artifact of entry.publicSurfaceArtifacts ?? []) { - expect(artifact).not.toMatch(/^test-/); - expect(artifact).not.toContain(".test-"); - expect(artifact).not.toMatch(/\.test\.js$/); - } - } + BUNDLED_PLUGIN_METADATA.forEach((entry) => + expectTestOnlyArtifactsExcluded(entry.publicSurfaceArtifacts ?? []), + ); }); it("prefers built generated paths when present and falls back to source paths", () => { diff --git a/src/plugins/bundled-provider-auth-env-vars.test.ts b/src/plugins/bundled-provider-auth-env-vars.test.ts index ff87f606ab9..b41cd5c5c9d 100644 --- a/src/plugins/bundled-provider-auth-env-vars.test.ts +++ b/src/plugins/bundled-provider-auth-env-vars.test.ts @@ -23,24 +23,34 @@ describe("bundled provider auth env vars", () => { }); it("reads bundled provider auth env vars from plugin manifests", () => { - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.brave).toEqual(["BRAVE_API_KEY"]); - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.firecrawl).toEqual(["FIRECRAWL_API_KEY"]); - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["github-copilot"]).toEqual([ - "COPILOT_GITHUB_TOKEN", - "GH_TOKEN", - "GITHUB_TOKEN", - ]); - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.perplexity).toEqual([ - "PERPLEXITY_API_KEY", - "OPENROUTER_API_KEY", - ]); - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.tavily).toEqual(["TAVILY_API_KEY"]); - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["minimax-portal"]).toEqual([ - "MINIMAX_OAUTH_TOKEN", - "MINIMAX_API_KEY", - ]); - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.openai).toEqual(["OPENAI_API_KEY"]); - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.fal).toEqual(["FAL_KEY"]); + 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({ + 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"], + }); expect("openai-codex" in BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES).toBe(false); }); diff --git a/src/plugins/provider-auth-choices.test.ts b/src/plugins/provider-auth-choices.test.ts index 9cc4ec4951a..e8597ae4f52 100644 --- a/src/plugins/provider-auth-choices.test.ts +++ b/src/plugins/provider-auth-choices.test.ts @@ -51,8 +51,9 @@ describe("provider auth choice manifest helpers", () => { expect(resolveManifestProviderAuthChoice("openai-api-key")?.providerId).toBe("openai"); }); - it("deduplicates flag metadata by option key + flag", () => { - loadPluginManifestRegistry.mockReturnValue({ + it.each([ + { + name: "deduplicates flag metadata by option key + flag", plugins: [ { id: "moonshot", @@ -80,21 +81,19 @@ describe("provider auth choice manifest helpers", () => { ], }, ], - }); - - expect(resolveManifestProviderOnboardAuthFlags()).toEqual([ - { - optionKey: "moonshotApiKey", - authChoice: "moonshot-api-key", - cliFlag: "--moonshot-api-key", - cliOption: "--moonshot-api-key ", - description: "Moonshot API key", - }, - ]); - }); - - it("resolves deprecated auth-choice aliases through manifest metadata", () => { - loadPluginManifestRegistry.mockReturnValue({ + run: () => + expect(resolveManifestProviderOnboardAuthFlags()).toEqual([ + { + optionKey: "moonshotApiKey", + authChoice: "moonshot-api-key", + cliFlag: "--moonshot-api-key", + cliOption: "--moonshot-api-key ", + description: "Moonshot API key", + }, + ]), + }, + { + name: "resolves deprecated auth-choice aliases through manifest metadata", plugins: [ { id: "minimax", @@ -108,14 +107,20 @@ describe("provider auth choice manifest helpers", () => { ], }, ], + run: () => { + expect(resolveManifestDeprecatedProviderAuthChoice("minimax")?.choiceId).toBe( + "minimax-global-api", + ); + expect(resolveManifestDeprecatedProviderAuthChoice("minimax-api")?.choiceId).toBe( + "minimax-global-api", + ); + expect(resolveManifestDeprecatedProviderAuthChoice("openai")).toBeUndefined(); + }, + }, + ])("$name", ({ plugins, run }) => { + loadPluginManifestRegistry.mockReturnValue({ + plugins, }); - - expect(resolveManifestDeprecatedProviderAuthChoice("minimax")?.choiceId).toBe( - "minimax-global-api", - ); - expect(resolveManifestDeprecatedProviderAuthChoice("minimax-api")?.choiceId).toBe( - "minimax-global-api", - ); - expect(resolveManifestDeprecatedProviderAuthChoice("openai")).toBeUndefined(); + run(); }); }); diff --git a/src/plugins/provider-onboarding-config.test.ts b/src/plugins/provider-onboarding-config.test.ts index 5c280c3075e..4e674805caa 100644 --- a/src/plugins/provider-onboarding-config.test.ts +++ b/src/plugins/provider-onboarding-config.test.ts @@ -23,59 +23,74 @@ function createModel(id: string, name: string): ModelDefinitionConfig { }; } describe("provider onboarding preset appliers", () => { - it("creates provider and primary-model appliers for a default model preset", () => { - const appliers = createDefaultModelPresetAppliers({ - primaryModelRef: "demo/demo-default", - resolveParams: () => ({ - providerId: "demo", - api: "openai-completions" as const, - baseUrl: "https://demo.test/v1", - defaultModel: createModel("demo-default", "Demo Default"), - defaultModelId: "demo-default", - aliases: [{ modelRef: "demo/demo-default", alias: "Demo" }], - }), - }); + it.each([ + { + name: "creates provider and primary-model appliers for a default model preset", + kind: "default-model", + }, + { + name: "passes variant args through default-models resolvers", + kind: "default-models", + }, + { + name: "creates model-catalog appliers that preserve existing aliases", + kind: "catalog-models", + }, + ] as const)("$name", ({ kind }) => { + if (kind === "default-model") { + const appliers = createDefaultModelPresetAppliers({ + primaryModelRef: "demo/demo-default", + resolveParams: () => ({ + providerId: "demo", + api: "openai-completions" as const, + baseUrl: "https://demo.test/v1", + defaultModel: createModel("demo-default", "Demo Default"), + defaultModelId: "demo-default", + aliases: [{ modelRef: "demo/demo-default", alias: "Demo" }], + }), + }); - const providerOnly = appliers.applyProviderConfig({}); - expect(providerOnly.agents?.defaults?.models).toMatchObject({ - "demo/demo-default": { - alias: "Demo", - }, - }); - expect(providerOnly.agents?.defaults?.model).toBeUndefined(); + const providerOnly = appliers.applyProviderConfig({}); + expect(providerOnly.agents?.defaults?.models).toMatchObject({ + "demo/demo-default": { + alias: "Demo", + }, + }); + expect(providerOnly.agents?.defaults?.model).toBeUndefined(); - const withPrimary = appliers.applyConfig({}); - expect(withPrimary.agents?.defaults?.model).toEqual({ - primary: "demo/demo-default", - }); - }); + const withPrimary = appliers.applyConfig({}); + expect(withPrimary.agents?.defaults?.model).toEqual({ + primary: "demo/demo-default", + }); + return; + } - it("passes variant args through default-models resolvers", () => { - const appliers = createDefaultModelsPresetAppliers<[string]>({ - primaryModelRef: "demo/a", - resolveParams: (_cfg, baseUrl) => ({ - providerId: "demo", - api: "openai-completions" as const, - baseUrl, - defaultModels: [createModel("a", "Model A"), createModel("b", "Model B")], - aliases: [{ modelRef: "demo/a", alias: "Demo A" }], - }), - }); + if (kind === "default-models") { + const appliers = createDefaultModelsPresetAppliers<[string]>({ + primaryModelRef: "demo/a", + resolveParams: (_cfg, baseUrl) => ({ + providerId: "demo", + api: "openai-completions" as const, + baseUrl, + defaultModels: [createModel("a", "Model A"), createModel("b", "Model B")], + aliases: [{ modelRef: "demo/a", alias: "Demo A" }], + }), + }); - const cfg = appliers.applyConfig({}, "https://alt.test/v1"); - expect(cfg.models?.providers?.demo).toMatchObject({ - baseUrl: "https://alt.test/v1", - models: [ - { id: "a", name: "Model A" }, - { id: "b", name: "Model B" }, - ], - }); - expect(cfg.agents?.defaults?.model).toEqual({ - primary: "demo/a", - }); - }); + const cfg = appliers.applyConfig({}, "https://alt.test/v1"); + expect(cfg.models?.providers?.demo).toMatchObject({ + baseUrl: "https://alt.test/v1", + models: [ + { id: "a", name: "Model A" }, + { id: "b", name: "Model B" }, + ], + }); + expect(cfg.agents?.defaults?.model).toEqual({ + primary: "demo/a", + }); + return; + } - it("creates model-catalog appliers that preserve existing aliases", () => { const appliers = createModelCatalogPresetAppliers({ primaryModelRef: "catalog/default", resolveParams: () => ({ diff --git a/src/plugins/provider-wizard.test.ts b/src/plugins/provider-wizard.test.ts index e2626185446..e6cb127b7a8 100644 --- a/src/plugins/provider-wizard.test.ts +++ b/src/plugins/provider-wizard.test.ts @@ -60,92 +60,98 @@ function resolveWizardOptionsTwice(params: { }); } +function expectSingleWizardChoice(params: { + provider: ProviderPlugin; + choice: string; + expectedOption: Record; + expectedWizard: unknown; +}) { + resolvePluginProviders.mockReturnValue([params.provider]); + expect(resolveProviderWizardOptions({})).toEqual([params.expectedOption]); + expect( + resolveProviderPluginChoice({ + providers: [params.provider], + choice: params.choice, + }), + ).toEqual({ + provider: params.provider, + method: params.provider.auth[0], + wizard: params.expectedWizard, + }); +} + describe("provider wizard boundaries", () => { beforeEach(() => { vi.clearAllMocks(); vi.useRealTimers(); }); - it("uses explicit setup 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: { - setup: { - choiceId: "self-hosted-vllm", - methodId: "local", - choiceLabel: "vLLM local", - groupId: "local-runtimes", - groupLabel: "Local runtimes", + it.each([ + { + name: "uses explicit setup choice ids and bound method ids", + 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: { + setup: { + choiceId: "self-hosted-vllm", + methodId: "local", + choiceLabel: "vLLM local", + groupId: "local-runtimes", + groupLabel: "Local runtimes", + }, }, - }, - }); - resolvePluginProviders.mockReturnValue([provider]); - - expect(resolveProviderWizardOptions({})).toEqual([ - { + }), + choice: "self-hosted-vllm", + expectedOption: { 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], - wizard: provider.wizard?.setup, - }); - }); - - it("builds wizard options from method-level metadata", () => { - const provider = makeProvider({ - id: "openai", - label: "OpenAI", - auth: [ - { - id: "api-key", - label: "OpenAI API key", - kind: "api_key", - wizard: { - choiceId: "openai-api-key", - choiceLabel: "OpenAI API key", - groupId: "openai", - groupLabel: "OpenAI", - onboardingScopes: ["text-inference"], + resolveWizard: (provider: ProviderPlugin) => provider.wizard?.setup, + }, + { + name: "builds wizard options from method-level metadata", + provider: makeProvider({ + id: "openai", + label: "OpenAI", + auth: [ + { + id: "api-key", + label: "OpenAI API key", + kind: "api_key", + wizard: { + choiceId: "openai-api-key", + choiceLabel: "OpenAI API key", + groupId: "openai", + groupLabel: "OpenAI", + onboardingScopes: ["text-inference"], + }, + run: vi.fn(), }, - run: vi.fn(), - }, - ], - }); - resolvePluginProviders.mockReturnValue([provider]); - - expect(resolveProviderWizardOptions({})).toEqual([ - { + ], + }), + choice: "openai-api-key", + expectedOption: { value: "openai-api-key", label: "OpenAI API key", groupId: "openai", groupLabel: "OpenAI", onboardingScopes: ["text-inference"], }, - ]); - expect( - resolveProviderPluginChoice({ - providers: [provider], - choice: "openai-api-key", - }), - ).toEqual({ + resolveWizard: (provider: ProviderPlugin) => provider.auth[0]?.wizard, + }, + ] as const)("$name", ({ provider, choice, expectedOption, resolveWizard }) => { + expectSingleWizardChoice({ provider, - method: provider.auth[0], - wizard: provider.auth[0]?.wizard, + choice, + expectedOption, + expectedWizard: resolveWizard(provider), }); }); @@ -317,27 +323,24 @@ describe("provider wizard boundaries", () => { expect(resolvePluginProviders).toHaveBeenCalledTimes(2); }); - it("skips provider-wizard memoization when plugin cache opt-outs are set", () => { + it.each([ + { + name: "skips provider-wizard memoization when plugin cache opt-outs are set", + env: { + OPENCLAW_HOME: "/tmp/openclaw-home", + OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1", + } as NodeJS.ProcessEnv, + }, + { + name: "skips provider-wizard memoization when discovery cache ttl is zero", + env: { + OPENCLAW_HOME: "/tmp/openclaw-home", + OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "0", + } as NodeJS.ProcessEnv, + }, + ] as const)("$name", ({ env }) => { const provider = createSglangSetupProvider(); const config = createSglangConfig(); - const env = { - OPENCLAW_HOME: "/tmp/openclaw-home", - OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1", - } as NodeJS.ProcessEnv; - resolvePluginProviders.mockReturnValue([provider]); - - resolveWizardOptionsTwice({ config, env }); - - expect(resolvePluginProviders).toHaveBeenCalledTimes(2); - }); - - it("skips provider-wizard memoization when discovery cache ttl is zero", () => { - const provider = createSglangSetupProvider(); - const config = createSglangConfig(); - const env = { - OPENCLAW_HOME: "/tmp/openclaw-home", - OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "0", - } as NodeJS.ProcessEnv; resolvePluginProviders.mockReturnValue([provider]); resolveWizardOptionsTwice({ config, env });