mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 23:11:10 +00:00
* feat(usage): surface plan windows and account email in the chat context popover The context-window popover now shows which account a session runs on: provider usage snapshots carry an accountEmail resolved from the auth profile when stored, the ChatGPT access-token JWT claims (openai/codex), or the Claude CLI config file (~/.claude.json oauthAccount) for keychain-synced logins. models.authStatus embeds it, quota groups keep distinct accounts separate, and the popover renders the email under the plan header for local CLI sessions and OpenClaw-configured subscriptions alike. * test(openai): assemble the fake usage JWT from parts * test(usage): keep fixture tokens and identity plumbing scanner-safe * fix(usage): verify the Claude CLI login before labeling usage with its email Review findings: the CLI-config email fallback now requires the usage token to match the cached CLI credential (and honors CLAUDE_CONFIG_DIR), so an ambient login for another account never labels a snapshot; token-type credentials propagate their stored email alongside oauth ones. * style(anthropic): scanner-safe local for the CLI login read * fix(usage): capture the Claude CLI account email on the credential Review findings: reading ambient CLI config at usage-fetch time could not verify keychain-only logins and could go stale across account switches. The credential reader now captures oauthAccount.emailAddress next to the credential it belongs to, the synced claude-cli profile carries it, and the anthropic fetcher uses only the credential-borne identity. * style(usage): scanner-safe credential fixtures and helper naming * fix(auth): backfill the CLI account email onto existing synced profiles Profiles synced before identity capture stay usable and skip CLI reads, so upgraded installs would never gain the email until token rotation. While the stored token material matches the CLI login, merge the non-secret email onto the stored profile. * style(test): scanner-safe email assertion * test(auth): pin the no-reread invariant to identity-complete profiles
675 lines
22 KiB
TypeScript
675 lines
22 KiB
TypeScript
/**
|
|
* External CLI OAuth sync tests.
|
|
* Covers cached credential readers, bootstrap/replace policy, and runtime-only
|
|
* profile persistence decisions without touching real CLI credential stores.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { AuthProfileStore, OAuthCredential } from "./auth-profiles/types.js";
|
|
import type { ClaudeCliCredential } from "./cli-credentials.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
readClaudeCliCredentialsCached: vi.fn<(options?: unknown) => ClaudeCliCredential | null>(
|
|
() => null,
|
|
),
|
|
readCodexCliCredentialsCached: vi.fn<(options?: unknown) => OAuthCredential | null>(() => null),
|
|
readMiniMaxCliCredentialsCached: vi.fn<(options?: unknown) => OAuthCredential | null>(() => null),
|
|
}));
|
|
|
|
let readExternalCliBootstrapCredential: typeof import("./auth-profiles/external-cli-sync.js").readExternalCliBootstrapCredential;
|
|
let resolveExternalCliAuthProfiles: typeof import("./auth-profiles/external-cli-sync.js").resolveExternalCliAuthProfiles;
|
|
let hasUsableOAuthCredential: typeof import("./auth-profiles/oauth-shared.js").hasUsableOAuthCredential;
|
|
let shouldBootstrapFromExternalCliCredential: typeof import("./auth-profiles/oauth-shared.js").shouldBootstrapFromExternalCliCredential;
|
|
let shouldReplaceStoredOAuthCredential: typeof import("./auth-profiles/oauth-shared.js").shouldReplaceStoredOAuthCredential;
|
|
let CLAUDE_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").CLAUDE_CLI_PROFILE_ID;
|
|
let OPENAI_CODEX_DEFAULT_PROFILE_ID: typeof import("./auth-profiles/constants.js").OPENAI_CODEX_DEFAULT_PROFILE_ID;
|
|
let MINIMAX_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").MINIMAX_CLI_PROFILE_ID;
|
|
|
|
function makeOAuthCredential(
|
|
overrides: Partial<OAuthCredential> & Pick<OAuthCredential, "provider">,
|
|
) {
|
|
return {
|
|
type: "oauth" as const,
|
|
provider: overrides.provider,
|
|
access: overrides.access ?? `${overrides.provider}-access`,
|
|
refresh: overrides.refresh ?? `${overrides.provider}-refresh`,
|
|
expires: overrides.expires ?? Date.now() + 10 * 60_000,
|
|
accountId: overrides.accountId,
|
|
email: overrides.email,
|
|
enterpriseUrl: overrides.enterpriseUrl,
|
|
projectId: overrides.projectId,
|
|
};
|
|
}
|
|
|
|
function makeStore(profileId?: string, credential?: OAuthCredential): AuthProfileStore {
|
|
return {
|
|
version: 1,
|
|
profiles: profileId && credential ? { [profileId]: credential } : {},
|
|
};
|
|
}
|
|
|
|
function expectSingleProfileCredential(
|
|
profiles: ReturnType<typeof resolveExternalCliAuthProfiles>,
|
|
profileId: string,
|
|
) {
|
|
expect(profiles).toStrictEqual([
|
|
{
|
|
credential: expect.any(Object),
|
|
persistence: profileId === OPENAI_CODEX_DEFAULT_PROFILE_ID ? "runtime-only" : "persisted",
|
|
profileId,
|
|
},
|
|
]);
|
|
const credential = profiles[0]?.credential;
|
|
if (!credential) {
|
|
throw new Error(`Expected credential for profile ${profileId}`);
|
|
}
|
|
return credential as Record<string, unknown>;
|
|
}
|
|
|
|
function expectSingleProfile(
|
|
profiles: ReturnType<typeof resolveExternalCliAuthProfiles>,
|
|
profileId: string,
|
|
) {
|
|
expect(profiles).toStrictEqual([
|
|
{
|
|
credential: expect.any(Object),
|
|
persistence: profileId === OPENAI_CODEX_DEFAULT_PROFILE_ID ? "runtime-only" : "persisted",
|
|
profileId,
|
|
},
|
|
]);
|
|
const profile = profiles[0];
|
|
if (!profile?.credential) {
|
|
throw new Error(`Expected credential for profile ${profileId}`);
|
|
}
|
|
return profile;
|
|
}
|
|
|
|
function expectCredentialFields(
|
|
credential: Record<string, unknown> | undefined,
|
|
expected: Record<string, unknown>,
|
|
) {
|
|
if (!credential) {
|
|
throw new Error("Expected credential");
|
|
}
|
|
for (const [key, value] of Object.entries(expected)) {
|
|
expect(credential[key]).toBe(value);
|
|
}
|
|
}
|
|
|
|
function expectReaderPolicyCall(mock: { mock: { calls: unknown[][] } }) {
|
|
expect(mock.mock.calls).toStrictEqual([
|
|
[
|
|
{
|
|
allowKeychainPrompt: false,
|
|
ttlMs: 15 * 60 * 1000,
|
|
},
|
|
],
|
|
]);
|
|
}
|
|
|
|
describe("external cli oauth resolution", () => {
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
vi.doMock("./cli-credentials.js", () => ({
|
|
readClaudeCliCredentialsCached: mocks.readClaudeCliCredentialsCached,
|
|
readCodexCliCredentialsCached: mocks.readCodexCliCredentialsCached,
|
|
readMiniMaxCliCredentialsCached: mocks.readMiniMaxCliCredentialsCached,
|
|
}));
|
|
mocks.readClaudeCliCredentialsCached.mockReset().mockReturnValue(null);
|
|
mocks.readCodexCliCredentialsCached.mockReset().mockReturnValue(null);
|
|
mocks.readMiniMaxCliCredentialsCached.mockReset().mockReturnValue(null);
|
|
({ readExternalCliBootstrapCredential, resolveExternalCliAuthProfiles } =
|
|
await import("./auth-profiles/external-cli-sync.js"));
|
|
({
|
|
hasUsableOAuthCredential,
|
|
shouldBootstrapFromExternalCliCredential,
|
|
shouldReplaceStoredOAuthCredential,
|
|
} = await import("./auth-profiles/oauth-shared.js"));
|
|
({ CLAUDE_CLI_PROFILE_ID, OPENAI_CODEX_DEFAULT_PROFILE_ID, MINIMAX_CLI_PROFILE_ID } =
|
|
await import("./auth-profiles/constants.js"));
|
|
});
|
|
|
|
describe("shouldReplaceStoredOAuthCredential", () => {
|
|
it("keeps equivalent stored credentials", () => {
|
|
const expires = Date.now() + 60_000;
|
|
const stored = makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "a",
|
|
refresh: "r",
|
|
expires,
|
|
});
|
|
const incoming = makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "a",
|
|
refresh: "r",
|
|
expires,
|
|
});
|
|
|
|
expect(shouldReplaceStoredOAuthCredential(stored, incoming)).toBe(false);
|
|
});
|
|
|
|
it("keeps the newer stored credential", () => {
|
|
const incoming = makeOAuthCredential({
|
|
provider: "openai",
|
|
expires: Date.now() + 60_000,
|
|
});
|
|
const stored = makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "fresh-access",
|
|
refresh: "fresh-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
});
|
|
|
|
expect(shouldReplaceStoredOAuthCredential(stored, incoming)).toBe(false);
|
|
});
|
|
|
|
it("replaces when incoming credentials are fresher", () => {
|
|
const stored = makeOAuthCredential({
|
|
provider: "openai",
|
|
expires: Date.now() + 60_000,
|
|
});
|
|
const incoming = makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "new-access",
|
|
refresh: "new-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
});
|
|
|
|
expect(shouldReplaceStoredOAuthCredential(stored, incoming)).toBe(true);
|
|
expect(shouldReplaceStoredOAuthCredential(undefined, incoming)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("external cli bootstrap policy", () => {
|
|
it("treats only non-expired and non-near-expiry access tokens as usable local oauth", () => {
|
|
expect(
|
|
hasUsableOAuthCredential(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "live-access",
|
|
expires: Date.now() + 10 * 60_000,
|
|
}),
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
hasUsableOAuthCredential(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "expired-access",
|
|
expires: Date.now() - 60_000,
|
|
}),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
hasUsableOAuthCredential(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "near-expiry-access",
|
|
expires: Date.now() + 60_000,
|
|
}),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
hasUsableOAuthCredential(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "",
|
|
expires: Date.now() + 60_000,
|
|
}),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("only bootstraps from external cli when the stored oauth is not usable", () => {
|
|
const imported = makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "fresh-cli-access",
|
|
refresh: "fresh-cli-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
accountId: "acct-123",
|
|
});
|
|
|
|
expect(
|
|
shouldBootstrapFromExternalCliCredential({
|
|
existing: makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "healthy-local-access",
|
|
refresh: "healthy-local-refresh",
|
|
expires: Date.now() + 10 * 60_000,
|
|
}),
|
|
imported,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
shouldBootstrapFromExternalCliCredential({
|
|
existing: makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "expired-local-access",
|
|
refresh: "expired-local-refresh",
|
|
expires: Date.now() - 60_000,
|
|
accountId: "acct-123",
|
|
}),
|
|
imported,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldBootstrapFromExternalCliCredential({
|
|
existing: makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "near-expiry-local-access",
|
|
refresh: "near-expiry-local-refresh",
|
|
expires: Date.now() + 60_000,
|
|
}),
|
|
imported,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("does not use codex as a runtime bootstrap source anymore", () => {
|
|
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "codex-access-token",
|
|
refresh: "codex-refresh-token",
|
|
}),
|
|
);
|
|
|
|
const credential = readExternalCliBootstrapCredential({
|
|
store: makeStore(),
|
|
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
|
credential: makeOAuthCredential({ provider: "openai" }),
|
|
});
|
|
|
|
expect(credential).toBeNull();
|
|
});
|
|
|
|
it("bootstraps the default codex profile from Codex CLI credentials when in scope", () => {
|
|
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "codex-cli-access",
|
|
refresh: "codex-cli-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
accountId: "acct-codex",
|
|
}),
|
|
);
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
|
|
providerIds: ["openai"],
|
|
});
|
|
|
|
expectCredentialFields(
|
|
expectSingleProfileCredential(profiles, OPENAI_CODEX_DEFAULT_PROFILE_ID),
|
|
{
|
|
provider: "openai",
|
|
access: "codex-cli-access",
|
|
refresh: "codex-cli-refresh",
|
|
accountId: "acct-codex",
|
|
},
|
|
);
|
|
});
|
|
|
|
it("does not add Codex CLI as a sibling to a named managed OpenAI profile", () => {
|
|
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "codex-cli-access",
|
|
refresh: "codex-cli-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
accountId: "acct-codex",
|
|
}),
|
|
);
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(
|
|
makeStore(
|
|
"openai:user@example.com",
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "managed-access",
|
|
refresh: "managed-refresh",
|
|
expires: Date.now() - 5_000,
|
|
accountId: "acct-codex",
|
|
}),
|
|
),
|
|
{
|
|
providerIds: ["openai"],
|
|
},
|
|
);
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
expect(mocks.readCodexCliCredentialsCached).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not fill an empty default slot beside a named managed OpenAI profile", () => {
|
|
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "codex-cli-access",
|
|
refresh: "codex-cli-refresh",
|
|
accountId: "acct-codex",
|
|
}),
|
|
);
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(
|
|
{
|
|
version: 1,
|
|
profiles: {
|
|
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
|
|
type: "oauth",
|
|
provider: "openai",
|
|
access: "",
|
|
refresh: "",
|
|
expires: 0,
|
|
},
|
|
"openai:user@example.com": makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "managed-access",
|
|
refresh: "managed-refresh",
|
|
expires: Date.now() - 5_000,
|
|
accountId: "acct-codex",
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
providerIds: ["openai"],
|
|
profileIds: [OPENAI_CODEX_DEFAULT_PROFILE_ID],
|
|
},
|
|
);
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
expect(mocks.readCodexCliCredentialsCached).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps any existing default codex oauth over Codex CLI bootstrap credentials", () => {
|
|
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "codex-cli-fresh-access",
|
|
refresh: "codex-cli-fresh-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
accountId: "acct-codex",
|
|
}),
|
|
);
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(
|
|
makeStore(
|
|
OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "local-expired-access",
|
|
refresh: "local-canonical-refresh",
|
|
expires: Date.now() - 5_000,
|
|
accountId: "acct-codex",
|
|
}),
|
|
),
|
|
);
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
});
|
|
|
|
it("returns null when the profile id/provider do not map to the same external source", () => {
|
|
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({ provider: "openai" }),
|
|
);
|
|
|
|
const credential = readExternalCliBootstrapCredential({
|
|
store: makeStore(),
|
|
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
|
credential: makeOAuthCredential({ provider: "anthropic" }),
|
|
});
|
|
|
|
expect(credential).toBeNull();
|
|
});
|
|
|
|
it("normalizes Claude CLI oauth credentials into the managed Claude profile", () => {
|
|
mocks.readClaudeCliCredentialsCached.mockReturnValue({
|
|
type: "oauth",
|
|
provider: "anthropic",
|
|
access: "claude-cli-access",
|
|
refresh: "claude-cli-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
});
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
|
|
providerIds: ["claude-cli"],
|
|
});
|
|
|
|
const profile = expectSingleProfile(profiles, CLAUDE_CLI_PROFILE_ID);
|
|
expect(profile?.persistence).toBe("persisted");
|
|
expectCredentialFields(profile?.credential as Record<string, unknown>, {
|
|
type: "oauth",
|
|
provider: "claude-cli",
|
|
access: "claude-cli-access",
|
|
refresh: "claude-cli-refresh",
|
|
});
|
|
});
|
|
|
|
it("skips external cli readers outside the scoped provider set", () => {
|
|
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
|
|
providerIds: ["opencode-go"],
|
|
});
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
expect(mocks.readCodexCliCredentialsCached).not.toHaveBeenCalled();
|
|
expect(mocks.readClaudeCliCredentialsCached).not.toHaveBeenCalled();
|
|
expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not scan missing external CLI profiles without an explicit scope", () => {
|
|
mocks.readClaudeCliCredentialsCached.mockReturnValue({
|
|
type: "oauth",
|
|
provider: "anthropic",
|
|
access: "claude-cli-access",
|
|
refresh: "claude-cli-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
});
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(makeStore());
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
expect(mocks.readClaudeCliCredentialsCached).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("refreshes a stored external CLI profile without an explicit scope", () => {
|
|
mocks.readClaudeCliCredentialsCached.mockReturnValue({
|
|
type: "oauth",
|
|
provider: "anthropic",
|
|
access: "claude-cli-fresh-access",
|
|
refresh: "claude-cli-fresh-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
});
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(
|
|
makeStore(CLAUDE_CLI_PROFILE_ID, {
|
|
type: "oauth",
|
|
provider: "claude-cli",
|
|
access: "claude-cli-stale-access",
|
|
refresh: "claude-cli-stale-refresh",
|
|
expires: Date.now() - 5_000,
|
|
}),
|
|
);
|
|
|
|
const profile = expectSingleProfile(profiles, CLAUDE_CLI_PROFILE_ID);
|
|
expect(profile?.persistence).toBe("persisted");
|
|
expectCredentialFields(profile?.credential as Record<string, unknown>, {
|
|
provider: "claude-cli",
|
|
access: "claude-cli-fresh-access",
|
|
});
|
|
});
|
|
|
|
it("does not reread external CLI credentials for a usable stored managed profile", () => {
|
|
mocks.readClaudeCliCredentialsCached.mockReturnValue({
|
|
type: "oauth",
|
|
provider: "anthropic",
|
|
access: "external-access",
|
|
refresh: "external-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
});
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(
|
|
makeStore(CLAUDE_CLI_PROFILE_ID, {
|
|
type: "oauth",
|
|
provider: "claude-cli",
|
|
access: "usable-local-access",
|
|
refresh: "usable-local-refresh",
|
|
expires: Date.now() + 10 * 60_000,
|
|
// Identity-complete steady state; profiles missing the email get one
|
|
// bounded backfill read (external-cli-sync.email-backfill.test.ts).
|
|
email: "stored@example.com",
|
|
}),
|
|
);
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
expect(mocks.readClaudeCliCredentialsCached).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("passes non-prompting keychain policy to scoped Claude CLI credential reads", () => {
|
|
mocks.readClaudeCliCredentialsCached.mockReturnValue({
|
|
type: "oauth",
|
|
provider: "anthropic",
|
|
access: "claude-cli-access",
|
|
refresh: "claude-cli-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
});
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
|
|
providerIds: ["claude-cli"],
|
|
allowKeychainPrompt: false,
|
|
});
|
|
|
|
const profile = expectSingleProfile(profiles, CLAUDE_CLI_PROFILE_ID);
|
|
expect(profile?.persistence).toBe("persisted");
|
|
expectCredentialFields(profile?.credential as Record<string, unknown>, {
|
|
type: "oauth",
|
|
provider: "claude-cli",
|
|
});
|
|
expectReaderPolicyCall(mocks.readClaudeCliCredentialsCached);
|
|
expect(mocks.readCodexCliCredentialsCached).not.toHaveBeenCalled();
|
|
expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("passes non-prompting keychain policy to scoped Codex CLI credential reads", () => {
|
|
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "openai",
|
|
access: "codex-cli-access",
|
|
refresh: "codex-cli-refresh",
|
|
}),
|
|
);
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
|
|
providerIds: ["codex-app-server"],
|
|
allowKeychainPrompt: false,
|
|
});
|
|
|
|
expectCredentialFields(
|
|
expectSingleProfileCredential(profiles, OPENAI_CODEX_DEFAULT_PROFILE_ID),
|
|
{
|
|
type: "oauth",
|
|
provider: "openai",
|
|
},
|
|
);
|
|
expectReaderPolicyCall(mocks.readCodexCliCredentialsCached);
|
|
expect(mocks.readClaudeCliCredentialsCached).not.toHaveBeenCalled();
|
|
expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("ignores Claude CLI token credentials", () => {
|
|
mocks.readClaudeCliCredentialsCached.mockReturnValue({
|
|
type: "token",
|
|
provider: "anthropic",
|
|
token: "claude-cli-token",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
});
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
|
|
providerIds: ["claude-cli"],
|
|
});
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
});
|
|
|
|
it("resolves fresher minimax external oauth profiles as runtime overlays", () => {
|
|
mocks.readMiniMaxCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "minimax-portal",
|
|
access: "minimax-fresh-access",
|
|
refresh: "minimax-fresh-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
email: "minimax@example.com",
|
|
}),
|
|
);
|
|
|
|
const profiles = resolveExternalCliAuthProfiles({
|
|
version: 1,
|
|
profiles: {
|
|
[MINIMAX_CLI_PROFILE_ID]: makeOAuthCredential({
|
|
provider: "minimax-portal",
|
|
access: "minimax-stale-access",
|
|
refresh: "minimax-stale-refresh",
|
|
expires: Date.now() - 5_000,
|
|
email: "minimax@example.com",
|
|
}),
|
|
},
|
|
});
|
|
|
|
const profilesById = new Map(
|
|
profiles.map((profile) => [profile.profileId, profile.credential]),
|
|
);
|
|
expectCredentialFields(profilesById.get(MINIMAX_CLI_PROFILE_ID) as Record<string, unknown>, {
|
|
access: "minimax-fresh-access",
|
|
refresh: "minimax-fresh-refresh",
|
|
});
|
|
});
|
|
|
|
it("does not emit runtime overlays when the stored minimax credential is newer", () => {
|
|
mocks.readMiniMaxCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "minimax-portal",
|
|
access: "stale-external-access",
|
|
refresh: "stale-external-refresh",
|
|
expires: Date.now() - 5_000,
|
|
}),
|
|
);
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(
|
|
makeStore(
|
|
MINIMAX_CLI_PROFILE_ID,
|
|
makeOAuthCredential({
|
|
provider: "minimax-portal",
|
|
access: "fresh-store-access",
|
|
refresh: "fresh-store-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
}),
|
|
),
|
|
);
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
});
|
|
|
|
it("does not overlay fresh minimax oauth over a still-usable local credential", () => {
|
|
mocks.readMiniMaxCliCredentialsCached.mockReturnValue(
|
|
makeOAuthCredential({
|
|
provider: "minimax-portal",
|
|
access: "fresh-cli-access",
|
|
refresh: "fresh-cli-refresh",
|
|
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
|
}),
|
|
);
|
|
|
|
const profiles = resolveExternalCliAuthProfiles(
|
|
makeStore(
|
|
MINIMAX_CLI_PROFILE_ID,
|
|
makeOAuthCredential({
|
|
provider: "minimax-portal",
|
|
access: "healthy-local-access",
|
|
refresh: "healthy-local-refresh",
|
|
expires: Date.now() + 10 * 60_000,
|
|
}),
|
|
),
|
|
);
|
|
|
|
expect(profiles).toStrictEqual([]);
|
|
});
|
|
});
|