Add user-scoped Hermes profile access

This commit is contained in:
ekko
2026-05-23 18:44:53 +08:00
committed by ekko
parent 56e7716302
commit 3f6a25d8f1
54 changed files with 2656 additions and 592 deletions
+48
View File
@@ -28,6 +28,7 @@ const mockSearchSessions = vi.hoisted(() => vi.fn())
const mockGetSessionDetail = vi.hoisted(() => vi.fn())
const mockGetExactSessionDetail = vi.hoisted(() => vi.fn())
const mockFindLatestExactSessionId = vi.hoisted(() => vi.fn())
const mockListUserProfiles = vi.hoisted(() => vi.fn())
vi.mock('fs/promises', () => ({
readFile: mockReadFile,
@@ -75,6 +76,10 @@ vi.mock('../../packages/server/src/db/hermes/sessions-db', () => ({
findLatestExactSessionIdWithProfile: mockFindLatestExactSessionId,
}))
vi.mock('../../packages/server/src/db/hermes/users-store', () => ({
listUserProfiles: mockListUserProfiles,
}))
import * as ctrl from '../../packages/server/src/controllers/hermes/kanban'
function ctx(overrides: Record<string, any> = {}) {
@@ -91,6 +96,7 @@ function ctx(overrides: Record<string, any> = {}) {
describe('kanban controller', () => {
beforeEach(() => {
vi.clearAllMocks()
mockListUserProfiles.mockReturnValue([{ profile_name: 'research' }])
})
it('lists boards and tasks with explicit/default board context', async () => {
@@ -129,6 +135,48 @@ describe('kanban controller', () => {
expect(mockListTasks).toHaveBeenLastCalledWith({ board: 'default', status: 'ready', assignee: undefined, tenant: undefined, includeArchived: false })
})
it('filters kanban tasks, stats, and assignees to the requested profile', async () => {
const tasks = [
{ id: 'task-1', assignee: 'research', status: 'todo' },
{ id: 'task-2', assignee: 'travel', status: 'done' },
{ id: 'task-3', assignee: null, status: 'blocked' },
]
mockListTasks.mockResolvedValue(tasks)
mockGetAssignees.mockResolvedValue([
{ name: 'research', on_disk: true, counts: { todo: 1 } },
{ name: 'travel', on_disk: true, counts: { done: 1 } },
{ name: 'default', on_disk: true, counts: { blocked: 1 } },
])
const state = { user: { id: 7, role: 'admin' }, profile: { name: 'research' } }
const listCtx = ctx({ state, query: { board: 'default', includeArchived: 'true' } })
await ctrl.list(listCtx)
expect(listCtx.body).toEqual({ tasks: [tasks[0]] })
const statsCtx = ctx({ state, query: { board: 'default' } })
await ctrl.stats(statsCtx)
expect(statsCtx.body).toEqual({ stats: { by_status: { todo: 1 }, by_assignee: { research: 1 }, total: 1 } })
const assigneesCtx = ctx({ state, query: { board: 'default' } })
await ctrl.assignees(assigneesCtx)
expect(assigneesCtx.body).toEqual({ assignees: [{ name: 'research', on_disk: true, counts: { todo: 1 } }] })
})
it('defaults created kanban tasks to the requested profile and rejects unauthorized assignees', async () => {
mockCreateTask.mockResolvedValue({ id: 'task-1', assignee: 'research' })
const state = { user: { id: 7, role: 'admin' }, profile: { name: 'research' } }
const createCtx = ctx({ state, query: { board: 'default' }, request: { body: { title: 'Ship it' } } })
await ctrl.create(createCtx)
expect(mockCreateTask).toHaveBeenCalledWith('Ship it', { board: 'default', body: undefined, assignee: 'research', priority: undefined, tenant: undefined })
expect(createCtx.body).toEqual({ task: { id: 'task-1', assignee: 'research' } })
const assignCtx = ctx({ state, query: { board: 'default' }, params: { id: 'task-1' }, request: { body: { profile: 'travel' } } })
await ctrl.assign(assignCtx)
expect(assignCtx.status).toBe(403)
expect(mockAssignTask).not.toHaveBeenCalled()
})
it('proxies comment/log/diagnostics with explicit board context', async () => {
const taskLog = { task_id: 'task-1', path: null, exists: true, size_bytes: 10, content: 'worker log', truncated: false }
mockAddComment.mockResolvedValue({ ok: true, output: 'commented' })