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:
@@ -12,7 +12,13 @@ import { vi } from 'vitest';
|
||||
export function createMockAgent() {
|
||||
return {
|
||||
setRegistry: vi.fn(),
|
||||
chat: vi.fn().mockResolvedValue('mock response'),
|
||||
chat: vi.fn().mockResolvedValue({
|
||||
text: 'mock response',
|
||||
messages: [
|
||||
{ role: 'user', content: 'test' },
|
||||
{ role: 'assistant', content: 'mock response' },
|
||||
],
|
||||
}),
|
||||
getToolCount: vi.fn().mockReturnValue({ core: 5, discovered: 0, total: 5 }),
|
||||
getContextUsageFormatted: vi.fn().mockReturnValue('10k / 200k'),
|
||||
getContextUsage: vi.fn().mockReturnValue({
|
||||
@@ -25,6 +31,10 @@ export function createMockAgent() {
|
||||
getCompressionManager: vi.fn().mockReturnValue({
|
||||
shouldCompress: vi.fn().mockReturnValue(false),
|
||||
}),
|
||||
getHistory: vi.fn().mockReturnValue([
|
||||
{ role: 'user', content: 'test' },
|
||||
{ role: 'assistant', content: 'mock response' },
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* SessionManager Mock 工厂
|
||||
*
|
||||
* 提供会话管理器的 mock 实现
|
||||
*
|
||||
* 注意:消息存储已移至 Core 层,SessionManager 只负责会话元数据管理
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
@@ -13,6 +15,7 @@ export interface MockSession {
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
workdir?: string;
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
export interface MockMessage {
|
||||
@@ -27,8 +30,6 @@ export interface MockMessage {
|
||||
*/
|
||||
export function createMockSessionManager() {
|
||||
const sessions = new Map<string, MockSession>();
|
||||
const messages = new Map<string, MockMessage[]>();
|
||||
let messageIdCounter = 1;
|
||||
|
||||
const manager = {
|
||||
// 初始化
|
||||
@@ -48,44 +49,40 @@ export function createMockSessionManager() {
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
workdir: data?.workdir ?? process.cwd(),
|
||||
messageCount: 0,
|
||||
};
|
||||
sessions.set(id, session);
|
||||
messages.set(id, []);
|
||||
return session;
|
||||
}),
|
||||
|
||||
delete: vi.fn(async (id: string) => {
|
||||
const existed = sessions.has(id);
|
||||
sessions.delete(id);
|
||||
messages.delete(id);
|
||||
return existed;
|
||||
}),
|
||||
|
||||
list: vi.fn(() => Array.from(sessions.values())),
|
||||
|
||||
// 消息管理
|
||||
addMessage: vi.fn(async (sessionId: string, msg: { role: 'user' | 'assistant'; content: string }) => {
|
||||
const msgList = messages.get(sessionId) || [];
|
||||
const newMsg: MockMessage = {
|
||||
id: `msg-${messageIdCounter++}`,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
msgList.push(newMsg);
|
||||
messages.set(sessionId, msgList);
|
||||
return newMsg;
|
||||
}),
|
||||
|
||||
getMessages: vi.fn((id: string) => messages.get(id) || []),
|
||||
|
||||
// 状态更新
|
||||
updateStatus: vi.fn((id: string, status: 'idle' | 'busy') => {
|
||||
const session = sessions.get(id);
|
||||
if (session) {
|
||||
session.status = status;
|
||||
session.updatedAt = Date.now();
|
||||
return session;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
|
||||
// 更新消息计数
|
||||
updateMessageCount: vi.fn((id: string, count: number) => {
|
||||
const session = sessions.get(id);
|
||||
if (session) {
|
||||
session.messageCount = count;
|
||||
session.updatedAt = Date.now();
|
||||
return session;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
|
||||
updateSessionName: vi.fn(async (id: string, name: string) => {
|
||||
@@ -98,26 +95,20 @@ export function createMockSessionManager() {
|
||||
return null;
|
||||
}),
|
||||
|
||||
// Storage 访问
|
||||
getStorage: vi.fn(() => null),
|
||||
getProjectId: vi.fn((_sessionId: string) => 'default-project'),
|
||||
|
||||
// 测试辅助方法
|
||||
_addSession: (session: MockSession) => {
|
||||
sessions.set(session.id, session);
|
||||
messages.set(session.id, []);
|
||||
},
|
||||
|
||||
_addMessage: (sessionId: string, message: MockMessage) => {
|
||||
const msgList = messages.get(sessionId) || [];
|
||||
msgList.push(message);
|
||||
messages.set(sessionId, msgList);
|
||||
},
|
||||
|
||||
_clear: () => {
|
||||
sessions.clear();
|
||||
messages.clear();
|
||||
messageIdCounter = 1;
|
||||
},
|
||||
|
||||
_getSessions: () => sessions,
|
||||
_getMessages: () => messages,
|
||||
};
|
||||
|
||||
return manager;
|
||||
@@ -134,6 +125,7 @@ export function createTestSession(overrides: Partial<MockSession> = {}): MockSes
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
workdir: '/test/workdir',
|
||||
messageCount: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -487,7 +487,6 @@ describe('Agent Adapter', () => {
|
||||
const mockSessionManager = {
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
updateStatus: vi.fn(),
|
||||
addMessage: vi.fn().mockResolvedValue({ id: 'msg-1', role: 'assistant', content: 'placeholder' }),
|
||||
};
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
@@ -523,8 +522,7 @@ describe('Agent Adapter', () => {
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
get: vi.fn().mockReturnValue({ id: 'session-1', name: 'Test' }),
|
||||
updateStatus: vi.fn(),
|
||||
addMessage: vi.fn().mockResolvedValue({ id: 'msg-1', role: 'assistant', content: 'response' }),
|
||||
getMessages: vi.fn().mockReturnValue([]),
|
||||
updateMessageCount: vi.fn(),
|
||||
};
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*
|
||||
* 注意:SessionManager 使用动态 import 加载 @ai-assistant/core。
|
||||
* 测试中会创建新实例并记录初始会话数量,以确保测试独立性。
|
||||
*
|
||||
* 消息存储已移至 Core 层,Server 的 SessionManager 只负责会话元数据管理。
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
@@ -172,16 +174,6 @@ describe('SessionManager', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('删除会话时同时删除消息', async () => {
|
||||
// 注意:这个测试会自己删除 session,不需要追踪
|
||||
const session = await manager.create({ name: 'With Messages' });
|
||||
await manager.addMessage(session.id, { role: 'user', content: 'Hello' });
|
||||
|
||||
await manager.delete(session.id);
|
||||
|
||||
expect(manager.getMessages(session.id)).toEqual([]);
|
||||
});
|
||||
|
||||
it('删除后 count 减少', async () => {
|
||||
// 注意:这个测试会自己删除 session,不需要追踪
|
||||
const session = await manager.create({ name: 'Test' });
|
||||
@@ -232,112 +224,28 @@ describe('SessionManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMessages - 获取消息', () => {
|
||||
it('返回会话消息', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
await manager.addMessage(session.id, { role: 'user', content: 'Hello' });
|
||||
await manager.addMessage(session.id, { role: 'assistant', content: 'Hi!' });
|
||||
|
||||
const messages = manager.getMessages(session.id);
|
||||
|
||||
expect(messages.length).toBe(2);
|
||||
expect(messages[0].content).toBe('Hello');
|
||||
expect(messages[1].content).toBe('Hi!');
|
||||
});
|
||||
|
||||
it('新会话消息为空', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const messages = manager.getMessages(session.id);
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('不存在的会话返回空数组', () => {
|
||||
const messages = manager.getMessages('non-existent-id-12345');
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMessage - 添加消息', () => {
|
||||
it('添加用户消息', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const message = await manager.addMessage(session.id, {
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
});
|
||||
|
||||
expect(message).toBeDefined();
|
||||
expect(message?.role).toBe('user');
|
||||
expect(message?.content).toBe('Hello');
|
||||
expect(message?.id).toBeDefined();
|
||||
expect(message?.sessionId).toBe(session.id);
|
||||
});
|
||||
|
||||
it('添加助手消息', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const message = await manager.addMessage(session.id, {
|
||||
role: 'assistant',
|
||||
content: 'Hello!',
|
||||
});
|
||||
|
||||
expect(message?.role).toBe('assistant');
|
||||
});
|
||||
|
||||
it('添加系统消息', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const message = await manager.addMessage(session.id, {
|
||||
role: 'system',
|
||||
content: 'System prompt',
|
||||
});
|
||||
|
||||
expect(message?.role).toBe('system');
|
||||
});
|
||||
|
||||
it('消息有唯一 ID', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const msg1 = await manager.addMessage(session.id, { role: 'user', content: 'Hello' });
|
||||
const msg2 = await manager.addMessage(session.id, { role: 'assistant', content: 'Hi!' });
|
||||
|
||||
expect(msg1?.id).not.toBe(msg2?.id);
|
||||
});
|
||||
|
||||
it('消息有正确的时间戳', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const before = new Date().toISOString();
|
||||
const message = await manager.addMessage(session.id, { role: 'user', content: 'Hello' });
|
||||
const after = new Date().toISOString();
|
||||
|
||||
expect(message?.createdAt).toBeDefined();
|
||||
expect(message?.createdAt! >= before).toBe(true);
|
||||
expect(message?.createdAt! <= after).toBe(true);
|
||||
});
|
||||
|
||||
it('更新会话的 messageCount', async () => {
|
||||
describe('updateMessageCount - 更新消息计数', () => {
|
||||
it('更新消息计数', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
expect(session.messageCount).toBe(0);
|
||||
|
||||
await manager.addMessage(session.id, { role: 'user', content: 'Hello' });
|
||||
|
||||
const updated = manager.get(session.id);
|
||||
expect(updated?.messageCount).toBe(1);
|
||||
const updated = manager.updateMessageCount(session.id, 5);
|
||||
expect(updated?.messageCount).toBe(5);
|
||||
});
|
||||
|
||||
it('更新会话的 updatedAt', async () => {
|
||||
it('更新时更新 updatedAt', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const originalUpdatedAt = session.updatedAt;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
await manager.addMessage(session.id, { role: 'user', content: 'Hello' });
|
||||
manager.updateMessageCount(session.id, 3);
|
||||
|
||||
const updated = manager.get(session.id);
|
||||
expect(updated?.updatedAt).not.toBe(originalUpdatedAt);
|
||||
});
|
||||
|
||||
it('不存在的会话返回 undefined', async () => {
|
||||
const result = await manager.addMessage('non-existent-id-12345', {
|
||||
role: 'user',
|
||||
content: 'Hello',
|
||||
});
|
||||
|
||||
it('不存在的会话返回 undefined', () => {
|
||||
const result = manager.updateMessageCount('non-existent-id-12345', 10);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -410,48 +318,30 @@ describe('SessionManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStorage 和 getProjectId - Storage 访问', () => {
|
||||
it('getStorage 返回 Storage 实例或 null', () => {
|
||||
const storage = manager.getStorage();
|
||||
// 可能为 null(如果 Core 未加载)或者是 SessionStorage 实例
|
||||
expect(storage === null || typeof storage === 'object').toBe(true);
|
||||
});
|
||||
|
||||
it('getProjectId 返回字符串', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const projectId = manager.getProjectId(session.id);
|
||||
expect(typeof projectId).toBe('string');
|
||||
});
|
||||
|
||||
it('不存在的会话返回默认 projectId', () => {
|
||||
const projectId = manager.getProjectId('non-existent-id');
|
||||
expect(typeof projectId).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('处理特殊字符的会话名称', async () => {
|
||||
const session = await createTrackedSession({ name: '测试会话 <>&"\'`' });
|
||||
expect(session.name).toBe('测试会话 <>&"\'`');
|
||||
});
|
||||
|
||||
it('处理长消息内容', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const longContent = 'x'.repeat(10000);
|
||||
const message = await manager.addMessage(session.id, {
|
||||
role: 'user',
|
||||
content: longContent,
|
||||
});
|
||||
|
||||
expect(message?.content).toBe(longContent);
|
||||
});
|
||||
|
||||
it('处理空字符串消息', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const message = await manager.addMessage(session.id, {
|
||||
role: 'user',
|
||||
content: '',
|
||||
});
|
||||
|
||||
expect(message?.content).toBe('');
|
||||
});
|
||||
|
||||
it('多个会话独立的消息', async () => {
|
||||
const session1 = await createTrackedSession({ name: 'Session 1' });
|
||||
const session2 = await createTrackedSession({ name: 'Session 2' });
|
||||
|
||||
await manager.addMessage(session1.id, { role: 'user', content: 'Message for session 1' });
|
||||
await manager.addMessage(session2.id, { role: 'user', content: 'Message for session 2' });
|
||||
|
||||
const messages1 = manager.getMessages(session1.id);
|
||||
const messages2 = manager.getMessages(session2.id);
|
||||
|
||||
expect(messages1.length).toBe(1);
|
||||
expect(messages2.length).toBe(1);
|
||||
expect(messages1[0].content).toBe('Message for session 1');
|
||||
expect(messages2[0].content).toBe('Message for session 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* WebSocket Handler 测试
|
||||
*
|
||||
* 测试 WebSocket 连接处理、消息路由、广播功能等
|
||||
*
|
||||
* 注意:消息存储已移至 Core 层,Server 的 WebSocket 只负责消息广播和路由
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
@@ -9,7 +11,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
// Create mock functions
|
||||
const mockExists = vi.fn();
|
||||
const mockUpdateStatus = vi.fn();
|
||||
const mockAddMessage = vi.fn();
|
||||
const mockGet = vi.fn();
|
||||
|
||||
// Mock dependencies before imports
|
||||
@@ -17,7 +18,6 @@ vi.mock('../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn(() => ({
|
||||
exists: mockExists,
|
||||
updateStatus: mockUpdateStatus,
|
||||
addMessage: mockAddMessage,
|
||||
get: mockGet,
|
||||
})),
|
||||
}));
|
||||
@@ -60,7 +60,6 @@ describe('WebSocket Handler', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockExists.mockReturnValue(false);
|
||||
mockAddMessage.mockReturnValue({ id: 'msg-1', role: 'user', content: '', timestamp: Date.now() });
|
||||
});
|
||||
|
||||
describe('handleWebSocket - 连接处理', () => {
|
||||
@@ -110,6 +109,8 @@ describe('WebSocket Handler', () => {
|
||||
|
||||
it('处理 message 类型消息', async () => {
|
||||
const ws = createMockWSContext();
|
||||
handleWebSocket(ws as any, 'session-1');
|
||||
|
||||
const message = JSON.stringify({
|
||||
type: 'message',
|
||||
payload: { content: 'Hello AI' },
|
||||
@@ -117,10 +118,11 @@ describe('WebSocket Handler', () => {
|
||||
|
||||
await handleWebSocketMessage(ws as any, 'session-1', message);
|
||||
|
||||
expect(mockAddMessage).toHaveBeenCalledWith('session-1', {
|
||||
role: 'user',
|
||||
content: 'Hello AI',
|
||||
});
|
||||
// 应该广播 message_received 确认
|
||||
expect(ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"type":"message_received"')
|
||||
);
|
||||
// 应该调用 processMessage
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'Hello AI');
|
||||
});
|
||||
|
||||
@@ -175,6 +177,7 @@ describe('WebSocket Handler', () => {
|
||||
|
||||
it('处理 ArrayBuffer 数据', async () => {
|
||||
const ws = createMockWSContext();
|
||||
handleWebSocket(ws as any, 'session-1');
|
||||
|
||||
const message = { type: 'message', payload: { content: 'ArrayBuffer test' } };
|
||||
const encoder = new TextEncoder();
|
||||
@@ -182,14 +185,13 @@ describe('WebSocket Handler', () => {
|
||||
|
||||
await handleWebSocketMessage(ws as any, 'session-1', buffer);
|
||||
|
||||
expect(mockAddMessage).toHaveBeenCalledWith('session-1', {
|
||||
role: 'user',
|
||||
content: 'ArrayBuffer test',
|
||||
});
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'ArrayBuffer test');
|
||||
});
|
||||
|
||||
it('空 content 处理正确', async () => {
|
||||
const ws = createMockWSContext();
|
||||
handleWebSocket(ws as any, 'session-1');
|
||||
|
||||
const message = JSON.stringify({
|
||||
type: 'message',
|
||||
payload: {},
|
||||
@@ -197,24 +199,19 @@ describe('WebSocket Handler', () => {
|
||||
|
||||
await handleWebSocketMessage(ws as any, 'session-1', message);
|
||||
|
||||
expect(mockAddMessage).toHaveBeenCalledWith('session-1', {
|
||||
role: 'user',
|
||||
content: '',
|
||||
});
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', '');
|
||||
});
|
||||
|
||||
it('处理 Blob 数据', async () => {
|
||||
const ws = createMockWSContext();
|
||||
handleWebSocket(ws as any, 'session-1');
|
||||
|
||||
const message = { type: 'message', payload: { content: 'Blob test' } };
|
||||
const blob = new Blob([JSON.stringify(message)]);
|
||||
|
||||
await handleWebSocketMessage(ws as any, 'session-1', blob);
|
||||
|
||||
expect(mockAddMessage).toHaveBeenCalledWith('session-1', {
|
||||
role: 'user',
|
||||
content: 'Blob test',
|
||||
});
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'Blob test');
|
||||
});
|
||||
|
||||
it('处理非标准数据类型(转为字符串)', async () => {
|
||||
@@ -229,21 +226,6 @@ describe('WebSocket Handler', () => {
|
||||
expect(cancelProcessing).toHaveBeenCalledWith('session-1');
|
||||
});
|
||||
|
||||
it('addMessage 返回 null 时不调用 processMessage', async () => {
|
||||
const ws = createMockWSContext();
|
||||
mockAddMessage.mockReturnValue(null);
|
||||
|
||||
const message = JSON.stringify({
|
||||
type: 'message',
|
||||
payload: { content: 'Test' },
|
||||
});
|
||||
|
||||
await handleWebSocketMessage(ws as any, 'session-1', message);
|
||||
|
||||
expect(mockAddMessage).toHaveBeenCalled();
|
||||
expect(processMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('处理 tool_response 类型消息(TODO 场景)', async () => {
|
||||
const ws = createMockWSContext();
|
||||
const message = JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user