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 端点
This commit is contained in:
@@ -2,20 +2,25 @@
|
||||
* 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, mockGetMessages, mockAddMessage } = vi.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(),
|
||||
mockGetMessages: vi.fn(),
|
||||
mockAddMessage: vi.fn(),
|
||||
mockGetStorage: vi.fn(),
|
||||
mockGetProjectId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/session/manager.js', () => ({
|
||||
@@ -25,8 +30,8 @@ vi.mock('../../../src/session/manager.js', () => ({
|
||||
get: mockGet,
|
||||
exists: mockExists,
|
||||
delete: mockDelete,
|
||||
getMessages: mockGetMessages,
|
||||
addMessage: mockAddMessage,
|
||||
getStorage: mockGetStorage,
|
||||
getProjectId: mockGetProjectId,
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -39,6 +44,10 @@ app.route('/sessions', sessionsRouter);
|
||||
describe('Sessions Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetStorage.mockReturnValue({
|
||||
loadSession: mockLoadSession,
|
||||
});
|
||||
mockGetProjectId.mockReturnValue('default-project');
|
||||
});
|
||||
|
||||
describe('GET /sessions - 列出会话', () => {
|
||||
@@ -160,13 +169,18 @@ describe('Sessions Route', () => {
|
||||
});
|
||||
|
||||
describe('GET /sessions/:id/messages - 获取消息', () => {
|
||||
it('返回会话消息', async () => {
|
||||
it('返回会话消息(从 Core Storage 读取)', async () => {
|
||||
const messages = [
|
||||
{ id: 'msg-1', role: 'user', content: 'Hello', timestamp: 1000 },
|
||||
{ id: 'msg-2', role: 'assistant', content: 'Hi!', timestamp: 2000 },
|
||||
{ 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);
|
||||
mockGetMessages.mockReturnValue(messages);
|
||||
mockLoadSession.mockResolvedValue({
|
||||
id: 'session-1',
|
||||
messages,
|
||||
});
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages');
|
||||
const json = await res.json();
|
||||
@@ -174,6 +188,8 @@ describe('Sessions Route', () => {
|
||||
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 () => {
|
||||
@@ -189,7 +205,32 @@ describe('Sessions Route', () => {
|
||||
|
||||
it('空消息返回空数组', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockGetMessages.mockReturnValue([]);
|
||||
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();
|
||||
@@ -198,83 +239,4 @@ describe('Sessions Route', () => {
|
||||
expect(json.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /sessions/:id/messages - 发送消息', () => {
|
||||
it('添加用户消息', async () => {
|
||||
const message = { id: 'msg-1', role: 'user', content: 'Hello', timestamp: Date.now() };
|
||||
mockExists.mockReturnValue(true);
|
||||
mockAddMessage.mockResolvedValue(message);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'user', content: 'Hello' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data).toEqual(message);
|
||||
});
|
||||
|
||||
it('添加助手消息', async () => {
|
||||
const message = { id: 'msg-2', role: 'assistant', content: 'Hi!', timestamp: Date.now() };
|
||||
mockExists.mockReturnValue(true);
|
||||
mockAddMessage.mockResolvedValue(message);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'assistant', content: 'Hi!' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(json.success).toBe(true);
|
||||
});
|
||||
|
||||
it('不存在的会话返回 404', async () => {
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
const res = await app.request('/sessions/non-existent/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'user', content: 'Hello' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(json.success).toBe(false);
|
||||
});
|
||||
|
||||
it('无效输入返回 400', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'invalid', content: 'Hello' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(json.success).toBe(false);
|
||||
});
|
||||
|
||||
it('添加消息失败返回 500', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockAddMessage.mockResolvedValue(null);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'user', content: 'Hello' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Failed to add message');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user