diff --git a/src/agents/auth-profiles/oauth.test.ts b/src/agents/auth-profiles/oauth.test.ts index e19986cd3e1..c649e8fcc31 100644 --- a/src/agents/auth-profiles/oauth.test.ts +++ b/src/agents/auth-profiles/oauth.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import type { AuthProfileStore } from "./types.js"; @@ -16,8 +16,7 @@ vi.mock("../../plugins/provider-runtime.runtime.js", () => ({ let resolveApiKeyForProfile: typeof import("./oauth.js").resolveApiKeyForProfile; -async function loadFreshOAuthModuleForTest() { - vi.resetModules(); +async function loadOAuthModuleForTest() { ({ resolveApiKeyForProfile } = await import("./oauth.js")); } @@ -110,11 +109,9 @@ async function expectResolvedApiKey(params: { }); } -describe("resolveApiKeyForProfile config compatibility", () => { - beforeEach(async () => { - await loadFreshOAuthModuleForTest(); - }); +beforeAll(loadOAuthModuleForTest); +describe("resolveApiKeyForProfile config compatibility", () => { it("accepts token credentials when config mode is oauth", async () => { const profileId = "anthropic:token"; const store: AuthProfileStore = { @@ -201,10 +198,6 @@ describe("resolveApiKeyForProfile config compatibility", () => { }); describe("resolveApiKeyForProfile token expiry handling", () => { - beforeEach(async () => { - await loadFreshOAuthModuleForTest(); - }); - it("accepts token credentials when expires is undefined", async () => { const profileId = "anthropic:token-no-expiry"; const result = await resolveWithConfig({ @@ -301,10 +294,6 @@ describe("resolveApiKeyForProfile token expiry handling", () => { }); describe("resolveApiKeyForProfile secret refs", () => { - beforeEach(async () => { - await loadFreshOAuthModuleForTest(); - }); - it("resolves api_key keyRef from env", async () => { const profileId = "openai:default"; const previous = process.env.OPENAI_API_KEY; diff --git a/src/agents/live-auth-keys.test.ts b/src/agents/live-auth-keys.test.ts index acd2c1b70a3..5dc9e962283 100644 --- a/src/agents/live-auth-keys.test.ts +++ b/src/agents/live-auth-keys.test.ts @@ -1,24 +1,32 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const ORIGINAL_MODELSTUDIO_API_KEY = process.env.MODELSTUDIO_API_KEY; const ORIGINAL_XAI_API_KEY = process.env.XAI_API_KEY; +let collectProviderApiKeys: typeof import("./live-auth-keys.js").collectProviderApiKeys; +let clearPluginManifestRegistryCache: typeof import("../plugins/manifest-registry.js").clearPluginManifestRegistryCache; -async function clearManifestRegistryCache(): Promise { - const { clearPluginManifestRegistryCache } = await import("../plugins/manifest-registry.js"); +async function loadModulesForTest(): Promise { + ({ clearPluginManifestRegistryCache } = await import("../plugins/manifest-registry.js")); + ({ collectProviderApiKeys } = await import("./live-auth-keys.js")); +} + +function clearManifestRegistryCache(): void { clearPluginManifestRegistryCache(); } describe("collectProviderApiKeys", () => { - beforeEach(async () => { - vi.resetModules(); + beforeAll(async () => { vi.doUnmock("../plugins/manifest-registry.js"); vi.doUnmock("../secrets/provider-env-vars.js"); - await clearManifestRegistryCache(); + await loadModulesForTest(); }); - afterEach(async () => { - vi.resetModules(); - await clearManifestRegistryCache(); + beforeEach(() => { + clearManifestRegistryCache(); + }); + + afterEach(() => { + clearManifestRegistryCache(); if (ORIGINAL_MODELSTUDIO_API_KEY === undefined) { delete process.env.MODELSTUDIO_API_KEY; } else { @@ -33,16 +41,12 @@ describe("collectProviderApiKeys", () => { it("honors manifest-declared provider auth env vars for nonstandard provider ids", async () => { process.env.MODELSTUDIO_API_KEY = "modelstudio-live-key"; - vi.resetModules(); - const { collectProviderApiKeys } = await import("./live-auth-keys.js"); expect(collectProviderApiKeys("alibaba")).toContain("modelstudio-live-key"); }); it("dedupes manifest env vars against direct provider env naming", async () => { process.env.XAI_API_KEY = "xai-live-key"; - vi.resetModules(); - const { collectProviderApiKeys } = await import("./live-auth-keys.js"); expect(collectProviderApiKeys("xai")).toEqual(["xai-live-key"]); }); diff --git a/src/agents/model-auth-markers.test.ts b/src/agents/model-auth-markers.test.ts index aadf4108fcb..ea01f13deb5 100644 --- a/src/agents/model-auth-markers.test.ts +++ b/src/agents/model-auth-markers.test.ts @@ -1,60 +1,58 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, describe, expect, it, vi } from "vitest"; + +let listKnownProviderEnvApiKeyNames: typeof import("./model-auth-env-vars.js").listKnownProviderEnvApiKeyNames; +let GCP_VERTEX_CREDENTIALS_MARKER: typeof import("./model-auth-markers.js").GCP_VERTEX_CREDENTIALS_MARKER; +let NON_ENV_SECRETREF_MARKER: typeof import("./model-auth-markers.js").NON_ENV_SECRETREF_MARKER; +let isKnownEnvApiKeyMarker: typeof import("./model-auth-markers.js").isKnownEnvApiKeyMarker; +let isNonSecretApiKeyMarker: typeof import("./model-auth-markers.js").isNonSecretApiKeyMarker; +let resolveOAuthApiKeyMarker: typeof import("./model-auth-markers.js").resolveOAuthApiKeyMarker; async function loadMarkerModules() { vi.doUnmock("../plugins/manifest-registry.js"); vi.doUnmock("../secrets/provider-env-vars.js"); vi.resetModules(); - return Promise.all([import("./model-auth-env-vars.js"), import("./model-auth-markers.js")]); + const [envVarsModule, markersModule] = await Promise.all([ + import("./model-auth-env-vars.js"), + import("./model-auth-markers.js"), + ]); + listKnownProviderEnvApiKeyNames = envVarsModule.listKnownProviderEnvApiKeyNames; + GCP_VERTEX_CREDENTIALS_MARKER = markersModule.GCP_VERTEX_CREDENTIALS_MARKER; + NON_ENV_SECRETREF_MARKER = markersModule.NON_ENV_SECRETREF_MARKER; + isKnownEnvApiKeyMarker = markersModule.isKnownEnvApiKeyMarker; + isNonSecretApiKeyMarker = markersModule.isNonSecretApiKeyMarker; + resolveOAuthApiKeyMarker = markersModule.resolveOAuthApiKeyMarker; } -beforeEach(() => { - vi.doUnmock("../plugins/manifest-registry.js"); - vi.doUnmock("../secrets/provider-env-vars.js"); -}); +beforeAll(loadMarkerModules); describe("model auth markers", () => { - it("recognizes explicit non-secret markers", async () => { - const [ - , - { - GCP_VERTEX_CREDENTIALS_MARKER, - NON_ENV_SECRETREF_MARKER, - isNonSecretApiKeyMarker, - resolveOAuthApiKeyMarker, - }, - ] = await loadMarkerModules(); + it("recognizes explicit non-secret markers", () => { expect(isNonSecretApiKeyMarker(NON_ENV_SECRETREF_MARKER)).toBe(true); expect(isNonSecretApiKeyMarker(resolveOAuthApiKeyMarker("chutes"))).toBe(true); expect(isNonSecretApiKeyMarker("ollama-local")).toBe(true); expect(isNonSecretApiKeyMarker(GCP_VERTEX_CREDENTIALS_MARKER)).toBe(true); }); - it("does not treat removed provider markers as active auth markers", async () => { - const [, { isNonSecretApiKeyMarker }] = await loadMarkerModules(); + it("does not treat removed provider markers as active auth markers", () => { expect(isNonSecretApiKeyMarker("qwen-oauth")).toBe(false); }); - it("recognizes known env marker names but not arbitrary all-caps keys", async () => { - const [, { isNonSecretApiKeyMarker }] = await loadMarkerModules(); + it("recognizes known env marker names but not arbitrary all-caps keys", () => { expect(isNonSecretApiKeyMarker("OPENAI_API_KEY")).toBe(true); expect(isNonSecretApiKeyMarker("ALLCAPS_EXAMPLE")).toBe(false); }); - it("recognizes all built-in provider env marker names", async () => { - const [{ listKnownProviderEnvApiKeyNames }, { isNonSecretApiKeyMarker }] = - await loadMarkerModules(); + it("recognizes all built-in provider env marker names", () => { for (const envVarName of listKnownProviderEnvApiKeyNames()) { expect(isNonSecretApiKeyMarker(envVarName)).toBe(true); } }); - it("can exclude env marker-name interpretation for display-only paths", async () => { - const [, { isNonSecretApiKeyMarker }] = await loadMarkerModules(); + it("can exclude env marker-name interpretation for display-only paths", () => { expect(isNonSecretApiKeyMarker("OPENAI_API_KEY", { includeEnvVarName: false })).toBe(false); }); - it("excludes aws-sdk env markers from known api key env marker helper", async () => { - const [, { isKnownEnvApiKeyMarker }] = await loadMarkerModules(); + it("excludes aws-sdk env markers from known api key env marker helper", () => { expect(isKnownEnvApiKeyMarker("OPENAI_API_KEY")).toBe(true); expect(isKnownEnvApiKeyMarker("AWS_PROFILE")).toBe(false); }); diff --git a/src/agents/model-fallback.probe.test.ts b/src/agents/model-fallback.probe.test.ts index 53c745e765f..2fdee3ed0f2 100644 --- a/src/agents/model-fallback.probe.test.ts +++ b/src/agents/model-fallback.probe.test.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { AuthProfileStore } from "./auth-profiles.js"; import { makeModelFallbackCfg } from "./test-helpers/model-fallback-config-fixture.js"; @@ -52,7 +52,6 @@ const makeCfg = makeModelFallbackCfg; let unregisterLogTransport: (() => void) | undefined; async function loadModelFallbackProbeModules() { - vi.resetModules(); const authProfilesStoreModule = await import("./auth-profiles/store.js"); const authProfilesUsageModule = await import("./auth-profiles/usage.js"); const authProfilesOrderModule = await import("./auth-profiles/order.js"); @@ -72,6 +71,8 @@ async function loadModelFallbackProbeModules() { setLoggerOverride = loggerModule.setLoggerOverride; } +beforeAll(loadModelFallbackProbeModules); + function expectFallbackUsed( result: { result: unknown; attempts: Array<{ reason?: string }> }, run: { @@ -170,8 +171,7 @@ describe("runWithModelFallback – probe logic", () => { run, }); - beforeEach(async () => { - await loadModelFallbackProbeModules(); + beforeEach(() => { realDateNow = Date.now; Date.now = vi.fn(() => NOW); diff --git a/src/agents/models-config.merge.test.ts b/src/agents/models-config.merge.test.ts index 4f526f6467d..52afcac1ee2 100644 --- a/src/agents/models-config.merge.test.ts +++ b/src/agents/models-config.merge.test.ts @@ -1,13 +1,21 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ExistingProviderConfig } from "./models-config.merge.js"; import type { ProviderConfig } from "./models-config.providers.secrets.js"; +let NON_ENV_SECRETREF_MARKER: typeof import("./model-auth-markers.js").NON_ENV_SECRETREF_MARKER; +let mergeProviderModels: typeof import("./models-config.merge.js").mergeProviderModels; +let mergeProviders: typeof import("./models-config.merge.js").mergeProviders; +let mergeWithExistingProviderSecrets: typeof import("./models-config.merge.js").mergeWithExistingProviderSecrets; + async function loadMergeModules() { vi.doUnmock("../plugins/manifest-registry.js"); - vi.resetModules(); - return Promise.all([import("./model-auth-markers.js"), import("./models-config.merge.js")]); + ({ NON_ENV_SECRETREF_MARKER } = await import("./model-auth-markers.js")); + ({ mergeProviderModels, mergeProviders, mergeWithExistingProviderSecrets } = + await import("./models-config.merge.js")); } +beforeAll(loadMergeModules); + beforeEach(() => { vi.doUnmock("../plugins/manifest-registry.js"); }); @@ -51,7 +59,6 @@ describe("models-config merge helpers", () => { } it("refreshes implicit model metadata while preserving explicit reasoning overrides", async () => { - const [, { mergeProviderModels }] = await loadMergeModules(); const merged = mergeProviderModels( { api: "openai-responses", @@ -95,7 +102,6 @@ describe("models-config merge helpers", () => { }); it("merges explicit providers onto trimmed keys", async () => { - const [, { mergeProviders }] = await loadMergeModules(); const merged = mergeProviders({ explicit: { " custom ": { @@ -111,7 +117,6 @@ describe("models-config merge helpers", () => { }); it("keeps existing providers alongside newly configured providers in merge mode", async () => { - const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules(); const merged = mergeWithExistingProviderSecrets({ nextProviders: { "custom-proxy": { @@ -137,7 +142,6 @@ describe("models-config merge helpers", () => { }); it("preserves non-empty existing apiKey while explicit baseUrl wins", async () => { - const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules(); const merged = mergeWithExistingProviderSecrets({ nextProviders: { custom: createConfigProvider(), @@ -154,7 +158,6 @@ describe("models-config merge helpers", () => { }); it("preserves existing apiKey after explicit provider key normalization", async () => { - const [, { mergeProviders, mergeWithExistingProviderSecrets }] = await loadMergeModules(); const normalized = mergeProviders({ explicit: { " custom ": createConfigProvider(), @@ -174,7 +177,6 @@ describe("models-config merge helpers", () => { }); it("preserves implicit provider headers when explicit config adds extra headers", async () => { - const [, { mergeProviderModels }] = await loadMergeModules(); const merged = mergeProviderModels( { baseUrl: "https://api.example.com", @@ -211,7 +213,6 @@ describe("models-config merge helpers", () => { }); it("replaces stale baseUrl when model api surface changes", async () => { - const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules(); const merged = mergeWithExistingProviderSecrets({ nextProviders: { custom: { @@ -239,7 +240,6 @@ describe("models-config merge helpers", () => { }); it("replaces stale baseUrl when only model-level apis change", async () => { - const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules(); const nextProvider = createConfigProvider(); delete (nextProvider as { api?: string }).api; nextProvider.models = [createModel({ api: "openai-responses" })]; @@ -263,7 +263,6 @@ describe("models-config merge helpers", () => { }); it("does not preserve stale plaintext apiKey when next entry is a marker", async () => { - const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules(); const merged = mergeWithExistingProviderSecrets({ nextProviders: { custom: { @@ -285,8 +284,6 @@ describe("models-config merge helpers", () => { }); it("does not preserve a stale non-env marker when config returns to plaintext", async () => { - const [{ NON_ENV_SECRETREF_MARKER }, { mergeWithExistingProviderSecrets }] = - await loadMergeModules(); const merged = mergeWithExistingProviderSecrets({ nextProviders: { custom: createConfigProvider({ apiKey: "ALLCAPS_SAMPLE" }), // pragma: allowlist secret @@ -305,7 +302,6 @@ describe("models-config merge helpers", () => { }); it("uses config apiKey/baseUrl when existing values are empty", async () => { - const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules(); const merged = mergeWithExistingProviderSecrets({ nextProviders: { custom: createConfigProvider(), diff --git a/src/agents/models-config.providers.auth-provenance.test.ts b/src/agents/models-config.providers.auth-provenance.test.ts index 64a5de8ece8..76953517c35 100644 --- a/src/agents/models-config.providers.auth-provenance.test.ts +++ b/src/agents/models-config.providers.auth-provenance.test.ts @@ -1,23 +1,45 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { captureEnv } from "../test-utils/env.js"; +vi.mock("../plugins/provider-runtime.js", () => ({ + resolveProviderSyntheticAuthWithPlugin: vi.fn(), +})); + +type ProviderRuntimeModule = typeof import("../plugins/provider-runtime.js"); + +let NON_ENV_SECRETREF_MARKER: typeof import("./model-auth-markers.js").NON_ENV_SECRETREF_MARKER; +let MINIMAX_OAUTH_MARKER: typeof import("./model-auth-markers.js").MINIMAX_OAUTH_MARKER; +let resolveApiKeyFromCredential: typeof import("./models-config.providers.secrets.js").resolveApiKeyFromCredential; +let createProviderAuthResolver: typeof import("./models-config.providers.secrets.js").createProviderAuthResolver; +let mockedResolveProviderSyntheticAuthWithPlugin: ReturnType< + typeof vi.mocked +>; + async function loadProviderAuthModules() { vi.doUnmock("../plugins/manifest-registry.js"); - vi.doUnmock("../plugins/provider-runtime.js"); vi.doUnmock("../secrets/provider-env-vars.js"); - vi.resetModules(); - return Promise.all([ + const [providerRuntimeModule, markersModule, secretsModule] = await Promise.all([ + import("../plugins/provider-runtime.js"), import("./model-auth-markers.js"), import("./models-config.providers.secrets.js"), ]); + mockedResolveProviderSyntheticAuthWithPlugin = vi.mocked( + providerRuntimeModule.resolveProviderSyntheticAuthWithPlugin, + ); + NON_ENV_SECRETREF_MARKER = markersModule.NON_ENV_SECRETREF_MARKER; + MINIMAX_OAUTH_MARKER = markersModule.MINIMAX_OAUTH_MARKER; + resolveApiKeyFromCredential = secretsModule.resolveApiKeyFromCredential; + createProviderAuthResolver = secretsModule.createProviderAuthResolver; } beforeEach(() => { vi.doUnmock("../plugins/manifest-registry.js"); - vi.doUnmock("../plugins/provider-runtime.js"); vi.doUnmock("../secrets/provider-env-vars.js"); + mockedResolveProviderSyntheticAuthWithPlugin.mockReset().mockReturnValue(undefined); }); +beforeAll(loadProviderAuthModules); + function buildPairedApiKeyProviders(apiKey: string) { return { provider: { apiKey }, @@ -26,8 +48,7 @@ function buildPairedApiKeyProviders(apiKey: string) { } describe("models-config provider auth provenance", () => { - it("persists env keyRef and tokenRef auth profiles as env var markers", async () => { - const [, { resolveApiKeyFromCredential }] = await loadProviderAuthModules(); + it("persists env keyRef and tokenRef auth profiles as env var markers", () => { const envSnapshot = captureEnv(["VOLCANO_ENGINE_API_KEY", "TOGETHER_API_KEY"]); delete process.env.VOLCANO_ENGINE_API_KEY; delete process.env.TOGETHER_API_KEY; @@ -52,9 +73,7 @@ describe("models-config provider auth provenance", () => { } }); - it("uses non-env marker for ref-managed profiles even when runtime plaintext is present", async () => { - const [{ NON_ENV_SECRETREF_MARKER }, { resolveApiKeyFromCredential }] = - await loadProviderAuthModules(); + it("uses non-env marker for ref-managed profiles even when runtime plaintext is present", () => { const byteplusApiKey = resolveApiKeyFromCredential({ type: "api_key", provider: "byteplus", @@ -74,8 +93,7 @@ describe("models-config provider auth provenance", () => { expect(togetherApiKey).toBe(NON_ENV_SECRETREF_MARKER); }); - it("keeps oauth compatibility markers for minimax-portal", async () => { - const [{ MINIMAX_OAUTH_MARKER }] = await loadProviderAuthModules(); + it("keeps oauth compatibility markers for minimax-portal", () => { const providers = { "minimax-portal": { apiKey: MINIMAX_OAUTH_MARKER, @@ -84,8 +102,7 @@ describe("models-config provider auth provenance", () => { expect(providers["minimax-portal"]?.apiKey).toBe(MINIMAX_OAUTH_MARKER); }); - it("prefers profile auth over env auth in provider summaries to match runtime resolution", async () => { - const [, { createProviderAuthResolver }] = await loadProviderAuthModules(); + it("prefers profile auth over env auth in provider summaries to match runtime resolution", () => { const auth = createProviderAuthResolver( { OPENAI_API_KEY: "env-openai-key", @@ -111,9 +128,10 @@ describe("models-config provider auth provenance", () => { }); }); - it("resolves plugin-owned synthetic auth through the provider hook", async () => { - const [{ NON_ENV_SECRETREF_MARKER }, { createProviderAuthResolver }] = - await loadProviderAuthModules(); + it("resolves plugin-owned synthetic auth through the provider hook", () => { + mockedResolveProviderSyntheticAuthWithPlugin.mockReturnValue({ + apiKey: "xai-plugin-key", + }); const auth = createProviderAuthResolver( {} as NodeJS.ProcessEnv, {