Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/client/src/api/hermes/kanban.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export interface KanbanListOptions extends KanbanBoardOptions {
status?: string
assignee?: string
tenant?: string
includeArchived?: boolean
}

function normalizedBoard(board?: string): string {
Expand Down Expand Up @@ -194,6 +195,7 @@ export async function listTasks(opts?: KanbanListOptions): Promise<KanbanTask[]>
if (opts?.status) params.set('status', opts.status)
if (opts?.assignee) params.set('assignee', opts.assignee)
if (opts?.tenant) params.set('tenant', opts.tenant)
if (opts?.includeArchived) params.set('includeArchived', 'true')
const res = await request<{ tasks: KanbanTask[] }>(appendQuery('/api/hermes/kanban', params))
return res.tasks
}
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/stores/hermes/kanban.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export const useKanbanStore = defineStore('kanban', () => {
board,
status: filterStatus.value || undefined,
assignee: filterAssignee.value || undefined,
includeArchived: true,
})
if (isCurrentRequest(seq, generation, board, tasksRequestSeq)) tasks.value = nextTasks
} catch (err) {
Expand Down
7 changes: 5 additions & 2 deletions packages/server/src/controllers/hermes/kanban.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,14 @@ export async function capabilities(ctx: Context) {
}

export async function list(ctx: Context) {
const { status, assignee, tenant } = ctx.query as Record<string, string | undefined>
const status = firstQueryValue(ctx.query.status as string | string[] | undefined)
const assignee = firstQueryValue(ctx.query.assignee as string | string[] | undefined)
const tenant = firstQueryValue(ctx.query.tenant as string | string[] | undefined)
const includeArchived = firstQueryValue(ctx.query.includeArchived as string | string[] | undefined) === 'true'
const board = requestBoard(ctx)
if (!board) return
try {
const tasks = await kanbanCli.listTasks({ board, status, assignee, tenant })
const tasks = await kanbanCli.listTasks({ board, status, assignee, tenant, includeArchived })
ctx.body = { tasks }
} catch (err: any) {
ctx.status = 500
Expand Down
10 changes: 9 additions & 1 deletion packages/server/src/services/hermes/hermes-kanban.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,10 @@ export async function listTasks(opts?: {
status?: string
assignee?: string
tenant?: string
includeArchived?: boolean
}): Promise<KanbanTask[]> {
const args = [...boardArgs(opts?.board), 'list', '--json']
if (opts?.includeArchived) args.push('--archived')
if (opts?.status) args.push('--status', opts.status)
if (opts?.assignee) args.push('--assignee', opts.assignee)
if (opts?.tenant) args.push('--tenant', opts.tenant)
Expand Down Expand Up @@ -346,7 +348,13 @@ export async function getStats(opts?: KanbanBoardOptions): Promise<KanbanStats>
timeout: 30000,
...execOpts,
})
return JSON.parse(stdout)
const stats = JSON.parse(stdout) as KanbanStats
const archivedTasks = await listTasks({ board: opts?.board, status: 'archived', includeArchived: true })
const existingArchived = stats.by_status?.archived || 0
const archivedCount = archivedTasks.length
stats.by_status = { ...(stats.by_status || {}), archived: archivedCount }
stats.total = (stats.total || 0) + Math.max(0, archivedCount - existingArchived)
return stats
} catch (err: any) {
logger.error(err, 'Hermes CLI: kanban stats failed')
throw new Error(`Failed to get kanban stats: ${err.message}`)
Expand Down
6 changes: 3 additions & 3 deletions tests/client/kanban-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ describe('Kanban API', () => {
vi.clearAllMocks()
})

it('serializes board and list filters into query params', async () => {
it('serializes board, list filters, and archived inclusion into query params', async () => {
mockRequest.mockResolvedValue({ tasks: [{ id: 'task-1' }] })

const result = await listTasks({ board: 'default', status: 'blocked', assignee: 'alice', tenant: 'ops' })
const result = await listTasks({ board: 'default', status: 'blocked', assignee: 'alice', tenant: 'ops', includeArchived: true })

expect(mockRequest).toHaveBeenCalledWith('/api/hermes/kanban?board=default&status=blocked&assignee=alice&tenant=ops')
expect(mockRequest).toHaveBeenCalledWith('/api/hermes/kanban?board=default&status=blocked&assignee=alice&tenant=ops&includeArchived=true')
expect(result).toEqual([{ id: 'task-1' }])
})

Expand Down
4 changes: 2 additions & 2 deletions tests/client/kanban-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('Kanban store', () => {
expect(store.loading).toBe(true)
await promise

expect(mockKanbanApi.listTasks).toHaveBeenCalledWith({ board: 'project-a', status: 'blocked', assignee: 'alice' })
expect(mockKanbanApi.listTasks).toHaveBeenCalledWith({ board: 'project-a', status: 'blocked', assignee: 'alice', includeArchived: true })
expect(store.tasks).toEqual([{ id: 'task-1', status: 'todo' }])
expect(store.loading).toBe(false)
})
Expand Down Expand Up @@ -128,7 +128,7 @@ describe('Kanban store', () => {
store.setSelectedBoard('project-a')
await store.refreshAll()

expect(mockKanbanApi.listTasks).toHaveBeenCalledWith({ board: 'project-a', status: undefined, assignee: undefined })
expect(mockKanbanApi.listTasks).toHaveBeenCalledWith({ board: 'project-a', status: undefined, assignee: undefined, includeArchived: true })
expect(mockKanbanApi.getStats).toHaveBeenCalledWith({ board: 'project-a' })
expect(mockKanbanApi.getAssignees).toHaveBeenCalledWith({ board: 'project-a' })
expect(mockKanbanApi.listBoards).toHaveBeenCalledWith({ includeArchived: false })
Expand Down
10 changes: 7 additions & 3 deletions tests/server/hermes-kanban-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,30 @@ describe('hermes kanban service', () => {
.mockResolvedValueOnce({ stdout: JSON.stringify([{ id: 'task-1' }]) })
.mockResolvedValueOnce({ stdout: JSON.stringify({ id: 'task-2' }) })
.mockResolvedValueOnce({ stdout: JSON.stringify({ total: 1, by_status: {}, by_assignee: {} }) })
.mockResolvedValueOnce({ stdout: JSON.stringify([{ id: 'archived-1', status: 'archived' }, { id: 'archived-2', status: 'archived' }]) })

await expect(service.listTasks({ board: 'project-a', status: 'todo', assignee: 'alice', tenant: 'ops' })).resolves.toEqual([{ id: 'task-1' }])
await expect(service.listTasks({ board: 'project-a', status: 'todo', assignee: 'alice', tenant: 'ops', includeArchived: true })).resolves.toEqual([{ id: 'task-1' }])
await expect(service.createTask('Ship', { board: 'project-a', body: 'write', assignee: 'alice', priority: 3, tenant: 'ops' })).resolves.toEqual({ id: 'task-2' })
await expect(service.getStats({ board: 'project-a' })).resolves.toEqual({ total: 1, by_status: {}, by_assignee: {} })
await expect(service.getStats({ board: 'project-a' })).resolves.toEqual({ total: 3, by_status: { archived: 2 }, by_assignee: {} })

expect(mockExecFileAsync.mock.calls[0][1]).toEqual(['kanban', '--board', 'project-a', 'list', '--json', '--status', 'todo', '--assignee', 'alice', '--tenant', 'ops'])
expect(mockExecFileAsync.mock.calls[0][1]).toEqual(['kanban', '--board', 'project-a', 'list', '--json', '--archived', '--status', 'todo', '--assignee', 'alice', '--tenant', 'ops'])
expect(mockExecFileAsync.mock.calls[1][1]).toEqual(['kanban', '--board', 'project-a', 'create', 'Ship', '--json', '--body', 'write', '--assignee', 'alice', '--priority', '3', '--tenant', 'ops'])
expect(mockExecFileAsync.mock.calls[2][1]).toEqual(['kanban', '--board', 'project-a', 'stats', '--json'])
expect(mockExecFileAsync.mock.calls[3][1]).toEqual(['kanban', '--board', 'project-a', 'list', '--json', '--archived', '--status', 'archived'])
})

it('normalizes omitted board to default instead of falling through to CLI current', async () => {
mockExecFileAsync
.mockResolvedValueOnce({ stdout: JSON.stringify([]) })
.mockResolvedValueOnce({ stdout: JSON.stringify({ total: 0, by_status: {}, by_assignee: {} }) })
.mockResolvedValueOnce({ stdout: JSON.stringify([]) })

await service.listTasks()
await service.getStats()

expect(mockExecFileAsync.mock.calls[0][1]).toEqual(['kanban', '--board', 'default', 'list', '--json'])
expect(mockExecFileAsync.mock.calls[1][1]).toEqual(['kanban', '--board', 'default', 'stats', '--json'])
expect(mockExecFileAsync.mock.calls[2][1]).toEqual(['kanban', '--board', 'default', 'list', '--json', '--archived', '--status', 'archived'])
})

it('builds action CLI calls and maps not-found show to null', async () => {
Expand Down
6 changes: 3 additions & 3 deletions tests/server/kanban-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ describe('kanban controller', () => {
expect(mockListBoards).toHaveBeenCalledWith({ includeArchived: true })
expect(boardsCtx.body).toEqual({ boards: [{ slug: 'default' }] })

const c = ctx({ query: { board: 'project-a', status: 'todo', assignee: 'alice', tenant: 'ops' } })
const c = ctx({ query: { board: 'project-a', status: 'todo', assignee: 'alice', tenant: 'ops', includeArchived: 'true' } })
await ctrl.list(c)
expect(mockListTasks).toHaveBeenCalledWith({ board: 'project-a', status: 'todo', assignee: 'alice', tenant: 'ops' })
expect(mockListTasks).toHaveBeenCalledWith({ board: 'project-a', status: 'todo', assignee: 'alice', tenant: 'ops', includeArchived: true })
expect(c.body).toEqual({ tasks: [{ id: 'task-1' }] })

mockCreateBoard.mockResolvedValue({ slug: 'project-b' })
Expand All @@ -106,7 +106,7 @@ describe('kanban controller', () => {

const defaultCtx = ctx({ query: { status: 'ready' } })
await ctrl.list(defaultCtx)
expect(mockListTasks).toHaveBeenLastCalledWith({ board: 'default', status: 'ready', assignee: undefined, tenant: undefined })
expect(mockListTasks).toHaveBeenLastCalledWith({ board: 'default', status: 'ready', assignee: undefined, tenant: undefined, includeArchived: false })
})

it('enriches completed task details using the latest run profile', async () => {
Expand Down
Loading