Download src/agents/runtime-plan/prepare-auth.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 68.1 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/runtime-plan/prepare-auth.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/agents/runtime-plan/prepare-auth.test.ts
-
curl -L -o prepare-auth.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/runtime-plan/prepare-auth.test.ts
68.1 kB
| import { describe, expect, it, vi } from "vitest"; | |
| import type { OpenClawConfig } from "../../config/types.openclaw.js"; | |
| import type { Model } from "../../llm/types.js"; | |
| import { createPluginMetadataSnapshotFixture } from "../../plugins/plugin-metadata.test-support.js"; | |
| import type { AuthProfileStore } from "../auth-profiles.js"; | |
| import { createApiKeyCredential } from "../auth-profiles/credential-fixtures.test-support.js"; | |
| import { createOAuthRefreshFence } from "../auth-profiles/oauth-refresh-marker.js"; | |
| import { resolveAgentHarnessPreparedAuthSupport } from "../harness/support.js"; | |
| import { getApiKeyForModelCore } from "../model-auth.js"; | |
| import { | |
| agentRuntimeAuthPlanMatchesTarget, | |
| canRunPreparedAgentRuntimeAuthAttempt, | |
| prepareAgentRuntimeAuth, | |
| preparedAgentRuntimeProfileAttemptHasCandidate, | |
| } from "./prepare-auth.js"; | |
| // This suite owns generic auth planning. Provider-hook behavior has dedicated | |
| // coverage; keep those runtimes out of this focused planner test. | |
| vi.mock("../../plugins/provider-runtime.js", () => ({ | |
| buildProviderMissingAuthMessageWithPlugin: () => undefined, | |
| resolveProviderDeprecatedAuthProfileIds: () => [], | |
| resolveProviderSyntheticAuthWithPlugin: () => undefined, | |
| shouldDeferProviderSyntheticProfileAuthWithPlugin: () => undefined, | |
| })); | |
| function prepareAgentRuntimeAuthPlan(params: Parameters<typeof prepareAgentRuntimeAuth>[0]) { | |
| return prepareAgentRuntimeAuth(params).plan; | |
| } | |
| function authStore( | |
| profiles: AuthProfileStore["profiles"], | |
| order?: AuthProfileStore["order"], | |
| ): AuthProfileStore { | |
| return { version: 1, profiles, ...(order ? { order } : {}) }; | |
| } | |
| function providerConfig(provider: string, config: Record<string, unknown>): OpenClawConfig { | |
| return { | |
| models: { | |
| providers: { | |
| [provider]: { baseUrl: "", models: [], ...config }, | |
| }, | |
| }, | |
| } as OpenClawConfig; | |
| } | |
| function openAIConfig(config: Record<string, unknown>): OpenClawConfig { | |
| return providerConfig("openai", config); | |
| } | |
| function openAIAuthFixture() { | |
| return { provider: "openai", modelId: "gpt-5.5" } as const; | |
| } | |
| function openAIPlatformAuthFixture() { | |
| return { | |
| ...openAIAuthFixture(), | |
| modelApi: "openai-responses", | |
| modelBaseUrl: "https://api.openai.com/v1", | |
| } as const; | |
| } | |
| function codexPlatformAuthFixture() { | |
| return { | |
| ...openAIPlatformAuthFixture(), | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| } as const; | |
| } | |
| function openAIChatGptAuthFixture() { | |
| return { | |
| ...openAIAuthFixture(), | |
| modelApi: "openai-chatgpt-responses", | |
| modelBaseUrl: "https://chatgpt.com/backend-api/codex", | |
| } as const; | |
| } | |
| function virtualCodexAuthFixture() { | |
| return { | |
| provider: "codex", | |
| modelId: "gpt-5.4", | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| } as const; | |
| } | |
| function openAIApiKeyProfile(key: string) { | |
| return createApiKeyCredential("openai", key); | |
| } | |
| function openAITokenProfile(token: string, expires?: number) { | |
| return { | |
| type: "token" as const, | |
| provider: "openai", | |
| token, | |
| ...(expires === undefined ? {} : { expires }), | |
| }; | |
| } | |
| function openAIOAuthProfile(access: string, refresh: string, expires: number) { | |
| return { type: "oauth" as const, provider: "openai", access, refresh, expires }; | |
| } | |
| function setProfileCooldown( | |
| store: AuthProfileStore, | |
| profileId: string, | |
| details: Omit<NonNullable<AuthProfileStore["usageStats"]>[string], "cooldownUntil"> = {}, | |
| ) { | |
| store.usageStats = { | |
| [profileId]: { cooldownUntil: Date.now() + 60_000, ...details }, | |
| }; | |
| } | |
| function allCooldownOpenAIStore(): AuthProfileStore { | |
| const store = authStore( | |
| { "openai:cooldown": openAIApiKeyProfile("cooldown-key") }, | |
| { openai: ["openai:cooldown"] }, | |
| ); | |
| setProfileCooldown(store, "openai:cooldown"); | |
| return store; | |
| } | |
| describe("prepareAgentRuntimeAuthPlan", () => { | |
| it.each([ | |
| { | |
| pinnedProfile: "openai:platform", | |
| primaryProfile: undefined, | |
| requirement: "api-key", | |
| attempts: ["openai:platform", "openai:chatgpt"], | |
| }, | |
| { | |
| pinnedProfile: "openai:chatgpt", | |
| primaryProfile: "openai:platform", | |
| requirement: "subscription", | |
| attempts: ["openai:chatgpt"], | |
| }, | |
| ])( | |
| "keeps $pinnedProfile ahead of inherited and automatic billing preferences", | |
| ({ pinnedProfile, primaryProfile, requirement, attempts }) => { | |
| const prepared = prepareAgentRuntimeAuth({ | |
| ...openAIPlatformAuthFixture(), | |
| env: {}, | |
| sessionAuthProfileId: pinnedProfile, | |
| sessionAuthProfileSource: "user", | |
| config: primaryProfile | |
| ? { | |
| agents: { defaults: { model: `openai/gpt-5.4@${primaryProfile}` } }, | |
| auth: { profiles: { "openai:platform": { provider: "openai", mode: "api_key" } } }, | |
| } | |
| : undefined, | |
| authProfileStore: authStore({ | |
| "openai:platform": openAIApiKeyProfile("fixture-key"), | |
| "openai:chatgpt": openAIOAuthProfile( | |
| "fixture-access", | |
| "fixture-refresh", | |
| Date.now() + 60_000, | |
| ), | |
| }), | |
| }); | |
| expect(prepared.plan).toMatchObject({ | |
| forwardedAuthProfileId: pinnedProfile, | |
| modelRoute: { authRequirement: requirement }, | |
| }); | |
| expect(prepared.attempts.map((attempt) => attempt.profileId)).toEqual(attempts); | |
| }, | |
| ); | |
| it("keeps auxiliary models on the default model's subscription route", () => { | |
| const model = "openai/gpt-5.4-mini"; | |
| const config: OpenClawConfig = { | |
| agents: { | |
| defaults: { | |
| model: "openai/gpt-5.5", | |
| models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } } }, | |
| heartbeat: { model }, | |
| subagents: { model }, | |
| }, | |
| }, | |
| models: { | |
| providers: { | |
| openai: { | |
| api: "openai-completions", | |
| baseUrl: "https://api.openai.com/v1", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| }; | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "openai", | |
| modelId: model.slice("openai/".length), | |
| config, | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "openai:default": openAIApiKeyProfile("fixture-api-key"), | |
| "openai:chatgpt-default": openAIOAuthProfile( | |
| "fixture-access", | |
| "fixture-refresh", | |
| Date.now() + 60_000, | |
| ), | |
| }), | |
| }); | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "openai:chatgpt-default", | |
| modelRoute: { | |
| authRequirement: "subscription", | |
| api: "openai-chatgpt-responses", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| }, | |
| }); | |
| }); | |
| it("keeps a materialized model endpoint on its own credential provider", () => { | |
| const config: OpenClawConfig = { | |
| models: { | |
| providers: { | |
| arcee: { baseUrl: "https://openrouter.ai/api/v1", models: [] }, | |
| }, | |
| }, | |
| }; | |
| const metadataSnapshot = createPluginMetadataSnapshotFixture({ | |
| plugins: [ | |
| { | |
| id: "arcee", | |
| providers: ["arcee"], | |
| providerAuthAliases: { | |
| arcee: { provider: "openrouter", baseUrls: ["https://openrouter.ai/api/v1"] }, | |
| }, | |
| }, | |
| ], | |
| }); | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "arcee", | |
| modelId: "trinity-large-thinking", | |
| modelApi: "openai-completions", | |
| modelBaseUrl: "https://api.arcee.ai/api/v1", | |
| config, | |
| metadataSnapshot, | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "arcee:direct": createApiKeyCredential("arcee", "direct-model-key"), | |
| "openrouter:routed": createApiKeyCredential("openrouter", "router-model-key"), | |
| }), | |
| }); | |
| expect(plan.providerForAuth).toBe("arcee"); | |
| expect(plan.forwardedAuthProfileId).toBe("arcee:direct"); | |
| expect(config.models?.providers?.arcee?.baseUrl).toBe("https://openrouter.ai/api/v1"); | |
| }); | |
| it("carries prepared provider aliases into generic auth planning", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "legacy-provider", | |
| modelId: "model", | |
| env: {}, | |
| authProfileStore: authStore({}), | |
| metadataSnapshot: createPluginMetadataSnapshotFixture({ | |
| plugins: [ | |
| { | |
| id: "alias-owner", | |
| origin: "bundled", | |
| providerAuthAliases: { "legacy-provider": "canonical-provider" }, | |
| }, | |
| ], | |
| }), | |
| }); | |
| expect(plan.providerForAuth).toBe("canonical-provider"); | |
| }); | |
| it("keeps unknown no-observation models on the legacy auth plan", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "openai", | |
| modelId: "gpt-5.4-nano", | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore({}), | |
| }); | |
| expect(plan).toMatchObject({ | |
| providerForAuth: "openai", | |
| harnessAuthProvider: "openai", | |
| }); | |
| expect(plan.modelRoute).toBeUndefined(); | |
| expect(plan.deferredRouteSupport).toBeUndefined(); | |
| }); | |
| it("keeps a generic provider-entry binding ahead of an automatic backup", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| config: providerConfig("xai", { apiKey: "xai:bound" }), | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "xai:bound": createApiKeyCredential("xai", "bound-key"), | |
| "xai:backup": createApiKeyCredential("xai", "backup-key"), | |
| }, | |
| { xai: ["xai:backup", "xai:bound"] }, | |
| ), | |
| sessionAuthProfileId: "xai:backup", | |
| sessionAuthProfileSource: "auto", | |
| }); | |
| expect(plan).toMatchObject({ | |
| providerForAuth: "xai", | |
| forwardedAuthProfileId: "xai:bound", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["xai:bound"], | |
| selectedAuthMode: "api_key", | |
| }); | |
| expect(plan.modelRoute).toBeUndefined(); | |
| }); | |
| it("rejects a cooldowned generic provider-entry binding instead of using a backup", () => { | |
| const store = authStore( | |
| { | |
| "xai:bound": createApiKeyCredential("xai", "bound-key"), | |
| "xai:backup": createApiKeyCredential("xai", "backup-key"), | |
| }, | |
| { xai: ["xai:backup", "xai:bound"] }, | |
| ); | |
| setProfileCooldown(store, "xai:bound"); | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| config: providerConfig("xai", { apiKey: "xai:bound" }), | |
| env: {}, | |
| authProfileStore: store, | |
| sessionAuthProfileId: "xai:backup", | |
| sessionAuthProfileSource: "auto", | |
| }), | |
| ).toThrow(/temporarily unavailable/u); | |
| }); | |
| it("keeps generic AWS SDK auth ahead of provider bindings and automatic profiles", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| config: providerConfig("xai", { auth: "aws-sdk", apiKey: "xai:bound" }), | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "xai:bound": createApiKeyCredential("xai", "bound-key"), | |
| "xai:backup": createApiKeyCredential("xai", "backup-key"), | |
| }), | |
| sessionAuthProfileId: "xai:backup", | |
| sessionAuthProfileSource: "auto", | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.forwardedAuthProfileCandidateIds).toBeUndefined(); | |
| expect(plan.selectedAuthMode).toBe("aws-sdk"); | |
| expect(plan.credentialSource).toEqual({ | |
| kind: "direct", | |
| evidence: "aws-sdk", | |
| authorization: "declared", | |
| }); | |
| expect(plan.modelRoute).toBeUndefined(); | |
| }); | |
| it("rotates a generic automatic profile past a model cooldown", () => { | |
| const store = authStore( | |
| { | |
| "xai:p1": createApiKeyCredential("xai", "p1-key"), | |
| "xai:p2": createApiKeyCredential("xai", "p2-key"), | |
| "xai:p3": createApiKeyCredential("xai", "p3-key"), | |
| }, | |
| { xai: ["xai:p1", "xai:p2", "xai:p3"] }, | |
| ); | |
| setProfileCooldown(store, "xai:p1", { | |
| cooldownReason: "rate_limit", | |
| cooldownModel: "grok-4", | |
| }); | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| env: {}, | |
| authProfileStore: store, | |
| sessionAuthProfileId: "xai:p1", | |
| sessionAuthProfileSource: "auto", | |
| }); | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "xai:p2", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["xai:p2", "xai:p3"], | |
| selectedAuthMode: "api_key", | |
| }); | |
| expect(plan.modelRoute).toBeUndefined(); | |
| }); | |
| it("applies a provider-owned preferred profile without turning it into a lock", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "xai:p1": createApiKeyCredential("xai", "p1-key"), | |
| "xai:p2": createApiKeyCredential("xai", "p2-key"), | |
| }, | |
| { xai: ["xai:p1", "xai:p2"] }, | |
| ), | |
| resolveProviderPreferredProfileId: () => "xai:p2", | |
| }); | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "xai:p2", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["xai:p2", "xai:p1"], | |
| }); | |
| }); | |
| it("drops proven-unavailable generic candidates before forwarding fallbacks", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| config: { | |
| secrets: { | |
| providers: { | |
| vault: { source: "file", path: "/tmp/secrets.json", mode: "json" }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "xai:missing": { | |
| type: "api_key", | |
| provider: "xai", | |
| keyRef: { source: "env", provider: "vault", id: "XAI_API_KEY" }, | |
| }, | |
| "xai:p2": createApiKeyCredential("xai", "p2-key"), | |
| "xai:p3": createApiKeyCredential("xai", "p3-key"), | |
| }, | |
| { xai: ["xai:missing", "xai:p2", "xai:p3"] }, | |
| ), | |
| sessionAuthProfileId: "xai:missing", | |
| sessionAuthProfileSource: "auto", | |
| }); | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "xai:p2", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["xai:p2", "xai:p3"], | |
| }); | |
| }); | |
| it("fails closed before resolving an all-cooldown generic order", () => { | |
| const store = authStore( | |
| { | |
| "xai:p1": createApiKeyCredential("xai", "p1-key"), | |
| "xai:p2": createApiKeyCredential("xai", "p2-key"), | |
| }, | |
| { xai: ["xai:p1", "xai:p2"] }, | |
| ); | |
| setProfileCooldown(store, "xai:p1"); | |
| store.usageStats!["xai:p2"] = { cooldownUntil: Date.now() + 60_000 }; | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| env: {}, | |
| authProfileStore: store, | |
| sessionAuthProfileId: "xai:p1", | |
| sessionAuthProfileSource: "auto", | |
| }), | |
| ).toThrow(/temporarily unavailable/u); | |
| }); | |
| it("fails closed when an explicit generic order contains only missing profiles", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| config: { | |
| auth: { order: { xai: ["xai:missing"] } }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "xai:backup": createApiKeyCredential("xai", "backup-key"), | |
| }), | |
| }), | |
| ).toThrow(/explicit auth order.*no usable profiles/iu); | |
| }); | |
| it("fails closed when an explicit generic order is empty", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| config: { | |
| auth: { order: { xai: [] } }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "xai:backup": createApiKeyCredential("xai", "backup-key"), | |
| }), | |
| }), | |
| ).toThrow(/explicit auth order.*no usable profiles/iu); | |
| }); | |
| it("skips a cooldowned user pin and selects the next same-provider profile", () => { | |
| const store = authStore( | |
| { | |
| "xai:p1": createApiKeyCredential("xai", "p1-key"), | |
| "xai:p2": createApiKeyCredential("xai", "p2-key"), | |
| }, | |
| { xai: ["xai:p1", "xai:p2"] }, | |
| ); | |
| setProfileCooldown(store, "xai:p1"); | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| env: {}, | |
| authProfileStore: store, | |
| sessionAuthProfileId: "xai:p1", | |
| sessionAuthProfileSource: "user", | |
| }); | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "xai:p2", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["xai:p2"], | |
| selectedAuthMode: "api_key", | |
| }); | |
| }); | |
| it("prepares a user pin first and retains same-provider profile fallbacks", () => { | |
| const prepared = prepareAgentRuntimeAuth({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "xai:p1": createApiKeyCredential("xai", "p1-key"), | |
| "xai:p2": createApiKeyCredential("xai", "p2-key"), | |
| }, | |
| { xai: ["xai:p2", "xai:p1"] }, | |
| ), | |
| sessionAuthProfileId: "xai:p1", | |
| sessionAuthProfileSource: "user", | |
| }); | |
| const profileAttempts = prepared.attempts.filter((attempt) => attempt.kind === "profile"); | |
| expect(profileAttempts.map((attempt) => attempt.profileId)).toEqual(["xai:p1", "xai:p2"]); | |
| expect(profileAttempts.map((attempt) => attempt.plan.forwardedAuthProfileSource)).toEqual([ | |
| "user", | |
| "auto", | |
| ]); | |
| }); | |
| it("prepares an automatic pending OAuth fence for runtime settlement", () => { | |
| const pendingProfileId = "xai:pending"; | |
| const backupProfileId = "xai:backup"; | |
| const pending = createOAuthRefreshFence({ | |
| profileId: pendingProfileId, | |
| credential: { | |
| type: "oauth", | |
| provider: "xai", | |
| access: "expired-access", | |
| refresh: "refresh-token", | |
| expires: 1, | |
| }, | |
| }); | |
| const prepared = prepareAgentRuntimeAuth({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| [pendingProfileId]: pending, | |
| [backupProfileId]: createApiKeyCredential("xai", "backup-key"), | |
| }, | |
| { xai: [pendingProfileId, backupProfileId] }, | |
| ), | |
| }); | |
| expect( | |
| prepared.attempts | |
| .filter((attempt) => attempt.kind === "profile") | |
| .map((attempt) => attempt.profileId), | |
| ).toEqual([pendingProfileId, backupProfileId]); | |
| }); | |
| it("keeps a user-pinned pending OAuth fence ahead of ordered siblings", () => { | |
| const pendingProfileId = "xai:pending"; | |
| const backupProfileId = "xai:backup"; | |
| const pending = createOAuthRefreshFence({ | |
| profileId: pendingProfileId, | |
| credential: { | |
| type: "oauth", | |
| provider: "xai", | |
| access: "expired-access", | |
| refresh: "refresh-token", | |
| expires: 1, | |
| }, | |
| }); | |
| const prepared = prepareAgentRuntimeAuth({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| [pendingProfileId]: pending, | |
| [backupProfileId]: createApiKeyCredential("xai", "backup-key"), | |
| }, | |
| { xai: [backupProfileId] }, | |
| ), | |
| sessionAuthProfileId: pendingProfileId, | |
| sessionAuthProfileSource: "user", | |
| }); | |
| expect( | |
| prepared.attempts | |
| .filter((attempt) => attempt.kind === "profile") | |
| .map((attempt) => attempt.profileId), | |
| ).toEqual([pendingProfileId, backupProfileId]); | |
| }); | |
| it("defers an ambiguous route when native Codex owns auth", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIChatGptAuthFixture(), | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| harnessAuthBootstrap: "harness", | |
| authProfileStore: authStore({}), | |
| }); | |
| expect(plan.harnessAuthProvider).toBe("openai"); | |
| expect(plan.modelRoute).toBeUndefined(); | |
| expect(plan.deferredRouteSupport).toEqual({ | |
| requestTransportOverrides: "none", | |
| runtimePolicy: { compatibleIds: ["openclaw", "codex"] }, | |
| }); | |
| expect(plan.credentialSource).toBeUndefined(); | |
| expect(resolveAgentHarnessPreparedAuthSupport({ plan })).toEqual({ source: "harness" }); | |
| }); | |
| it("falls through an unusable env marker to an ordered API-key profile", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: openAIConfig({ apiKey: "OPENAI_API_KEY" }), | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore( | |
| { | |
| "openai:backup": openAIApiKeyProfile("backup-key"), | |
| }, | |
| { openai: ["openai:backup"] }, | |
| ), | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBe("openai:backup"); | |
| expect(plan.modelRoute?.authRequirement).toBe("api-key"); | |
| }); | |
| it("rejects a concrete route when an env marker has no usable credential", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: openAIConfig({ apiKey: "OPENAI_API_KEY" }), | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore({}), | |
| }), | |
| ).toThrow(/No route-compatible authentication source/u); | |
| }); | |
| it("skips cooldowned automatic profiles before selecting a healthy backup", () => { | |
| const store = authStore( | |
| { | |
| "openai:cooldown": openAIApiKeyProfile("cooldown-key"), | |
| "openai:backup": openAIApiKeyProfile("backup-key"), | |
| }, | |
| { openai: ["openai:cooldown", "openai:backup"] }, | |
| ); | |
| setProfileCooldown(store, "openai:cooldown"); | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...codexPlatformAuthFixture(), | |
| authProfileStore: store, | |
| sessionAuthProfileId: "openai:cooldown", | |
| sessionAuthProfileSource: "auto", | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBe("openai:backup"); | |
| expect(plan.forwardedAuthProfileCandidateIds).toEqual(["openai:backup"]); | |
| }); | |
| it("does not bypass an all-cooldown auth order through native Codex auth", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: allCooldownOpenAIStore(), | |
| }), | |
| ).toThrow(/temporarily unavailable/u); | |
| }); | |
| it("keeps an explicit provider SecretRef ahead of an all-cooldown auth order", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| apiKey: { source: "env", provider: "default", id: "DIRECT_OPENAI_KEY" }, | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| secrets: { providers: { default: { source: "env" } } }, | |
| } as OpenClawConfig, | |
| env: { DIRECT_OPENAI_KEY: "sk-direct" }, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: allCooldownOpenAIStore(), | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.credentialSource).toEqual({ | |
| kind: "direct", | |
| evidence: "environment", | |
| authorization: "declared", | |
| }); | |
| }); | |
| it("does not let clear OAuth auth hide a cooldown Platform tier before literal fallback", () => { | |
| const store = authStore( | |
| { | |
| "openai:chatgpt": openAIOAuthProfile("oauth-access", "oauth-refresh", Date.now() + 60_000), | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| }, | |
| { openai: ["openai:chatgpt", "openai:platform"] }, | |
| ); | |
| store.usageStats = { | |
| "openai:platform": { cooldownUntil: Date.now() + 60_000 }, | |
| }; | |
| expect(() => | |
| prepareAgentRuntimeAuth({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| routeIntent: { authRequirement: "api-key", source: "explicit" }, | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { apiKey: "configured-platform-key", baseUrl: "", models: [] }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: store, | |
| }), | |
| ).toThrow(/temporarily unavailable/u); | |
| }); | |
| it("rejects an incompatible provider-bound profile before Codex forwarding", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| modelApi: "openai-responses", | |
| modelBaseUrl: "https://relay.example/v1", | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| api: "openai-responses", | |
| apiKey: "relay:key", | |
| baseUrl: "https://relay.example/v1", | |
| models: [], | |
| }, | |
| relay: { | |
| api: "openai-responses", | |
| baseUrl: "https://relay.example/v1", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore({ | |
| "relay:key": createApiKeyCredential("relay", "relay-secret"), | |
| }), | |
| }), | |
| ).toThrow(/has no usable credentials/u); | |
| }); | |
| it("rejects an incompatible provider binding on a generic Codex plan", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| provider: "codex", | |
| modelId: "gpt-5.4", | |
| config: { | |
| models: { | |
| providers: { | |
| codex: { | |
| apiKey: "relay:key", | |
| baseUrl: "https://relay.example/v1", | |
| models: [], | |
| }, | |
| relay: { | |
| baseUrl: "https://relay.example/v1", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore({ | |
| "relay:key": createApiKeyCredential("relay", "relay-secret"), | |
| }), | |
| }), | |
| ).toThrow(/has no usable credentials/u); | |
| }); | |
| it("selects the first compatible auth.order profile with its exact route", () => { | |
| const preparation = prepareAgentRuntimeAuth({ | |
| ...openAIPlatformAuthFixture(), | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore( | |
| { | |
| "openai:chatgpt": openAITokenProfile("subscription-token", Date.now() + 60_000), | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| }, | |
| { openai: ["openai:chatgpt", "openai:platform"] }, | |
| ), | |
| }); | |
| const plan = preparation.plan; | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "openai:chatgpt", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:chatgpt"], | |
| selectedAuthMode: "token", | |
| modelRoute: { | |
| api: "openai-chatgpt-responses", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| authRequirement: "subscription", | |
| }, | |
| }); | |
| expect( | |
| preparation.attempts.map((attempt) => ({ | |
| profileId: attempt.profileId, | |
| authRequirement: attempt.plan.modelRoute?.authRequirement, | |
| })), | |
| ).toEqual([ | |
| { profileId: "openai:chatgpt", authRequirement: "subscription" }, | |
| { profileId: "openai:platform", authRequirement: "api-key" }, | |
| ]); | |
| }); | |
| it("prepares every ordered same-route profile as an exhaustive fallback set", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: openAIConfig({ baseUrl: "https://api.openai.com/v1" }), | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore( | |
| { | |
| "openai:missing": { | |
| type: "api_key", | |
| provider: "openai", | |
| keyRef: { | |
| source: "env", | |
| provider: "default", | |
| id: "OPENCLAW_TEST_MISSING_PREPARED_AUTH", | |
| }, | |
| }, | |
| "openai:backup": openAIApiKeyProfile("backup-key"), | |
| }, | |
| { openai: ["openai:missing", "openai:backup"] }, | |
| ), | |
| }); | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "openai:missing", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:missing", "openai:backup"], | |
| selectedAuthMode: "api_key", | |
| modelRoute: { authRequirement: "api-key" }, | |
| }); | |
| }); | |
| it("keeps same-route native candidates ahead of interleaved route fallbacks", () => { | |
| const preparation = prepareAgentRuntimeAuth({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| secrets: { | |
| providers: { | |
| vault: { source: "file", path: "/tmp/secrets.json", mode: "json" }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore( | |
| { | |
| "openai:subscription-missing": { | |
| type: "token", | |
| provider: "openai", | |
| tokenRef: { source: "file", provider: "vault", id: "/chatgpt/token" }, | |
| }, | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| "openai:subscription-backup": openAITokenProfile("subscription-token"), | |
| }, | |
| { | |
| openai: ["openai:subscription-missing", "openai:platform", "openai:subscription-backup"], | |
| }, | |
| ), | |
| }); | |
| expect(preparation.plan.forwardedAuthProfileCandidateIds).toEqual([ | |
| "openai:subscription-missing", | |
| "openai:subscription-backup", | |
| ]); | |
| expect( | |
| preparation.attempts.map((attempt) => ({ | |
| profileId: attempt.profileId, | |
| authRequirement: attempt.plan.modelRoute?.authRequirement, | |
| })), | |
| ).toEqual([ | |
| { profileId: "openai:subscription-missing", authRequirement: "subscription" }, | |
| { profileId: "openai:subscription-backup", authRequirement: "subscription" }, | |
| { profileId: "openai:platform", authRequirement: "api-key" }, | |
| ]); | |
| }); | |
| it("skips a definitively invalid ordered profile before selecting a sibling route", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| auth: { order: { openai: ["openai:bad-key", "openai:chatgpt"] } }, | |
| secrets: { | |
| providers: { | |
| vault: { source: "file", path: "/tmp/secrets.json", mode: "json" }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore( | |
| { | |
| "openai:bad-key": { | |
| type: "api_key", | |
| provider: "openai", | |
| keyRef: { source: "env", provider: "vault", id: "OPENAI_API_KEY" }, | |
| }, | |
| "openai:chatgpt": openAIOAuthProfile( | |
| "access-token", | |
| "refresh-token", | |
| Date.now() + 10 * 60_000, | |
| ), | |
| }, | |
| { openai: ["openai:bad-key", "openai:chatgpt"] }, | |
| ), | |
| }); | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "openai:chatgpt", | |
| forwardedAuthProfileCandidateIds: ["openai:chatgpt"], | |
| selectedAuthMode: "oauth", | |
| modelRoute: { authRequirement: "subscription" }, | |
| }); | |
| }); | |
| it("keeps an explicit provider SecretRef ahead of an all-invalid auth order", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| auth: { order: { openai: ["openai:ordered"] } }, | |
| secrets: { | |
| providers: { | |
| default: { source: "env" }, | |
| vault: { source: "file", path: "/tmp/secrets.json", mode: "json" }, | |
| }, | |
| }, | |
| models: { | |
| providers: { | |
| openai: { | |
| apiKey: { source: "env", provider: "default", id: "DIRECT_OPENAI_KEY" }, | |
| baseUrl: "https://api.openai.com/v1", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: { DIRECT_OPENAI_KEY: "sk-direct" }, | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| authProfileStore: authStore( | |
| { | |
| "openai:ordered": { | |
| type: "api_key", | |
| provider: "openai", | |
| keyRef: { source: "env", provider: "vault", id: "ORDERED_OPENAI_KEY" }, | |
| }, | |
| }, | |
| { openai: ["openai:ordered"] }, | |
| ), | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.credentialSource).toEqual({ | |
| kind: "direct", | |
| evidence: "environment", | |
| authorization: "declared", | |
| }); | |
| }); | |
| it("does not cross to an incompatible auth route for a user pin", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...openAIChatGptAuthFixture(), | |
| env: {}, | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| api: "openai-chatgpt-responses", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| sessionAuthProfileId: "openai:platform", | |
| sessionAuthProfileSource: "user", | |
| authProfileStore: authStore({ | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| }), | |
| }), | |
| ).toThrow(/no route-compatible authentication source/iu); | |
| }); | |
| it("lets an explicit provider API key outrank automatic subscription profiles", () => { | |
| const config = { | |
| models: { | |
| providers: { | |
| openai: { | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| auth: "api-key", | |
| apiKey: "configured-platform-key", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig; | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIChatGptAuthFixture(), | |
| config, | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "openai:chatgpt": openAIOAuthProfile( | |
| "subscription-token", | |
| "refresh-token", | |
| Date.now() + 60_000, | |
| ), | |
| }), | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.modelRoute).toMatchObject({ | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key", | |
| }); | |
| }); | |
| it("rejects an official authored route with unvalidated native auth", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "api-key", | |
| apiKey: "configured-platform-key", | |
| baseUrl: "https://api.openai.com/v1", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: { OPENAI_API_KEY: "ambient-platform-key" }, | |
| authProfileStore: authStore( | |
| { | |
| "openai:chatgpt": openAIOAuthProfile( | |
| "subscription-token", | |
| "refresh-token", | |
| Date.now() + 60_000, | |
| ), | |
| }, | |
| { openai: ["openai:chatgpt"] }, | |
| ), | |
| sessionAuthProfileId: "openai:chatgpt", | |
| sessionAuthProfileSource: "auto", | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| harnessAuthBootstrap: "harness", | |
| allowHarnessAuthProfileForwarding: false, | |
| }), | |
| ).toThrow(/route-compatible authentication source/u); | |
| }); | |
| it("rejects a user-locked profile when the harness cannot accept host auth", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "openai:work": openAIApiKeyProfile("platform-key"), | |
| }), | |
| sessionAuthProfileId: "openai:work", | |
| sessionAuthProfileSource: "user", | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| allowHarnessAuthProfileForwarding: false, | |
| }), | |
| ).toThrow(/native account instead/u); | |
| }); | |
| it("honors the no-host-auth policy for non-Codex harnesses", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "xai", | |
| modelId: "grok-4", | |
| config: providerConfig("xai", { auth: "api-key", apiKey: "xai-key" }), | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "xai:auto": createApiKeyCredential("xai", "profile-key"), | |
| }), | |
| sessionAuthProfileId: "xai:auto", | |
| sessionAuthProfileSource: "auto", | |
| harnessId: "native-remote", | |
| harnessRuntime: "native-remote", | |
| allowHarnessAuthProfileForwarding: false, | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.selectedAuthMode).toBeUndefined(); | |
| }); | |
| it("lets a provider-entry token profile binding outrank configured auth and auth.order", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "api-key", | |
| apiKey: "openai:bound", | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "openai:bound": openAITokenProfile("subscription-token", Date.now() + 60_000), | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| }, | |
| { openai: ["openai:platform", "openai:bound"] }, | |
| ), | |
| }); | |
| expect(plan).toMatchObject({ | |
| forwardedAuthProfileId: "openai:bound", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:bound"], | |
| selectedAuthMode: "token", | |
| modelRoute: { | |
| api: "openai-chatgpt-responses", | |
| authRequirement: "subscription", | |
| }, | |
| }); | |
| }); | |
| it.each([ | |
| { provider: "anthropic", mode: "api_key" as const }, | |
| { provider: "openai", mode: "oauth" as const }, | |
| ])("rejects a bound profile with conflicting $provider/$mode metadata", ({ mode, provider }) => { | |
| expect(() => | |
| prepareAgentRuntimeAuth({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| config: { | |
| auth: { profiles: { "openai:bound": { provider, mode } } }, | |
| models: { | |
| providers: { | |
| openai: { apiKey: "openai:bound", baseUrl: "", models: [] }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "openai:bound": openAIApiKeyProfile("bound-platform-key"), | |
| }), | |
| }), | |
| ).toThrow(/no usable credentials/u); | |
| }); | |
| it("rejects an incompatible provider-entry profile without borrowing auth.order", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| apiKey: "openai:oauth", | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "openai:oauth": openAIOAuthProfile( | |
| "subscription-token", | |
| "refresh-token", | |
| Date.now() + 60_000, | |
| ), | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| }, | |
| { openai: ["openai:platform"] }, | |
| ), | |
| }), | |
| ).toThrow(/not a compatible bearer profile/u); | |
| }); | |
| it("does not forward a cooldowned provider-entry profile", () => { | |
| const store = authStore({ | |
| "openai:bound": openAITokenProfile("subscription-token", Date.now() + 60_000), | |
| }); | |
| setProfileCooldown(store, "openai:bound"); | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { apiKey: "openai:bound", baseUrl: "", models: [] }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: store, | |
| }), | |
| ).toThrow(/temporarily unavailable/u); | |
| }); | |
| it("keeps an explicit AWS SDK auth mode ahead of provider-entry profile bindings", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "aws-sdk", | |
| apiKey: "openai:bound", | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "openai:bound": openAITokenProfile("subscription-token", Date.now() + 60_000), | |
| }), | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.selectedAuthMode).toBe("aws-sdk"); | |
| expect(plan.modelRoute).toMatchObject({ | |
| api: "openai-responses", | |
| authRequirement: "api-key", | |
| }); | |
| }); | |
| it("keeps AWS SDK auth terminal when an API-key SecretRef and ordered profile also exist", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "aws-sdk", | |
| apiKey: { source: "file", provider: "vault", id: "/openai/api-key" }, | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| secrets: { | |
| providers: { | |
| vault: { source: "file", path: "/tmp/openai-secrets.json", mode: "json" }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| }, | |
| { openai: ["openai:platform"] }, | |
| ), | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.forwardedAuthProfileCandidateIds).toBeUndefined(); | |
| expect(plan.selectedAuthMode).toBe("aws-sdk"); | |
| expect(plan.modelRoute).toMatchObject({ | |
| api: "openai-responses", | |
| authRequirement: "api-key", | |
| }); | |
| }); | |
| it("keeps an explicit SecretRef API key ahead of ordered API-key profiles", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIChatGptAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| " openai ": { | |
| auth: "api-key", | |
| apiKey: { source: "file", provider: "vault", id: "/openai/api-key" }, | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| secrets: { | |
| providers: { | |
| vault: { source: "file", path: "/tmp/openai-secrets.json", mode: "json" }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "openai:chatgpt": openAIOAuthProfile( | |
| "subscription-token", | |
| "refresh-token", | |
| Date.now() + 60_000, | |
| ), | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| }, | |
| { openai: ["openai:chatgpt", "openai:platform"] }, | |
| ), | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.forwardedAuthProfileCandidateIds).toBeUndefined(); | |
| expect(plan.selectedAuthMode).toBe("api-key"); | |
| expect(plan.modelRoute).toMatchObject({ | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key", | |
| }); | |
| }); | |
| it("keeps profile auth ahead of a literal provider apiKey fallback", async () => { | |
| const config = { | |
| models: { | |
| providers: { | |
| openai: { | |
| apiKey: "configured-platform-key", | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig; | |
| const store = authStore( | |
| { | |
| "openai:platform-backup": openAIApiKeyProfile("profile-platform-key"), | |
| }, | |
| { openai: ["openai:platform-backup"] }, | |
| ); | |
| const prepared = prepareAgentRuntimeAuth({ | |
| ...openAIChatGptAuthFixture(), | |
| config, | |
| env: {}, | |
| authProfileStore: store, | |
| }); | |
| const plan = prepared.plan; | |
| expect(plan.forwardedAuthProfileId).toBe("openai:platform-backup"); | |
| expect(plan.forwardedAuthProfileCandidateIds).toEqual(["openai:platform-backup"]); | |
| expect(plan.selectedAuthMode).toBe("api_key"); | |
| expect(plan.modelRoute).toMatchObject({ | |
| api: "openai-responses", | |
| authRequirement: "api-key", | |
| }); | |
| expect( | |
| prepared.attempts.map((attempt) => ({ | |
| kind: attempt.kind, | |
| profileId: attempt.profileId, | |
| allowAuthProfileFallback: attempt.allowAuthProfileFallback, | |
| requiresPriorProfileAttempt: attempt.requiresPriorProfileAttempt, | |
| forwardedAuthProfileId: attempt.plan.forwardedAuthProfileId, | |
| credentialSource: attempt.plan.credentialSource, | |
| })), | |
| ).toEqual([ | |
| { | |
| kind: "profile", | |
| profileId: "openai:platform-backup", | |
| allowAuthProfileFallback: undefined, | |
| requiresPriorProfileAttempt: undefined, | |
| forwardedAuthProfileId: "openai:platform-backup", | |
| credentialSource: { kind: "profile" }, | |
| }, | |
| { | |
| kind: "direct", | |
| profileId: undefined, | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: true, | |
| forwardedAuthProfileId: undefined, | |
| credentialSource: { | |
| kind: "direct", | |
| evidence: "provider-config", | |
| authorization: "declared", | |
| }, | |
| }, | |
| ]); | |
| expect(prepared.attempts[1]?.plan).toMatchObject({ | |
| selectedAuthMode: "api-key", | |
| modelRoute: { | |
| api: "openai-responses", | |
| authRequirement: "api-key", | |
| }, | |
| }); | |
| const model = { | |
| id: "gpt-5.5", | |
| name: "GPT-5.5", | |
| provider: "openai", | |
| api: plan.modelRoute?.api ?? "openai-responses", | |
| baseUrl: plan.modelRoute?.baseUrl ?? "https://api.openai.com/v1", | |
| reasoning: true, | |
| input: ["text", "image"], | |
| cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, | |
| contextWindow: 272_000, | |
| maxTokens: 128_000, | |
| } as Model; | |
| const profileAttempt = prepared.attempts[0]; | |
| const profileResolved = await getApiKeyForModelCore({ | |
| model, | |
| cfg: config, | |
| profileId: profileAttempt?.profileId, | |
| allowAuthProfileFallback: profileAttempt?.allowAuthProfileFallback, | |
| lockedProfile: true, | |
| store, | |
| }); | |
| expect(profileResolved).toMatchObject({ | |
| apiKey: "profile-platform-key", | |
| profileId: "openai:platform-backup", | |
| source: "profile:openai:platform-backup", | |
| mode: "api-key", | |
| }); | |
| }); | |
| it("does not unlock direct fallback when every prepared profile cools down before dispatch", () => { | |
| const store = authStore( | |
| { | |
| "openai:platform": openAIApiKeyProfile("profile-platform-key"), | |
| }, | |
| { openai: ["openai:platform"] }, | |
| ); | |
| const prepared = prepareAgentRuntimeAuth({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { apiKey: "configured-platform-key", baseUrl: "", models: [] }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: store, | |
| }); | |
| const profileAttempt = prepared.attempts[0]; | |
| const directAttempt = prepared.attempts[1]; | |
| if (profileAttempt?.kind !== "profile" || directAttempt?.kind !== "direct") { | |
| throw new Error("expected profile and direct attempts"); | |
| } | |
| store.usageStats = { | |
| "openai:platform": { cooldownUntil: Date.now() + 60_000 }, | |
| }; | |
| expect( | |
| preparedAgentRuntimeProfileAttemptHasCandidate({ | |
| attempt: profileAttempt, | |
| store, | |
| modelId: "gpt-5.5", | |
| }), | |
| ).toBe(false); | |
| expect( | |
| canRunPreparedAgentRuntimeAuthAttempt({ | |
| attempt: directAttempt, | |
| priorProfileAttempted: false, | |
| }), | |
| ).toBe(false); | |
| expect( | |
| canRunPreparedAgentRuntimeAuthAttempt({ | |
| attempt: directAttempt, | |
| priorProfileAttempted: true, | |
| }), | |
| ).toBe(true); | |
| }); | |
| // Zero-config still works: when a provider has no usable auth profile at all, | |
| // a bare `PROVIDER_API_KEY` remains the credential for the route. Refusing an | |
| // undeclared credential is about not letting it silently *succeed a declared | |
| // profile*, not about banning the documented zero-config path. | |
| it("still uses an undeclared env key when the provider has no usable profile", () => { | |
| const prepared = prepareAgentRuntimeAuth({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| env: { OPENAI_API_KEY: "ambient-platform-key" }, | |
| authProfileStore: authStore({}), | |
| }); | |
| expect(prepared.attempts).toMatchObject([{ kind: "direct" }]); | |
| expect(prepared.attempts.some((attempt) => attempt.kind === "profile")).toBe(false); | |
| expect(prepared.plan.credentialSource).toEqual({ | |
| kind: "direct", | |
| evidence: "environment", | |
| authorization: "ambient", | |
| }); | |
| }); | |
| // Declared apiKey material keeps normal direct-source standing, so the | |
| // narrowing is scoped to credentials that appear nowhere in config. | |
| it("still routes a declared provider apiKey with no profiles present", () => { | |
| const prepared = prepareAgentRuntimeAuth({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| config: { | |
| models: { | |
| providers: { openai: { apiKey: "configured-platform-key", baseUrl: "", models: [] } }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({}), | |
| }); | |
| expect(prepared.attempts).toMatchObject([{ kind: "direct" }]); | |
| expect(prepared.plan.credentialSource).toEqual({ | |
| kind: "direct", | |
| evidence: "provider-config", | |
| authorization: "declared", | |
| }); | |
| }); | |
| it("reports a local provider marker as synthetic auth", () => { | |
| const prepared = prepareAgentRuntimeAuth({ | |
| provider: "ollama-remote", | |
| modelId: "qwen3.5:27b", | |
| config: { | |
| models: { | |
| providers: { | |
| "ollama-remote": { | |
| api: "ollama", | |
| apiKey: "ollama-local", | |
| baseUrl: "http://192.168.178.122:11434", | |
| models: [ | |
| { | |
| id: "qwen3.5:27b", | |
| name: "Qwen 3.5 27B", | |
| reasoning: false, | |
| input: ["text"], | |
| cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, | |
| contextWindow: 8_192, | |
| maxTokens: 4_096, | |
| }, | |
| ], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({}), | |
| }); | |
| expect(prepared.plan.credentialSource).toEqual({ | |
| kind: "direct", | |
| evidence: "synthetic", | |
| authorization: "declared", | |
| }); | |
| }); | |
| // An environment credential named nowhere in config is not an authorized | |
| // route. `auth.order` filtering already refuses to silently try a *stored* | |
| // profile the operator omitted from the explicit order | |
| // (docs/auth-credential-semantics.md, "Explicit auth order filtering"), and | |
| // docs/providers/openai.md reserves bare `OPENAI_API_KEY` for non-agent | |
| // surfaces. An undeclared env key must therefore not be queued behind a | |
| // declared profile, where it would silently absorb that profile's failures — | |
| // potentially onto a different billing account. | |
| it.each([ | |
| { | |
| label: "ambient Platform key behind an OAuth profile", | |
| rejects: false, | |
| env: { OPENAI_API_KEY: "ambient-platform-key" }, | |
| profileId: "openai:chatgpt", | |
| profile: { | |
| type: "oauth" as const, | |
| provider: "openai", | |
| access: "subscription-token", | |
| refresh: "refresh-token", | |
| expires: Date.now() + 60_000, | |
| }, | |
| requirements: ["subscription"], | |
| }, | |
| { | |
| label: "ambient OAuth token behind an incompatible Platform profile", | |
| rejects: true, | |
| config: { | |
| models: { providers: { openai: { auth: "oauth", baseUrl: "", models: [] } } }, | |
| } as OpenClawConfig, | |
| env: { OPENAI_API_KEY: "ambient-oauth-token" }, | |
| profileId: "openai:platform", | |
| profile: { | |
| type: "api_key" as const, | |
| provider: "openai", | |
| key: "profile-platform-key", | |
| }, | |
| requirements: ["api-key"], | |
| }, | |
| ])("does not queue $label", ({ config, env, profile, profileId, requirements, rejects }) => { | |
| const prepare = () => | |
| prepareAgentRuntimeAuth({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| config, | |
| env, | |
| authProfileStore: authStore({ [profileId]: profile }, { openai: [profileId] }), | |
| }); | |
| if (rejects) { | |
| expect(prepare).toThrow("Explicit auth order for openai has no usable profiles."); | |
| return; | |
| } | |
| const prepared = prepare(); | |
| expect(prepared.attempts.map((attempt) => attempt.plan.modelRoute?.authRequirement)).toEqual( | |
| requirements, | |
| ); | |
| expect(prepared.attempts).toMatchObject([{ kind: "profile", profileId }]); | |
| expect(prepared.attempts.some((attempt) => attempt.kind === "direct")).toBe(false); | |
| }); | |
| it("resolves an env SecretRef on its prepared Platform route", async () => { | |
| vi.stubEnv("OPENAI_API_KEY", ""); | |
| vi.stubEnv("OPENAI_PLATFORM_KEY", "secret-ref-platform-key"); | |
| try { | |
| const config = { | |
| models: { | |
| providers: { | |
| openai: { | |
| apiKey: { source: "env", provider: "default", id: "OPENAI_PLATFORM_KEY" }, | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig; | |
| const store = authStore({}); | |
| const prepared = prepareAgentRuntimeAuth({ | |
| ...openAIChatGptAuthFixture(), | |
| config, | |
| env: process.env, | |
| authProfileStore: store, | |
| }); | |
| expect(prepared.attempts).toEqual([ | |
| { | |
| kind: "direct", | |
| plan: prepared.plan, | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: false, | |
| }, | |
| ]); | |
| expect(prepared.plan).toMatchObject({ | |
| forwardedAuthProfileId: undefined, | |
| selectedAuthMode: "api-key", | |
| credentialSource: { | |
| kind: "direct", | |
| evidence: "environment", | |
| authorization: "declared", | |
| }, | |
| modelRoute: { | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key", | |
| }, | |
| }); | |
| const resolved = await getApiKeyForModelCore({ | |
| model: { | |
| id: "gpt-5.5", | |
| name: "GPT-5.5", | |
| provider: "openai", | |
| api: prepared.plan.modelRoute?.api ?? "openai-responses", | |
| baseUrl: prepared.plan.modelRoute?.baseUrl ?? "https://api.openai.com/v1", | |
| reasoning: true, | |
| input: ["text", "image"], | |
| cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, | |
| contextWindow: 272_000, | |
| maxTokens: 128_000, | |
| } as Model, | |
| cfg: config, | |
| profileId: prepared.attempts[0]?.profileId, | |
| allowAuthProfileFallback: prepared.attempts[0]?.allowAuthProfileFallback, | |
| store, | |
| }); | |
| expect(resolved).toMatchObject({ | |
| apiKey: "secret-ref-platform-key", | |
| source: "env: OPENAI_PLATFORM_KEY (models.json secretref)", | |
| mode: "api-key", | |
| }); | |
| expect(resolved.profileId).toBeUndefined(); | |
| expect(JSON.stringify(prepared.plan.credentialSource)).not.toContain( | |
| "secret-ref-platform-key", | |
| ); | |
| expect(JSON.stringify(prepared.plan.credentialSource)).not.toContain("OPENAI_PLATFORM_KEY"); | |
| } finally { | |
| vi.unstubAllEnvs(); | |
| } | |
| }); | |
| it("keeps a provider apiKey SecretRef ahead of API-key-compatible profiles", () => { | |
| const prepared = prepareAgentRuntimeAuth({ | |
| ...openAIChatGptAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| apiKey: { source: "file", provider: "vault", id: "/openai/api-key" }, | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| secrets: { | |
| providers: { | |
| vault: { source: "file", path: "/tmp/openai-secrets.json", mode: "json" }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore( | |
| { | |
| "openai:chatgpt": openAIOAuthProfile( | |
| "subscription-token", | |
| "refresh-token", | |
| Date.now() + 10 * 60_000, | |
| ), | |
| "openai:platform": openAIApiKeyProfile("platform-key"), | |
| }, | |
| { openai: ["openai:chatgpt", "openai:platform"] }, | |
| ), | |
| }); | |
| expect(prepared.plan).toMatchObject({ | |
| forwardedAuthProfileId: undefined, | |
| selectedAuthMode: "api-key", | |
| modelRoute: { | |
| api: "openai-responses", | |
| authRequirement: "api-key", | |
| }, | |
| }); | |
| expect(prepared.attempts).toMatchObject([ | |
| { | |
| kind: "direct", | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: false, | |
| }, | |
| ]); | |
| }); | |
| it("uses explicit OAuth mode for literal provider material", () => { | |
| const prepared = prepareAgentRuntimeAuth({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "oauth", | |
| apiKey: "configured-oauth-token", | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({}), | |
| }); | |
| expect(prepared.plan).toMatchObject({ | |
| selectedAuthMode: "oauth", | |
| modelRoute: { | |
| api: "openai-chatgpt-responses", | |
| authRequirement: "subscription", | |
| }, | |
| }); | |
| expect(prepared.attempts).toMatchObject([ | |
| { | |
| kind: "direct", | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: false, | |
| }, | |
| ]); | |
| }); | |
| it("keeps configured OAuth direct material on the subscription route", () => { | |
| const prepared = prepareAgentRuntimeAuth({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "oauth", | |
| apiKey: "configured-oauth-token", | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({ | |
| "openai:platform": openAIApiKeyProfile("profile-platform-key"), | |
| }), | |
| }); | |
| expect(prepared.attempts.map((attempt) => attempt.plan.modelRoute?.authRequirement)).toEqual([ | |
| "subscription", | |
| ]); | |
| expect(prepared.attempts).toMatchObject([ | |
| { | |
| kind: "direct", | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: false, | |
| plan: { selectedAuthMode: "oauth" }, | |
| }, | |
| ]); | |
| }); | |
| it("preserves explicit provider token auth before auth.order or route defaults", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "token", | |
| apiKey: "configured-subscription-token", | |
| baseUrl: "", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({}), | |
| }); | |
| expect(plan.forwardedAuthProfileId).toBeUndefined(); | |
| expect(plan.selectedAuthMode).toBe("token"); | |
| expect(plan.modelRoute).toMatchObject({ | |
| api: "openai-chatgpt-responses", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| authRequirement: "subscription", | |
| }); | |
| }); | |
| it.each([ | |
| { | |
| auth: "oauth" as const, | |
| profile: { type: "api_key" as const, provider: "openai", key: "platform-key" }, | |
| requirement: "subscription", | |
| }, | |
| { | |
| auth: "api-key" as const, | |
| profile: { | |
| type: "oauth" as const, | |
| provider: "openai", | |
| access: "oauth-access", | |
| refresh: "oauth-refresh", | |
| expires: Date.now() + 60_000, | |
| }, | |
| requirement: "api-key", | |
| }, | |
| ])("rejects a $profile.type profile for configured $auth auth", ({ auth, profile }) => { | |
| expect(() => | |
| prepareAgentRuntimeAuth({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| config: { | |
| models: { providers: { openai: { auth, baseUrl: "", models: [] } } }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({ "openai:wrong-route": profile }), | |
| }), | |
| ).toThrow(/no compatible credential source/u); | |
| }); | |
| it("rejects configured harness-native auth without a compatible host source", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| config: { | |
| models: { providers: { openai: { auth: "oauth", baseUrl: "", models: [] } } }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({}), | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| harnessAuthBootstrap: "harness", | |
| }), | |
| ).toThrow(/no compatible credential source/u); | |
| }); | |
| it("rejects configured provider auth that contradicts an authored route", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| config: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "oauth", | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| env: {}, | |
| authProfileStore: authStore({}), | |
| }), | |
| ).toThrow(/not compatible/u); | |
| }); | |
| it("preserves an explicit environment endpoint in the selected route", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...openAIPlatformAuthFixture(), | |
| env: { | |
| OPENAI_API_KEY: "platform-key", | |
| OPENAI_BASE_URL: "https://relay.example.test/v1", | |
| }, | |
| authProfileStore: authStore({}), | |
| }); | |
| expect(plan.modelRoute).toEqual({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-responses", | |
| baseUrl: "https://relay.example.test/v1", | |
| authRequirement: "api-key", | |
| requestTransportOverrides: "none", | |
| runtimePolicy: { compatibleIds: ["openclaw"] }, | |
| }); | |
| }); | |
| it("keeps the live codex virtual provider on a generic either-auth plan", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...virtualCodexAuthFixture(), | |
| authProfileStore: authStore({ | |
| "openai:account": openAIOAuthProfile( | |
| "subscription-token", | |
| "refresh-token", | |
| Date.now() + 60_000, | |
| ), | |
| }), | |
| sessionAuthProfileId: "openai:account", | |
| sessionAuthProfileSource: "user", | |
| harnessId: "codex", | |
| harnessRuntime: "codex", | |
| }); | |
| expect(plan).toMatchObject({ | |
| providerForAuth: "codex", | |
| harnessAuthProvider: "openai", | |
| forwardedAuthProfileId: "openai:account", | |
| forwardedAuthProfileSource: "user", | |
| }); | |
| expect(plan.modelRoute).toBeUndefined(); | |
| }); | |
| it("resolves automatic virtual Codex profiles from the OpenAI auth order", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| ...virtualCodexAuthFixture(), | |
| authProfileStore: authStore( | |
| { | |
| "openai:p1": openAITokenProfile("p1-token"), | |
| "openai:p2": openAIApiKeyProfile("p2-key"), | |
| }, | |
| { openai: ["openai:p1", "openai:p2"] }, | |
| ), | |
| sessionAuthProfileId: "openai:p1", | |
| sessionAuthProfileSource: "auto", | |
| }); | |
| expect(plan).toMatchObject({ | |
| providerForAuth: "codex", | |
| harnessAuthProvider: "openai", | |
| forwardedAuthProfileId: "openai:p1", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:p1", "openai:p2"], | |
| selectedAuthMode: "token", | |
| }); | |
| expect(plan.modelRoute).toBeUndefined(); | |
| }); | |
| it("keeps same-provider retries behind a user-pinned virtual Codex profile", () => { | |
| const preparation = prepareAgentRuntimeAuth({ | |
| ...virtualCodexAuthFixture(), | |
| authProfileStore: authStore( | |
| { | |
| "openai:p1": openAITokenProfile("p1-token"), | |
| "openai:p2": openAIApiKeyProfile("p2-key"), | |
| }, | |
| { openai: ["openai:p2", "openai:p1"] }, | |
| ), | |
| sessionAuthProfileId: "openai:p1", | |
| sessionAuthProfileSource: "user", | |
| }); | |
| const profileAttempts = preparation.attempts.filter((attempt) => attempt.kind === "profile"); | |
| expect(profileAttempts.map((attempt) => attempt.profileId)).toEqual(["openai:p1", "openai:p2"]); | |
| expect(profileAttempts.map((attempt) => attempt.plan.forwardedAuthProfileSource)).toEqual([ | |
| "user", | |
| "auto", | |
| ]); | |
| }); | |
| it("rejects a user-pinned non-OpenAI profile on the virtual Codex provider", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...virtualCodexAuthFixture(), | |
| authProfileStore: authStore({ | |
| "anthropic:work": createApiKeyCredential("anthropic", "anthropic-key"), | |
| }), | |
| sessionAuthProfileId: "anthropic:work", | |
| sessionAuthProfileSource: "user", | |
| }), | |
| ).toThrow(/not configured for openai/u); | |
| }); | |
| it("keeps provider incompatibility for a config-only AWS SDK profile", () => { | |
| const profileId = "amazon-bedrock:default"; | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...virtualCodexAuthFixture(), | |
| config: { | |
| auth: { profiles: { [profileId]: { provider: "amazon-bedrock", mode: "aws-sdk" } } }, | |
| models: { | |
| providers: { | |
| "amazon-bedrock": { | |
| auth: "aws-sdk", | |
| baseUrl: "https://bedrock.example.test", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| }, | |
| authProfileStore: authStore({}), | |
| sessionAuthProfileId: profileId, | |
| sessionAuthProfileSource: "user", | |
| }), | |
| ).toThrow(/not configured for openai/u); | |
| }); | |
| it("rejects unavailable user-pinned OpenAI profiles on the virtual Codex provider", () => { | |
| expect(() => | |
| prepareAgentRuntimeAuthPlan({ | |
| ...virtualCodexAuthFixture(), | |
| config: { | |
| auth: { | |
| profiles: { | |
| "openai:missing": { provider: "openai", mode: "oauth" }, | |
| }, | |
| }, | |
| } as OpenClawConfig, | |
| authProfileStore: authStore({}), | |
| sessionAuthProfileId: "openai:missing", | |
| sessionAuthProfileSource: "user", | |
| }), | |
| ).toThrow( | |
| expect.objectContaining({ | |
| code: "selected_auth_profile_unavailable", | |
| reason: "auth", | |
| status: 401, | |
| }), | |
| ); | |
| }); | |
| it("does not reuse a routed plan across compaction model overrides", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| env: { OPENAI_API_KEY: "platform-key" }, | |
| authProfileStore: authStore({}), | |
| }); | |
| expect( | |
| agentRuntimeAuthPlanMatchesTarget(plan, { provider: "openai", modelId: "gpt-5.5" }), | |
| ).toBe(true); | |
| expect( | |
| agentRuntimeAuthPlanMatchesTarget(plan, { provider: "openai", modelId: "gpt-5.6" }), | |
| ).toBe(false); | |
| }); | |
| it("does not reuse a generic plan across model-scoped auth decisions", () => { | |
| const plan = prepareAgentRuntimeAuthPlan({ | |
| provider: "anthropic", | |
| modelId: "claude-sonnet-4-6", | |
| env: {}, | |
| authProfileStore: authStore({}), | |
| }); | |
| expect( | |
| agentRuntimeAuthPlanMatchesTarget(plan, { | |
| provider: "anthropic", | |
| modelId: "claude-sonnet-4-6", | |
| }), | |
| ).toBe(true); | |
| expect( | |
| agentRuntimeAuthPlanMatchesTarget(plan, { | |
| provider: "anthropic", | |
| modelId: "claude-opus-4-6", | |
| }), | |
| ).toBe(false); | |
| }); | |
| }); | |
| /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ | |