openclaw / src /agents /runtime-plan /resolve-auth.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
e249c6d verified
Raw History Blame Contribute Delete
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"],
},
});
});
});