import { beforeEach, describe, expect, it, vi } from 'vitest' const listConversationsMock = vi.fn(async (ctx: any) => { ctx.body = { sessions: [{ id: 'conversation-1' }] } }) const getConversationMessagesMock = vi.fn(async (ctx: any) => { ctx.body = { session_id: ctx.params.id, messages: [] } }) const getConversationMessagesPaginatedMock = vi.fn(async (ctx: any) => { ctx.body = { session_id: ctx.params.id, messages: [], pagination: {} } }) const listMock = vi.fn(async (ctx: any) => { ctx.body = { sessions: [{ id: 's1' }] } }) const countMock = vi.fn(async (ctx: any) => { ctx.body = { count: 1 } }) const listHermesSessionsMock = vi.fn(async (ctx: any) => { ctx.body = { sessions: [{ id: 'hermes-1' }] } }) const getHermesSessionMock = vi.fn(async (ctx: any) => { ctx.body = { session: { id: ctx.params.id } } }) const importHermesSessionMock = vi.fn(async (ctx: any) => { ctx.body = { session_id: ctx.params.id } }) const searchMock = vi.fn(async (ctx: any) => { ctx.body = { results: [{ id: 'search-1' }] } }) const getMock = vi.fn(async (ctx: any) => { ctx.body = { session: { id: ctx.params.id } } }) const getContextMock = vi.fn(async (ctx: any) => { ctx.body = { session_id: ctx.params.id, messages: [] } }) const removeMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const renameMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const archiveMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const unarchiveMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const setWorkspaceMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const setModelMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const listWorkspaceFoldersMock = vi.fn(async (ctx: any) => { ctx.body = { folders: [] } }) const createWorkspaceFolderMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const renameWorkspaceFolderMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const deleteWorkspaceFolderMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const usageBatchMock = vi.fn(async (ctx: any) => { ctx.body = {} }) const usageSingleMock = vi.fn(async (ctx: any) => { ctx.body = { input_tokens: 0, output_tokens: 0 } }) const usageStatsMock = vi.fn(async (ctx: any) => { ctx.body = { total_input_tokens: 0, total_output_tokens: 0 } }) const contextLengthMock = vi.fn(async (ctx: any) => { ctx.body = { context_length: 256000 } }) const batchRemoveMock = vi.fn(async (ctx: any) => { ctx.body = { deleted: 1, failed: 0, errors: [] } }) const exportSessionMock = vi.fn(async (ctx: any) => { ctx.body = JSON.stringify({ id: ctx.params.id }) }) const listWorkspaceRunChangesMock = vi.fn(async (ctx: any) => { ctx.body = { changes: [] } }) const getWorkspaceRunChangeFileMock = vi.fn(async (ctx: any) => { ctx.body = { file: null } }) const listWorkspaceFilesMock = vi.fn(async (ctx: any) => { ctx.body = { entries: [], path: '' } }) const readWorkspaceFileMock = vi.fn(async (ctx: any) => { ctx.body = { content: '' } }) const writeWorkspaceFileMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const mkdirWorkspaceFileMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const deleteWorkspaceFileMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const renameWorkspaceFileMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) const copyWorkspaceFileMock = vi.fn(async (ctx: any) => { ctx.body = { ok: true } }) vi.mock('../../packages/server/src/controllers/hermes/sessions', () => ({ listConversations: listConversationsMock, getConversationMessages: getConversationMessagesMock, getConversationMessagesPaginated: getConversationMessagesPaginatedMock, list: listMock, count: countMock, listHermesSessions: listHermesSessionsMock, getHermesSession: getHermesSessionMock, importHermesSession: importHermesSessionMock, search: searchMock, get: getMock, getContext: getContextMock, remove: removeMock, batchRemove: batchRemoveMock, rename: renameMock, archive: archiveMock, unarchive: unarchiveMock, setWorkspace: setWorkspaceMock, setModel: setModelMock, listWorkspaceFolders: listWorkspaceFoldersMock, createWorkspaceFolder: createWorkspaceFolderMock, renameWorkspaceFolder: renameWorkspaceFolderMock, deleteWorkspaceFolder: deleteWorkspaceFolderMock, usageBatch: usageBatchMock, usageSingle: usageSingleMock, usageStats: usageStatsMock, contextLength: contextLengthMock, exportSession: exportSessionMock, listWorkspaceRunChanges: listWorkspaceRunChangesMock, getWorkspaceRunChangeFile: getWorkspaceRunChangeFileMock, listWorkspaceFiles: listWorkspaceFilesMock, readWorkspaceFile: readWorkspaceFileMock, writeWorkspaceFile: writeWorkspaceFileMock, mkdirWorkspaceFile: mkdirWorkspaceFileMock, deleteWorkspaceFile: deleteWorkspaceFileMock, renameWorkspaceFile: renameWorkspaceFileMock, copyWorkspaceFile: copyWorkspaceFileMock, })) describe('session routes', () => { beforeEach(() => { vi.resetModules() listConversationsMock.mockClear() getConversationMessagesMock.mockClear() getConversationMessagesPaginatedMock.mockClear() listMock.mockClear() countMock.mockClear() listHermesSessionsMock.mockClear() getHermesSessionMock.mockClear() importHermesSessionMock.mockClear() searchMock.mockClear() getMock.mockClear() getContextMock.mockClear() removeMock.mockClear() renameMock.mockClear() archiveMock.mockClear() unarchiveMock.mockClear() setModelMock.mockClear() listWorkspaceFoldersMock.mockClear() createWorkspaceFolderMock.mockClear() renameWorkspaceFolderMock.mockClear() deleteWorkspaceFolderMock.mockClear() listWorkspaceRunChangesMock.mockClear() getWorkspaceRunChangeFileMock.mockClear() listWorkspaceFilesMock.mockClear() readWorkspaceFileMock.mockClear() writeWorkspaceFileMock.mockClear() mkdirWorkspaceFileMock.mockClear() deleteWorkspaceFileMock.mockClear() renameWorkspaceFileMock.mockClear() copyWorkspaceFileMock.mockClear() }) it('registers conversations, session list, and search routes', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const paths = sessionRoutes.stack.map((entry: any) => entry.path) expect(paths).toEqual(expect.arrayContaining([ '/api/hermes/sessions/conversations', '/api/hermes/sessions/conversations/:id/messages', '/api/hermes/sessions/conversations/:id/messages/paginated', '/api/hermes/sessions', '/api/hermes/sessions/count', '/api/hermes/sessions/hermes', '/api/hermes/sessions/hermes/:id', '/api/hermes/sessions/hermes/:id/import', '/api/hermes/search/sessions', '/api/hermes/sessions/search', '/api/hermes/sessions/usage', '/api/hermes/usage/stats', '/api/hermes/sessions/context-length', '/api/hermes/sessions/:id/context', '/api/hermes/sessions/:id/workspace-run-changes', '/api/hermes/sessions/:id/workspace-run-changes/:changeId/files/:fileId', '/api/hermes/sessions/:id/workspace-files/list', '/api/hermes/sessions/:id/workspace-file/read', '/api/hermes/sessions/:id/workspace-file/write', '/api/hermes/sessions/:id/workspace-file/mkdir', '/api/hermes/sessions/:id/workspace-file/delete', '/api/hermes/sessions/:id/workspace-file/rename', '/api/hermes/sessions/:id/workspace-file/copy', '/api/hermes/sessions/:id', '/api/hermes/sessions/:id/export', '/api/hermes/sessions/:id/usage', '/api/hermes/sessions/:id/rename', '/api/hermes/sessions/:id/archive', '/api/hermes/sessions/:id/unarchive', '/api/hermes/sessions/:id/model', '/api/hermes/workspace/folders', '/api/hermes/workspace/folders/rename', ])) }) it('delegates session count route before the session id route', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const countLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/count') const idLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id') expect(sessionRoutes.stack.indexOf(countLayer)).toBeLessThan(sessionRoutes.stack.indexOf(idLayer)) const ctx: any = { query: { source: 'cli' }, body: null, params: {} } await countLayer.stack[0](ctx) expect(countMock).toHaveBeenCalledWith(ctx) expect(getMock).not.toHaveBeenCalled() expect(ctx.body).toEqual({ count: 1 }) }) it('delegates session context route to the controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const layer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/context') const handler = layer.stack[0] const ctx: any = { query: {}, body: null, params: { id: 'session-1' } } await handler(ctx) expect(getContextMock).toHaveBeenCalledWith(ctx) expect(ctx.body).toEqual({ session_id: 'session-1', messages: [] }) }) it('delegates workspace folder routes to the controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const listLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/workspace/folders' && entry.methods.includes('HEAD')) const createLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/workspace/folders' && entry.methods.includes('POST')) const renameLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/workspace/folders/rename') const deleteLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/workspace/folders' && entry.methods.includes('DELETE')) const listCtx: any = { query: {}, request: { body: {} }, body: null, params: {} } await listLayer.stack[0](listCtx) expect(listWorkspaceFoldersMock).toHaveBeenCalledWith(listCtx) const createCtx: any = { query: {}, request: { body: { parentPath: '', name: 'new-folder' } }, body: null, params: {} } await createLayer.stack[0](createCtx) expect(createWorkspaceFolderMock).toHaveBeenCalledWith(createCtx) const renameCtx: any = { query: {}, request: { body: { path: 'old-folder', name: 'new-folder' } }, body: null, params: {} } await renameLayer.stack[0](renameCtx) expect(renameWorkspaceFolderMock).toHaveBeenCalledWith(renameCtx) const deleteCtx: any = { query: {}, request: { body: { path: 'new-folder' } }, body: null, params: {} } await deleteLayer.stack[0](deleteCtx) expect(deleteWorkspaceFolderMock).toHaveBeenCalledWith(deleteCtx) }) it('delegates session workspace file routes to the controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const listLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/workspace-files/list') const readLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/workspace-file/read') const writeLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/workspace-file/write') const mkdirLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/workspace-file/mkdir') const deleteLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/workspace-file/delete') const renameLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/workspace-file/rename') const copyLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/workspace-file/copy') const ctx: any = { query: {}, request: { body: {} }, body: null, params: { id: 'session-1' } } await listLayer.stack[0](ctx) await readLayer.stack[0](ctx) await writeLayer.stack[0](ctx) await mkdirLayer.stack[0](ctx) await deleteLayer.stack[0](ctx) await renameLayer.stack[0](ctx) await copyLayer.stack[0](ctx) expect(listWorkspaceFilesMock).toHaveBeenCalledWith(ctx) expect(readWorkspaceFileMock).toHaveBeenCalledWith(ctx) expect(writeWorkspaceFileMock).toHaveBeenCalledWith(ctx) expect(mkdirWorkspaceFileMock).toHaveBeenCalledWith(ctx) expect(deleteWorkspaceFileMock).toHaveBeenCalledWith(ctx) expect(renameWorkspaceFileMock).toHaveBeenCalledWith(ctx) expect(copyWorkspaceFileMock).toHaveBeenCalledWith(ctx) }) it('delegates session search to the controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const layer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/search/sessions') const handler = layer.stack[0] const ctx: any = { query: { q: 'docker', limit: '8' }, body: null, params: {} } await handler(ctx) expect(searchMock).toHaveBeenCalledWith(ctx) expect(ctx.body).toEqual({ results: [{ id: 'search-1' }] }) }) it('keeps the legacy search path wired to the same controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const layer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/search') const handler = layer.stack[0] const ctx: any = { query: { q: 'docker' }, body: null, params: {} } await handler(ctx) expect(searchMock).toHaveBeenCalledWith(ctx) expect(ctx.body).toEqual({ results: [{ id: 'search-1' }] }) }) it('delegates conversations list and detail routes', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const listLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/conversations') const detailLayer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/conversations/:id/messages') const listCtx: any = { query: {}, body: null, params: {} } await listLayer.stack[0](listCtx) expect(listConversationsMock).toHaveBeenCalledWith(listCtx) expect(listCtx.body).toEqual({ sessions: [{ id: 'conversation-1' }] }) const detailCtx: any = { params: { id: 'child-session' }, query: {}, body: null } await detailLayer.stack[0](detailCtx) expect(getConversationMessagesMock).toHaveBeenCalledWith(detailCtx) expect(detailCtx.body).toEqual({ session_id: 'child-session', messages: [] }) }) it('delegates Hermes session import to the controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const layer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/hermes/:id/import') const handler = layer.stack[0] const ctx: any = { params: { id: 'hermes-abc' }, query: {}, request: { body: { profile: 'default' } }, body: null } await handler(ctx) expect(importHermesSessionMock).toHaveBeenCalledWith(ctx) expect(ctx.body).toEqual({ session_id: 'hermes-abc' }) }) it('delegates session export to the controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const layer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/export') const handler = layer.stack[0] const ctx: any = { params: { id: 'session-abc' }, query: {}, body: null, set: vi.fn() } await handler(ctx) expect(exportSessionMock).toHaveBeenCalledWith(ctx) }) it('delegates session archive to the controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const layer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/archive') const handler = layer.stack[0] const ctx: any = { params: { id: 'session-abc' }, query: {}, body: null } await handler(ctx) expect(archiveMock).toHaveBeenCalledWith(ctx) expect(ctx.body).toEqual({ ok: true }) }) it('delegates session unarchive to the controller', async () => { const { sessionRoutes } = await import('../../packages/server/src/routes/hermes/sessions') const layer = sessionRoutes.stack.find((entry: any) => entry.path === '/api/hermes/sessions/:id/unarchive') const handler = layer.stack[0] const ctx: any = { params: { id: 'session-abc' }, query: {}, body: null } await handler(ctx) expect(unarchiveMock).toHaveBeenCalledWith(ctx) expect(ctx.body).toEqual({ ok: true }) }) })