Download src/agents/runtime-plan/resolve-auth.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 26.1 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/runtime-plan/resolve-auth.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/agents/runtime-plan/resolve-auth.test.ts
-
curl -L -o resolve-auth.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/runtime-plan/resolve-auth.test.ts
26.1 kB
| import type { Model } from "openclaw/plugin-sdk/llm"; | |
| import { afterEach, aroundEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import { createPluginMetadataSnapshotFixture } from "../../plugins/plugin-metadata.test-support.js"; | |
| import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/generation-scope.js"; | |
| import { SecretSurfaceUnavailableError } from "../../secrets/runtime-degraded-state.js"; | |
| import type { AuthProfileStore } from "../auth-profiles.js"; | |
| import { createApiKeyCredential } from "../auth-profiles/credential-fixtures.test-support.js"; | |
| import { OAuthRefreshFailureError } from "../auth-profiles/oauth-refresh-failure.js"; | |
| import { | |
| resolvePreparedRuntimeAuthAttempts, | |
| resolvePreparedRuntimeModelAuth, | |
| scopeAuthProfileStoreToPreparedPlan, | |
| } from "./resolve-auth.js"; | |
| vi.mock("../model-auth-env-vars.js", async (importOriginal) => ({ | |
| ...(await importOriginal<typeof import("../model-auth-env-vars.js")>()), | |
| resolveProviderEnvAuthLookupMaps: () => ({ | |
| aliasMap: {}, | |
| envCandidateMap: { openai: ["OPENAI_API_KEY", "OPENAI_OAUTH_TOKEN"] }, | |
| authEvidenceMap: {}, | |
| setupProviderFallbackRefs: [], | |
| }), | |
| })); | |
| vi.mock("../../llm/oauth.js", async (importOriginal) => ({ | |
| ...(await importOriginal<typeof import("../../llm/oauth.js")>()), | |
| getOAuthApiKey: vi.fn(async () => { | |
| throw new Error("invalid_grant"); | |
| }), | |
| })); | |
| const platformModel = { | |
| id: "gpt-5.5", | |
| name: "gpt-5.5", | |
| provider: "openai", | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| input: ["text"], | |
| reasoning: true, | |
| cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, | |
| contextWindow: 128_000, | |
| maxTokens: 8_000, | |
| } as Model; | |
| const subscriptionModel = { | |
| ...platformModel, | |
| api: "openai-chatgpt-responses", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| } as Model; | |
| function authStore(profiles: AuthProfileStore["profiles"]): AuthProfileStore { | |
| return { version: 1, profiles }; | |
| } | |
| describe("resolvePreparedRuntimeModelAuth", () => { | |
| aroundEach((runTest) => | |
| withPluginRuntimeGenerationScope( | |
| { | |
| metadataSnapshot: createPluginMetadataSnapshotFixture({ | |
| plugins: [{ id: "openai", providers: ["openai"] }], | |
| }), | |
| }, | |
| runTest, | |
| ), | |
| ); | |
| beforeEach(() => { | |
| vi.stubEnv("OPENCLAW_TEST_MISSING_PREPARED_AUTH", ""); | |
| vi.stubEnv("OPENCLAW_TEST_MISSING_BOUND_AUTH", ""); | |
| }); | |
| afterEach(() => { | |
| vi.unstubAllEnvs(); | |
| }); | |
| it("removes profile and selection state after a Platform key is resolved", () => { | |
| const store = { | |
| ...authStore({ | |
| "openai:subscription": { | |
| type: "token", | |
| provider: "openai", | |
| token: "subscription-token", | |
| expires: Date.now() + 60_000, | |
| }, | |
| "openai:platform": createApiKeyCredential("openai", "platform-key"), | |
| }), | |
| order: { openai: ["openai:subscription", "openai:platform"] }, | |
| lastGood: { openai: "openai:subscription" }, | |
| usageStats: { "openai:subscription": { lastUsed: 1 } }, | |
| runtimePersistedProfileIds: ["openai:subscription"], | |
| runtimeExternalProfileIds: ["openai:subscription"], | |
| runtimeExternalProfileIdsAuthoritative: true, | |
| } satisfies AuthProfileStore; | |
| expect( | |
| scopeAuthProfileStoreToPreparedPlan(store, { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:platform", | |
| forwardedAuthProfileCandidateIds: ["openai:platform"], | |
| selectedAuthMode: "api-key", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.6", | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key", | |
| requestTransportOverrides: "none", | |
| }, | |
| }), | |
| ).toMatchObject({ | |
| profiles: {}, | |
| order: { openai: [] }, | |
| lastGood: {}, | |
| usageStats: {}, | |
| runtimePersistedProfileIds: [], | |
| runtimeExternalProfileIds: [], | |
| runtimeExternalProfileIdsAuthoritative: true, | |
| }); | |
| }); | |
| it("keeps a failed explicit SecretRef terminal across prepared profile candidates", async () => { | |
| const store = authStore({ | |
| "openai:missing": { | |
| type: "api_key", | |
| provider: "openai", | |
| keyRef: { | |
| source: "env", | |
| provider: "default", | |
| id: "OPENCLAW_TEST_MISSING_PREPARED_AUTH", | |
| }, | |
| }, | |
| "openai:backup": createApiKeyCredential("openai", "backup-key"), | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:missing", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:missing", "openai:backup"], | |
| selectedAuthMode: "api_key", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key", | |
| requestTransportOverrides: "none", | |
| }, | |
| }, | |
| model: platformModel, | |
| cfg: {}, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).rejects.toMatchObject({ | |
| code: "SECRET_SURFACE_UNAVAILABLE", | |
| ownerKind: "account", | |
| }); | |
| }); | |
| it( | |
| "does not borrow an unprepared API-key profile for direct subscription auth", | |
| { timeout: 1_000 }, | |
| async () => { | |
| vi.stubEnv("OPENAI_API_KEY", ""); | |
| const store = authStore({ | |
| "openai:platform": createApiKeyCredential("openai", "platform-key"), | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| selectedAuthMode: "token", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-chatgpt-responses", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| authRequirement: "subscription", | |
| requestTransportOverrides: "none", | |
| }, | |
| }, | |
| model: subscriptionModel, | |
| cfg: {}, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).rejects.toThrow('No API key found for provider "openai"'); | |
| }, | |
| ); | |
| it("resolves an ambient Platform key without borrowing the OAuth-only full store", async () => { | |
| vi.stubEnv("OPENAI_API_KEY", "ambient-platform-key"); | |
| const store = authStore({ | |
| "openai:chatgpt": { | |
| type: "oauth", | |
| provider: "openai", | |
| access: "subscription-token", | |
| refresh: "refresh-token", | |
| expires: Date.now() + 60_000, | |
| }, | |
| }); | |
| const resolved = await resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| selectedAuthMode: "api-key", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key", | |
| requestTransportOverrides: "none", | |
| }, | |
| }, | |
| model: platformModel, | |
| cfg: {}, | |
| store, | |
| secretSentinels: true, | |
| }); | |
| expect(resolved).toMatchObject({ | |
| auth: { | |
| apiKey: "ambient-platform-key", | |
| mode: "api-key", | |
| }, | |
| plan: { | |
| forwardedAuthProfileId: undefined, | |
| selectedAuthMode: "api-key", | |
| }, | |
| }); | |
| expect(resolved.auth.source).toContain("OPENAI_API_KEY"); | |
| }); | |
| it("keeps authored unpinned provider auth ahead of an opposite-route store", async () => { | |
| const store = authStore({ | |
| "openai:chatgpt": { | |
| type: "token", | |
| provider: "openai", | |
| token: "subscription-token", | |
| }, | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| selectedAuthMode: "api-key", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key", | |
| requestTransportOverrides: "none", | |
| }, | |
| }, | |
| model: platformModel, | |
| cfg: { | |
| models: { | |
| providers: { | |
| openai: { | |
| apiKey: "configured-platform-key", | |
| baseUrl: "https://api.openai.com/v1", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| }, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).resolves.toMatchObject({ auth: { mode: "api-key" } }); | |
| }); | |
| it("materializes authored OpenAI oauth without borrowing the API-only full store", async () => { | |
| vi.stubEnv("OPENAI_API_KEY", ""); | |
| const store = authStore({ | |
| "openai:platform": createApiKeyCredential("openai", "platform-key"), | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| selectedAuthMode: "oauth", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-chatgpt-responses", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| authRequirement: "subscription", | |
| requestTransportOverrides: "none", | |
| }, | |
| }, | |
| model: subscriptionModel, | |
| cfg: { | |
| models: { | |
| providers: { | |
| openai: { | |
| auth: "oauth", | |
| apiKey: "configured-subscription-credential", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| models: [], | |
| }, | |
| }, | |
| }, | |
| }, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).resolves.toMatchObject({ | |
| auth: { | |
| apiKey: "configured-subscription-credential", | |
| source: "models.json", | |
| mode: "oauth", | |
| }, | |
| plan: { | |
| selectedAuthMode: "oauth", | |
| modelRoute: { authRequirement: "subscription" }, | |
| }, | |
| }); | |
| }); | |
| it("skips a prepared candidate whose stored credential class changed", async () => { | |
| const store = authStore({ | |
| "openai:changed": createApiKeyCredential("openai", "platform-key"), | |
| "openai:backup": { | |
| type: "token", | |
| provider: "openai", | |
| token: "subscription-token", | |
| expires: Date.now() + 60_000, | |
| }, | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:changed", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:changed", "openai:backup"], | |
| selectedAuthMode: "token", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-chatgpt-responses", | |
| baseUrl: "https://chatgpt.com/backend-api/codex", | |
| authRequirement: "subscription", | |
| requestTransportOverrides: "none", | |
| }, | |
| }, | |
| model: subscriptionModel, | |
| cfg: {}, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).resolves.toMatchObject({ | |
| auth: { profileId: "openai:backup", mode: "token" }, | |
| plan: { | |
| forwardedAuthProfileId: "openai:backup", | |
| forwardedAuthProfileCandidateIds: ["openai:backup"], | |
| selectedAuthMode: "token", | |
| }, | |
| }); | |
| }); | |
| it("falls through from a stale OAuth automatic candidate to a prepared backup profile", async () => { | |
| const store = authStore({ | |
| "openai:expired": { | |
| type: "oauth", | |
| provider: "openai", | |
| access: "expired-access", | |
| refresh: "expired-refresh", | |
| expires: Date.now() - 60_000, | |
| }, | |
| "openai:backup": createApiKeyCredential("openai", "backup-key"), | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:expired", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:expired", "openai:backup"], | |
| selectedAuthMode: "api_key", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-responses", | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key", | |
| requestTransportOverrides: "none", | |
| }, | |
| }, | |
| model: platformModel, | |
| cfg: {}, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).resolves.toMatchObject({ | |
| auth: { profileId: "openai:backup", mode: "api-key" }, | |
| plan: { | |
| forwardedAuthProfileId: "openai:backup", | |
| forwardedAuthProfileCandidateIds: ["openai:backup"], | |
| selectedAuthMode: "api-key", | |
| }, | |
| }); | |
| }); | |
| it("skips an automatic candidate that cooled down after plan preparation", async () => { | |
| const store = authStore({ | |
| "openai:first": createApiKeyCredential("openai", "first-key"), | |
| "openai:backup": createApiKeyCredential("openai", "backup-key"), | |
| }); | |
| const plan = { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:first", | |
| forwardedAuthProfileSource: "auto" as const, | |
| forwardedAuthProfileCandidateIds: ["openai:first", "openai:backup"], | |
| selectedAuthMode: "api_key", | |
| modelRoute: { | |
| provider: "openai", | |
| modelId: "gpt-5.5", | |
| api: "openai-responses" as const, | |
| baseUrl: "https://api.openai.com/v1", | |
| authRequirement: "api-key" as const, | |
| requestTransportOverrides: "none" as const, | |
| }, | |
| }; | |
| store.usageStats = { | |
| "openai:first": { | |
| cooldownUntil: Date.now() + 60_000, | |
| cooldownReason: "rate_limit", | |
| cooldownModel: "gpt-5.5", | |
| }, | |
| }; | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan, | |
| model: platformModel, | |
| cfg: {}, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).resolves.toMatchObject({ | |
| auth: { profileId: "openai:backup" }, | |
| plan: { | |
| forwardedAuthProfileId: "openai:backup", | |
| forwardedAuthProfileCandidateIds: ["openai:backup"], | |
| }, | |
| }); | |
| }); | |
| it("fails closed when every prepared automatic candidate is in cooldown", async () => { | |
| const store = authStore({ | |
| "openai:first": { type: "api_key", provider: "openai", key: "first-key" }, | |
| "openai:backup": { type: "api_key", provider: "openai", key: "backup-key" }, | |
| }); | |
| store.usageStats = Object.fromEntries( | |
| ["openai:first", "openai:backup"].map((profileId) => [ | |
| profileId, | |
| { | |
| cooldownUntil: Date.now() + 60_000, | |
| cooldownReason: "rate_limit" as const, | |
| cooldownModel: "gpt-5.5", | |
| }, | |
| ]), | |
| ); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:first", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:first", "openai:backup"], | |
| }, | |
| model: platformModel, | |
| cfg: {}, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).rejects.toThrow("temporarily unavailable"); | |
| }); | |
| it("does not unlock direct fallback when a profile cools during materialization", async () => { | |
| const store = authStore({ | |
| "openai:first": { type: "api_key", provider: "openai", key: "first-key" }, | |
| }); | |
| const profilePlan = { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:first", | |
| forwardedAuthProfileSource: "auto" as const, | |
| forwardedAuthProfileCandidateIds: ["openai:first"], | |
| }; | |
| const directPlan = { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| }; | |
| const resolveAuth = vi.fn(async () => ({ plan: directPlan, auth: "unused" })); | |
| const materializeModel = vi.fn(async () => { | |
| store.usageStats = { | |
| "openai:first": { cooldownUntil: Date.now() + 60_000 }, | |
| }; | |
| return platformModel; | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeAuthAttempts({ | |
| attempts: [ | |
| { kind: "profile", plan: profilePlan, profileId: "openai:first" }, | |
| { | |
| kind: "direct", | |
| plan: directPlan, | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: true, | |
| }, | |
| ], | |
| store, | |
| modelId: "gpt-5.5", | |
| model: platformModel, | |
| materializeModel, | |
| resolveAuth, | |
| errorMessage: "prepared auth failed", | |
| }), | |
| ).rejects.toThrow("temporarily unavailable"); | |
| expect(materializeModel).toHaveBeenCalledOnce(); | |
| expect(resolveAuth).not.toHaveBeenCalled(); | |
| }); | |
| it("does not unlock another prepared attempt after an explicit profile ref fails", async () => { | |
| const profilePlan = { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:cold", | |
| forwardedAuthProfileSource: "auto" as const, | |
| forwardedAuthProfileCandidateIds: ["openai:cold"], | |
| }; | |
| const directPlan = { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| }; | |
| const unavailable = new SecretSurfaceUnavailableError({ | |
| ownerKind: "account", | |
| ownerId: "openai:cold", | |
| state: "unavailable", | |
| paths: ["auth-profiles.openai:cold.key"], | |
| refKeys: ["env:default:MISSING_OPENAI_KEY"], | |
| reason: "secret reference was not found", | |
| }); | |
| const resolveAuth = vi.fn(async ({ attempt }: { attempt: { kind: string } }) => { | |
| if (attempt.kind === "profile") { | |
| throw unavailable; | |
| } | |
| return { plan: directPlan, auth: "must-not-be-used" }; | |
| }); | |
| const materializeModel = vi.fn(async () => platformModel); | |
| await expect( | |
| resolvePreparedRuntimeAuthAttempts({ | |
| attempts: [ | |
| { kind: "profile", plan: profilePlan, profileId: "openai:cold" }, | |
| { | |
| kind: "direct", | |
| plan: directPlan, | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: true, | |
| }, | |
| ], | |
| store: authStore({ | |
| "openai:cold": { type: "api_key", provider: "openai", key: "unused" }, | |
| }), | |
| modelId: "gpt-5.5", | |
| model: platformModel, | |
| materializeModel, | |
| resolveAuth, | |
| errorMessage: "prepared auth failed", | |
| }), | |
| ).rejects.toBe(unavailable); | |
| expect(resolveAuth).toHaveBeenCalledOnce(); | |
| expect(materializeModel).toHaveBeenCalledOnce(); | |
| }); | |
| it("does not unlock direct fallback after a prepared OAuth refresh failure", async () => { | |
| const profilePlan = { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:oauth", | |
| forwardedAuthProfileSource: "auto" as const, | |
| forwardedAuthProfileCandidateIds: ["openai:oauth"], | |
| }; | |
| const directPlan = { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| selectedAuthMode: "api-key", | |
| }; | |
| const refreshFailure = new OAuthRefreshFailureError({ | |
| provider: "openai", | |
| profileId: "openai:oauth", | |
| message: "OAuth token refresh failed for openai: expired refresh credential", | |
| }); | |
| const resolveAuth = vi.fn(async ({ attempt }: { attempt: { kind: string } }) => { | |
| if (attempt.kind === "profile") { | |
| throw refreshFailure; | |
| } | |
| return { plan: directPlan, auth: "must-not-be-used" }; | |
| }); | |
| const materializeModel = vi.fn(async () => platformModel); | |
| await expect( | |
| resolvePreparedRuntimeAuthAttempts({ | |
| attempts: [ | |
| { kind: "profile", plan: profilePlan, profileId: "openai:oauth" }, | |
| { | |
| kind: "direct", | |
| plan: directPlan, | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: true, | |
| }, | |
| ], | |
| store: authStore({ | |
| "openai:oauth": { | |
| type: "oauth", | |
| provider: "openai", | |
| access: "expired-access", | |
| refresh: "expired-refresh", | |
| expires: Date.now() - 60_000, | |
| }, | |
| }), | |
| modelId: "gpt-5.5", | |
| model: platformModel, | |
| materializeModel, | |
| resolveAuth, | |
| errorMessage: "prepared auth failed", | |
| }), | |
| ).rejects.toBe(refreshFailure); | |
| expect(resolveAuth).toHaveBeenCalledOnce(); | |
| expect(materializeModel).toHaveBeenCalledOnce(); | |
| }); | |
| it("forces unscoped model rematerialization for direct fallback after profile failure", async () => { | |
| const store = authStore({ | |
| "github-copilot:first": { | |
| type: "token", | |
| provider: "github-copilot", | |
| token: "profile-token", | |
| }, | |
| }); | |
| const profilePlan = { | |
| providerForAuth: "github-copilot", | |
| authProfileProviderForAuth: "github-copilot", | |
| forwardedAuthProfileId: "github-copilot:first", | |
| forwardedAuthProfileSource: "auto" as const, | |
| forwardedAuthProfileCandidateIds: ["github-copilot:first"], | |
| }; | |
| const directPlan = { | |
| providerForAuth: "github-copilot", | |
| authProfileProviderForAuth: "github-copilot", | |
| selectedAuthMode: "token", | |
| }; | |
| const profileModel = { ...platformModel, contextWindow: 1_050_000 }; | |
| const directModel = { ...platformModel, contextWindow: 400_000 }; | |
| const materializeModel = vi.fn(async ({ forceResolve }: { forceResolve?: boolean }) => | |
| forceResolve ? directModel : profileModel, | |
| ); | |
| const resolveAuth = vi.fn( | |
| async ({ attempt }: { attempt: { kind: "direct" | "implicit" | "profile" } }) => { | |
| if (attempt.kind === "profile") { | |
| throw new Error("profile credential failed"); | |
| } | |
| return { plan: directPlan, auth: "direct" }; | |
| }, | |
| ); | |
| const result = await resolvePreparedRuntimeAuthAttempts({ | |
| attempts: [ | |
| { kind: "profile", plan: profilePlan, profileId: "github-copilot:first" }, | |
| { | |
| kind: "direct", | |
| plan: directPlan, | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: true, | |
| }, | |
| ], | |
| store, | |
| modelId: "gpt-5.6-sol", | |
| model: platformModel, | |
| materializeModel, | |
| resolveAuth, | |
| errorMessage: "prepared auth failed", | |
| }); | |
| expect(result).toMatchObject({ auth: "direct", model: { contextWindow: 400_000 } }); | |
| expect(materializeModel.mock.calls.map(([input]) => input.forceResolve)).toEqual([false, true]); | |
| }); | |
| it("forces the first direct model when provider metadata is credential-scoped", async () => { | |
| const directPlan = { | |
| providerForAuth: "github-copilot", | |
| authProfileProviderForAuth: "github-copilot", | |
| selectedAuthMode: "api-key", | |
| }; | |
| const directModel = { ...platformModel, contextWindow: 1_050_000 }; | |
| const materializeModel = vi.fn(async () => directModel); | |
| const result = await resolvePreparedRuntimeAuthAttempts({ | |
| attempts: [ | |
| { | |
| kind: "direct", | |
| plan: directPlan, | |
| allowAuthProfileFallback: false, | |
| requiresPriorProfileAttempt: false, | |
| }, | |
| ], | |
| store: authStore({}), | |
| modelId: "gpt-5.6-sol", | |
| model: platformModel, | |
| materializeModel, | |
| resolveAuth: async () => ({ plan: directPlan, auth: "direct" }), | |
| forceCredentialScopedDirectModelResolve: true, | |
| errorMessage: "prepared auth failed", | |
| }); | |
| expect(result.model).toBe(directModel); | |
| expect(materializeModel).toHaveBeenCalledWith({ | |
| plan: directPlan, | |
| model: platformModel, | |
| forceResolve: true, | |
| }); | |
| }); | |
| it("keeps a single bound prepared profile terminal", async () => { | |
| const store = authStore({ | |
| "openai:bound": { | |
| type: "api_key", | |
| provider: "openai", | |
| keyRef: { | |
| source: "env", | |
| provider: "default", | |
| id: "OPENCLAW_TEST_MISSING_BOUND_AUTH", | |
| }, | |
| }, | |
| "openai:unbound": createApiKeyCredential("openai", "must-not-be-borrowed"), | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:bound", | |
| forwardedAuthProfileSource: "auto", | |
| forwardedAuthProfileCandidateIds: ["openai:bound"], | |
| }, | |
| model: platformModel, | |
| cfg: { | |
| auth: { order: { openai: ["openai:bound", "openai:unbound"] } }, | |
| }, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).rejects.toThrow(); | |
| }); | |
| it("keeps a user-locked profile terminal when environment auth is also present", async () => { | |
| vi.stubEnv("OPENAI_API_KEY", "ambient-key"); | |
| const store = authStore({ | |
| "openai:locked": { | |
| type: "api_key", | |
| provider: "openai", | |
| key: "codex-app-server", | |
| }, | |
| }); | |
| await expect( | |
| resolvePreparedRuntimeModelAuth({ | |
| plan: { | |
| providerForAuth: "openai", | |
| authProfileProviderForAuth: "openai", | |
| forwardedAuthProfileId: "openai:locked", | |
| forwardedAuthProfileSource: "user", | |
| forwardedAuthProfileCandidateIds: ["openai:locked"], | |
| }, | |
| model: platformModel, | |
| cfg: {}, | |
| store, | |
| secretSentinels: true, | |
| }), | |
| ).resolves.toMatchObject({ | |
| auth: { profileId: "openai:locked" }, | |
| plan: { | |
| forwardedAuthProfileId: "openai:locked", | |
| forwardedAuthProfileSource: "user", | |
| forwardedAuthProfileCandidateIds: ["openai:locked"], | |
| }, | |
| }); | |
| }); | |
| }); | |