[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:
ekko
2026-05-24 14:00:31 +08:00
committed by GitHub
parent 634a622934
commit f61a1d9454
24 changed files with 310 additions and 30 deletions
@@ -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'])
+55 -5
View File
@@ -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 () => {