Files
hermes-web-ui/tests/server/anthropic-gemini-auth-controller.test.ts
ekko b67607729d [codex] include auth providers in model catalog cache (#1583)
* include auth providers in model catalog cache

* fetch auth provider model catalogs
2026-06-16 10:36:12 +08:00

144 lines
6.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
import { dirname, join } from 'path'
import { tmpdir } from 'os'
import YAML from 'js-yaml'
import {
applyAnthropicOAuthDefaultModel,
saveAnthropicOAuthTokensForProfile,
status as anthropicStatus,
} from '../../packages/server/src/controllers/hermes/anthropic-auth'
import {
applyGeminiOAuthDefaultModel,
resolveGeminiOAuthClientCredentials,
saveGeminiOAuthTokensForProfile,
status as geminiStatus,
} from '../../packages/server/src/controllers/hermes/gemini-auth'
let hermesHome = ''
function writeFile(relativePath: string, content: string) {
const target = join(hermesHome, relativePath)
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, content)
}
function readYaml(relativePath: string) {
return YAML.load(readFileSync(join(hermesHome, relativePath), 'utf-8')) as any
}
function readJson(relativePath: string) {
return JSON.parse(readFileSync(join(hermesHome, relativePath), 'utf-8'))
}
function makeCtx(profile: string): any {
return {
state: { profile: { name: profile } },
query: {},
request: { body: {} },
get: () => '',
status: 200,
body: undefined as unknown,
}
}
describe('Anthropic and Gemini OAuth controllers', () => {
const originalGeminiClientId = process.env.HERMES_GEMINI_CLIENT_ID
const originalGeminiClientSecret = process.env.HERMES_GEMINI_CLIENT_SECRET
beforeEach(() => {
hermesHome = mkdtempSync(join(tmpdir(), 'hwui-oauth-providers-'))
process.env.HERMES_HOME = hermesHome
delete process.env.HERMES_GEMINI_CLIENT_ID
delete process.env.HERMES_GEMINI_CLIENT_SECRET
})
afterEach(() => {
delete process.env.HERMES_HOME
if (originalGeminiClientId === undefined) delete process.env.HERMES_GEMINI_CLIENT_ID
else process.env.HERMES_GEMINI_CLIENT_ID = originalGeminiClientId
if (originalGeminiClientSecret === undefined) delete process.env.HERMES_GEMINI_CLIENT_SECRET
else process.env.HERMES_GEMINI_CLIENT_SECRET = originalGeminiClientSecret
if (hermesHome) rmSync(hermesHome, { recursive: true, force: true })
hermesHome = ''
})
it('uses provider-compatible default models when applying OAuth defaults', () => {
expect(applyAnthropicOAuthDefaultModel({
model: { provider: 'deepseek', default: 'deepseek-chat', base_url: 'x', api_key: 'y' },
}).model).toEqual({ provider: 'claude-oauth', default: 'claude-sonnet-4-6' })
expect(applyGeminiOAuthDefaultModel({
model: { provider: 'anthropic', default: 'claude-sonnet-4-6', base_url: 'x', api_key: 'y' },
}).model).toEqual({ provider: 'google-gemini-cli', default: 'gemini-3.1-pro-preview' })
})
it('uses the public Gemini CLI OAuth client when env credentials are not configured', () => {
const defaults = resolveGeminiOAuthClientCredentials()
expect(defaults.clientId).toMatch(/^681255809395-.+\.apps\.googleusercontent\.com$/)
expect(defaults.clientSecret).toMatch(/^GOCSPX-.+$/)
process.env.HERMES_GEMINI_CLIENT_ID = 'custom-client-id'
process.env.HERMES_GEMINI_CLIENT_SECRET = 'custom-client-secret'
expect(resolveGeminiOAuthClientCredentials()).toEqual({
clientId: 'custom-client-id',
clientSecret: 'custom-client-secret',
})
})
it('persists Anthropic OAuth credentials in the request-scoped profile only', async () => {
mkdirSync(join(hermesHome, 'profiles', 'research'), { recursive: true })
writeFile('config.yaml', 'model:\n provider: deepseek\n default: deepseek-chat\n')
writeFile('profiles/research/config.yaml', 'model:\n provider: openrouter\n default: openrouter-model\n')
await saveAnthropicOAuthTokensForProfile('research', {
access_token: 'anthropic-access-token',
refresh_token: 'anthropic-refresh-token',
expires_in: 3600,
})
expect(existsSync(join(hermesHome, 'auth.json'))).toBe(false)
const auth = readJson('profiles/research/auth.json')
expect(auth.providers['claude-oauth'].tokens.access_token).toBe('anthropic-access-token')
expect(auth.credential_pool['claude-oauth'][0].refresh_token).toBe('anthropic-refresh-token')
expect(auth.providers.anthropic.tokens.access_token).toBe('anthropic-access-token')
expect(auth.credential_pool.anthropic[0].refresh_token).toBe('anthropic-refresh-token')
expect(readJson('profiles/research/.anthropic_oauth.json').accessToken).toBe('anthropic-access-token')
expect(readYaml('config.yaml').model).toEqual({ provider: 'deepseek', default: 'deepseek-chat' })
expect(readYaml('profiles/research/config.yaml').model).toEqual({ provider: 'claude-oauth', default: 'claude-sonnet-4-6' })
const ctx = makeCtx('research')
await anthropicStatus(ctx)
expect(ctx.body).toMatchObject({ authenticated: true })
})
it('persists Gemini OAuth credentials in the request-scoped profile only', async () => {
mkdirSync(join(hermesHome, 'profiles', 'research'), { recursive: true })
writeFile('config.yaml', 'model:\n provider: deepseek\n default: deepseek-chat\n')
writeFile('profiles/research/config.yaml', 'model:\n provider: openrouter\n default: openrouter-model\n')
await saveGeminiOAuthTokensForProfile('research', {
access_token: 'gemini-access-token',
refresh_token: 'gemini-refresh-token',
expires_in: 3600,
}, 'user@example.com')
expect(existsSync(join(hermesHome, 'auth.json'))).toBe(false)
const auth = readJson('profiles/research/auth.json')
expect(auth.providers['google-gemini-cli'].access_token).toBe('gemini-access-token')
expect(auth.credential_pool['google-gemini-cli'][0].refresh_token).toBe('gemini-refresh-token')
expect(readJson('profiles/research/auth/google_oauth.json')).toMatchObject({
access: 'gemini-access-token',
refresh: 'gemini-refresh-token',
email: 'user@example.com',
})
expect(readYaml('config.yaml').model).toEqual({ provider: 'deepseek', default: 'deepseek-chat' })
expect(readYaml('profiles/research/config.yaml').model).toEqual({ provider: 'google-gemini-cli', default: 'gemini-3.1-pro-preview' })
const ctx = makeCtx('research')
await geminiStatus(ctx)
expect(ctx.body).toMatchObject({ authenticated: true, email: 'user@example.com' })
})
})