[codex] fix auth startup and profile model defaults (#966)
* docs release 0.6.0 changelog * fix auth startup and profile model defaults
This commit is contained in:
@@ -232,6 +232,54 @@ describe('App Store', () => {
|
||||
expect(store.displayModelName('unknown', 'deepseek')).toBe('unknown')
|
||||
})
|
||||
|
||||
it('selects the browser active profile default instead of the aggregate response default', async () => {
|
||||
window.localStorage.setItem('hermes_active_profile_name', 'tester')
|
||||
mockSystemApi.fetchAvailableModels.mockResolvedValue({
|
||||
default: 'glm-5-turbo',
|
||||
default_provider: 'custom:glm-coding-plan',
|
||||
groups: [{
|
||||
provider: 'custom:glm-coding-plan',
|
||||
label: 'glm-coding-plan',
|
||||
base_url: 'https://api.z.ai/api/anthropic',
|
||||
models: ['glm-5-turbo', 'glm-5.1'],
|
||||
api_key: '',
|
||||
}],
|
||||
allProviders: [],
|
||||
profiles: [
|
||||
{
|
||||
profile: 'default',
|
||||
default: 'glm-5-turbo',
|
||||
default_provider: 'custom:glm-coding-plan',
|
||||
groups: [{
|
||||
provider: 'custom:glm-coding-plan',
|
||||
label: 'glm-coding-plan',
|
||||
base_url: 'https://api.z.ai/api/anthropic',
|
||||
models: ['glm-5-turbo', 'glm-5.1'],
|
||||
api_key: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
profile: 'tester',
|
||||
default: 'claude-opus-4-6',
|
||||
default_provider: 'custom:subrouter',
|
||||
groups: [{
|
||||
provider: 'custom:subrouter',
|
||||
label: 'subrouter',
|
||||
base_url: 'https://subrouter.ai/v1',
|
||||
models: ['claude-opus-4-6', 'gpt-5.5'],
|
||||
api_key: '',
|
||||
}],
|
||||
},
|
||||
],
|
||||
})
|
||||
const store = useAppStore()
|
||||
|
||||
await store.loadModels()
|
||||
|
||||
expect(store.selectedModel).toBe('claude-opus-4-6')
|
||||
expect(store.selectedProvider).toBe('custom:subrouter')
|
||||
})
|
||||
|
||||
it('does not refetch available models within the cache window after an empty response', async () => {
|
||||
mockSystemApi.fetchAvailableModels.mockResolvedValue({
|
||||
default: '',
|
||||
|
||||
@@ -37,7 +37,15 @@ async function loadSkillsController() {
|
||||
}
|
||||
|
||||
function makeCtx(body: unknown): any {
|
||||
return { request: { body }, status: 200, body: undefined, query: {}, params: {} }
|
||||
return {
|
||||
request: { body },
|
||||
status: 200,
|
||||
body: undefined,
|
||||
query: {},
|
||||
params: {},
|
||||
state: {},
|
||||
get: vi.fn(() => ''),
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -75,6 +83,24 @@ describe('config mutating controllers', () => {
|
||||
expect(config.terminal.backend).toBe('local')
|
||||
})
|
||||
|
||||
it('setConfigModel uses the requested profile header when auth has not populated state.profile', async () => {
|
||||
const researchDir = join(hermesHome, 'profiles', 'research')
|
||||
await mkdir(researchDir, { recursive: true })
|
||||
await writeFile(join(hermesHome, 'config.yaml'), 'model:\n default: root-model\n', 'utf-8')
|
||||
await writeFile(join(researchDir, 'config.yaml'), 'model:\n default: old-research\n', 'utf-8')
|
||||
const { setConfigModel } = await loadModelsController()
|
||||
const ctx = makeCtx({ default: 'research-model', provider: 'deepseek' })
|
||||
ctx.get = vi.fn((name: string) => name.toLowerCase() === 'x-hermes-profile' ? 'research' : '')
|
||||
|
||||
await setConfigModel(ctx)
|
||||
|
||||
expect(ctx.body).toEqual({ success: true })
|
||||
const rootConfig = YAML.load(await readFile(join(hermesHome, 'config.yaml'), 'utf-8')) as any
|
||||
const researchConfig = YAML.load(await readFile(join(researchDir, 'config.yaml'), 'utf-8')) as any
|
||||
expect(rootConfig.model.default).toBe('root-model')
|
||||
expect(researchConfig.model).toEqual({ default: 'research-model', provider: 'deepseek' })
|
||||
})
|
||||
|
||||
it('skill toggle preserves unrelated config while adding and removing disabled skills', async () => {
|
||||
await writeFile(join(hermesHome, 'config.yaml'), [
|
||||
'model:',
|
||||
|
||||
@@ -185,6 +185,25 @@ describe('models controller — model visibility', () => {
|
||||
]))
|
||||
})
|
||||
|
||||
it('uses the requested profile for aggregate response defaults', async () => {
|
||||
mockListProfileNamesFromDisk.mockReturnValue(['default', 'tester'])
|
||||
mockReadConfigYamlForProfile.mockImplementation(async (profile: string) => ({
|
||||
model: {
|
||||
default: profile === 'tester' ? 'deepseek-reasoner' : 'deepseek-chat',
|
||||
provider: 'deepseek',
|
||||
},
|
||||
}))
|
||||
|
||||
const ctx = makeCtx()
|
||||
ctx.state = { user: { id: 1, username: 'admin', role: 'super_admin' } }
|
||||
ctx.get = vi.fn((name: string) => name.toLowerCase() === 'x-hermes-profile' ? 'tester' : '')
|
||||
await ctrl.getAvailable(ctx)
|
||||
|
||||
expect(ctx.body.default).toBe('deepseek-reasoner')
|
||||
expect(ctx.body.default_provider).toBe('deepseek')
|
||||
expect(ctx.body.profiles.map((profile: any) => profile.profile)).toEqual(['default', 'tester'])
|
||||
})
|
||||
|
||||
it('uses explicit query profile for single-profile model fetches', async () => {
|
||||
mockListProfileNamesFromDisk.mockReturnValue(['default', 'research'])
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@ describe('user auth tables and middleware', () => {
|
||||
const user = users.bootstrapDefaultSuperAdmin('admin', '123456')!
|
||||
const token = auth.signUserJwt(user, 'test-secret')
|
||||
const ctx = {
|
||||
path: '/api/hermes/download',
|
||||
headers: {},
|
||||
query: { token },
|
||||
state: {},
|
||||
@@ -183,6 +184,46 @@ describe('user auth tables and middleware', () => {
|
||||
expect(next).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('lets SPA and static asset paths pass through without a JWT', async () => {
|
||||
const { auth } = await initUsers()
|
||||
const ctx = {
|
||||
path: '/',
|
||||
headers: {},
|
||||
query: {},
|
||||
state: {},
|
||||
request: { body: {} },
|
||||
status: 200,
|
||||
body: null,
|
||||
} as any
|
||||
const next = vi.fn(async () => {})
|
||||
|
||||
await auth.requireUserJwt(ctx, next)
|
||||
|
||||
expect(next).toHaveBeenCalledOnce()
|
||||
expect(ctx.status).toBe(200)
|
||||
expect(ctx.body).toBeNull()
|
||||
})
|
||||
|
||||
it('still requires a JWT for protected API paths', async () => {
|
||||
const { auth } = await initUsers()
|
||||
const ctx = {
|
||||
path: '/api/hermes/sessions',
|
||||
headers: {},
|
||||
query: {},
|
||||
state: {},
|
||||
request: { body: {} },
|
||||
status: 200,
|
||||
body: null,
|
||||
} as any
|
||||
const next = vi.fn(async () => {})
|
||||
|
||||
await auth.requireUserJwt(ctx, next)
|
||||
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
expect(ctx.status).toBe(401)
|
||||
expect(ctx.body).toEqual({ error: 'Unauthorized' })
|
||||
})
|
||||
|
||||
it('bootstraps the default super admin through password login and returns a user JWT', async () => {
|
||||
await initUsers()
|
||||
const ctrl = await import('../../packages/server/src/controllers/auth')
|
||||
@@ -200,7 +241,7 @@ describe('user auth tables and middleware', () => {
|
||||
expect(ctx.body.token).toMatch(/^[^.]+\.[^.]+\.[^.]+$/)
|
||||
})
|
||||
|
||||
it('marks the default account credentials as requiring a change', async () => {
|
||||
it('marks only admin with password 123456 as requiring a credential change', async () => {
|
||||
const { users } = await initUsers()
|
||||
const admin = users.bootstrapDefaultSuperAdmin('admin', '123456')!
|
||||
const ctrl = await import('../../packages/server/src/controllers/auth')
|
||||
@@ -213,15 +254,24 @@ describe('user auth tables and middleware', () => {
|
||||
await ctrl.currentUser(defaultCtx)
|
||||
expect(defaultCtx.body.user.requiresCredentialChange).toBe(true)
|
||||
|
||||
users.updateUsername(admin.id, 'owner')
|
||||
users.updateUserPassword(admin.id, 'stronger-password')
|
||||
const changedCtx = {
|
||||
const passwordChangedCtx = {
|
||||
state: { user: { id: admin.id, username: 'admin', role: 'super_admin' } },
|
||||
status: 200,
|
||||
body: null,
|
||||
} as any
|
||||
await ctrl.currentUser(passwordChangedCtx)
|
||||
expect(passwordChangedCtx.body.user.requiresCredentialChange).toBe(false)
|
||||
|
||||
users.updateUserPassword(admin.id, '123456')
|
||||
users.updateUsername(admin.id, 'owner')
|
||||
const usernameChangedCtx = {
|
||||
state: { user: { id: admin.id, username: 'owner', role: 'super_admin' } },
|
||||
status: 200,
|
||||
body: null,
|
||||
} as any
|
||||
await ctrl.currentUser(changedCtx)
|
||||
expect(changedCtx.body.user.requiresCredentialChange).toBe(false)
|
||||
await ctrl.currentUser(usernameChangedCtx)
|
||||
expect(usernameChangedCtx.body.user.requiresCredentialChange).toBe(false)
|
||||
})
|
||||
|
||||
it('lets super admins create regular admins with profile bindings', async () => {
|
||||
|
||||
Reference in New Issue
Block a user