/** * Sessions Route 测试 * * 测试会话管理 REST API 端点 * * 注意:消息存储已移至 Core 层,GET /sessions/:id/messages 从 Core Storage 读取 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Hono } from 'hono'; // Mock storage interface const mockLoadSession = vi.fn(); // Use vi.hoisted to create mocks before vi.mock is hoisted const { mockList, mockCreate, mockGet, mockExists, mockDelete, mockGetStorage, mockGetProjectId } = vi.hoisted(() => ({ mockList: vi.fn(), mockCreate: vi.fn(), mockGet: vi.fn(), mockExists: vi.fn(), mockDelete: vi.fn(), mockGetStorage: vi.fn(), mockGetProjectId: vi.fn(), })); vi.mock('../../../src/session/manager.js', () => ({ getSessionManager: vi.fn(() => ({ list: mockList, create: mockCreate, get: mockGet, exists: mockExists, delete: mockDelete, getStorage: mockGetStorage, getProjectId: mockGetProjectId, })), })); import { sessionsRouter } from '../../../src/routes/sessions.js'; // Create test app const app = new Hono(); app.route('/sessions', sessionsRouter); describe('Sessions Route', () => { beforeEach(() => { vi.clearAllMocks(); mockGetStorage.mockReturnValue({ loadSession: mockLoadSession, }); mockGetProjectId.mockReturnValue('default-project'); }); describe('GET /sessions - 列出会话', () => { it('返回会话列表', async () => { const sessions = [ { id: 'session-1', name: 'Test 1', status: 'idle', createdAt: 1000, updatedAt: 1000 }, { id: 'session-2', name: 'Test 2', status: 'active', createdAt: 2000, updatedAt: 2000 }, ]; mockList.mockReturnValue(sessions); const res = await app.request('/sessions'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toEqual(sessions); }); it('空列表返回空数组', async () => { mockList.mockReturnValue([]); const res = await app.request('/sessions'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toEqual([]); }); }); describe('POST /sessions - 创建会话', () => { it('创建新会话', async () => { const newSession = { id: 'new-session', name: 'My Session', status: 'idle', createdAt: Date.now(), updatedAt: Date.now(), }; mockCreate.mockResolvedValue(newSession); const res = await app.request('/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'My Session' }), }); const json = await res.json(); expect(res.status).toBe(201); expect(json.success).toBe(true); expect(json.data).toEqual(newSession); }); it('无效输入返回 400', async () => { const res = await app.request('/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: 'invalid json', }); const json = await res.json(); expect(res.status).toBe(400); expect(json.success).toBe(false); }); }); describe('GET /sessions/:id - 获取单个会话', () => { it('返回存在的会话', async () => { const session = { id: 'session-1', name: 'Test', status: 'idle' }; mockGet.mockReturnValue(session); const res = await app.request('/sessions/session-1'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toEqual(session); }); it('不存在的会话返回 404', async () => { mockGet.mockReturnValue(null); const res = await app.request('/sessions/non-existent'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); expect(json.error).toBe('Session not found'); }); }); describe('DELETE /sessions/:id - 删除会话', () => { it('删除存在的会话', async () => { mockExists.mockReturnValue(true); mockDelete.mockResolvedValue(true); const res = await app.request('/sessions/session-1', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(mockDelete).toHaveBeenCalledWith('session-1'); }); it('不存在的会话返回 404', async () => { mockExists.mockReturnValue(false); const res = await app.request('/sessions/non-existent', { method: 'DELETE', }); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); expect(json.error).toBe('Session not found'); }); }); describe('GET /sessions/:id/messages - 获取消息', () => { it('返回会话消息(从 Core Storage 读取)', async () => { const messages = [ { role: 'user', content: 'Hello' }, { role: 'assistant', content: [{ type: 'tool-call', toolName: 'read_file' }] }, { role: 'user', content: [{ type: 'tool-result', toolCallId: 'call-1' }] }, { role: 'assistant', content: 'Hi!' }, ]; mockExists.mockReturnValue(true); mockLoadSession.mockResolvedValue({ id: 'session-1', messages, }); const res = await app.request('/sessions/session-1/messages'); const json = await res.json(); expect(res.status).toBe(200); expect(json.success).toBe(true); expect(json.data).toEqual(messages); expect(mockGetStorage).toHaveBeenCalled(); expect(mockGetProjectId).toHaveBeenCalledWith('session-1'); }); it('不存在的会话返回 404', async () => { mockExists.mockReturnValue(false); const res = await app.request('/sessions/non-existent/messages'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); expect(json.error).toBe('Session not found'); }); it('空消息返回空数组', async () => { mockExists.mockReturnValue(true); mockLoadSession.mockResolvedValue({ id: 'session-1', messages: [], }); const res = await app.request('/sessions/session-1/messages'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual([]); }); it('Storage 不可用时返回空数组', async () => { mockExists.mockReturnValue(true); mockGetStorage.mockReturnValue(null); const res = await app.request('/sessions/session-1/messages'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual([]); }); it('Session 数据不存在时返回空数组', async () => { mockExists.mockReturnValue(true); mockLoadSession.mockResolvedValue(null); const res = await app.request('/sessions/session-1/messages'); const json = await res.json(); expect(res.status).toBe(200); expect(json.data).toEqual([]); }); }); });