mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-11 11:10:21 +00:00
b67607729d
* include auth providers in model catalog cache * fetch auth provider model catalogs
144 lines
6.2 KiB
TypeScript
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' })
|
|
})
|
|
})
|