Files
ai-terminal-assistant/packages/server/tests/unit/routes/sessions.test.ts
T
kurihada 6342a46e59 refactor(storage): 统一消息存储到 Core 层
问题:Server 端只存储最终文本响应,工具调用的中间消息丢失。

解决方案:
- Agent.chat() 返回 ChatResult,包含完整消息链
- Server SessionManager 简化为只管理会话元数据
- 消息 API 改为从 Core Storage 读取
- 移除 Server 端的消息存储和 addMessage 方法

影响范围:
- core: Agent.chat() 返回类型变更
- server: SessionManager 接口变更,移除消息存储
- server: GET /sessions/:id/messages 从 Core 读取
- server: 移除 POST /sessions/:id/messages 端点
2025-12-15 10:04:22 +08:00

243 lines
7.0 KiB
TypeScript

/**
* 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([]);
});
});
});