Files
makelore/tests/unit/agent-session-sync.test.ts
inman 22add3f01f
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
完善客户端模块与工作区能力
2026-08-13 19:51:02 +08:00

164 lines
5.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { RawMessage } from '@/types/chat';
import {
buildAgentSessionData,
flushPendingAgentSessionSync,
queueAgentSessionSync,
resetAgentSessionSyncForTests,
sanitizeAgentSessionText,
} from '@/lib/agent-session-sync';
const getAuthStateMock = vi.hoisted(() => vi.fn());
const getProfileStateMock = vi.hoisted(() => vi.fn());
const pushSessionDataMock = vi.hoisted(() => vi.fn());
vi.mock('@/stores/auth', () => ({
useAuthStore: {
getState: () => getAuthStateMock(),
},
}));
vi.mock('@/stores/user-profile', () => ({
getProfileAccountKey: (value: string | null | undefined) => value?.trim() || null,
isUserProfileComplete: (profile: { displayName?: string } | undefined) => Boolean(profile?.displayName?.trim()),
useUserProfileStore: {
getState: () => getProfileStateMock(),
},
}));
describe('local Agent session sync', () => {
beforeEach(() => {
resetAgentSessionSyncForTests();
window.localStorage.clear();
getAuthStateMock.mockReset();
getProfileStateMock.mockReset();
pushSessionDataMock.mockReset();
getAuthStateMock.mockReturnValue({
getValidAccessToken: vi.fn().mockResolvedValue('access-token'),
user: { userId: 'user-a' },
});
getProfileStateMock.mockReturnValue({
profilesByUserId: { 'user-a': { displayName: '小泥' } },
pushSessionData: pushSessionDataMock,
});
pushSessionDataMock.mockResolvedValue(undefined);
});
it('removes code, paths, logs, tool parts, attachments and errors from session snapshots', () => {
const messages: RawMessage[] = [
{
id: 'user-1',
role: 'user',
content: '请解释这个问题。\n```ts\nconst secret = 1;\n```\n文件在 /Users/demo/project/src/main.ts',
_attachedFiles: [{
fileName: 'screen.png',
mimeType: 'image/png',
fileSize: 10,
preview: 'data:image/png;base64,AAAA',
}],
},
{
id: 'assistant-1',
role: 'assistant',
content: [
{ type: 'thinking', thinking: '内部推理不应上传。' },
{ type: 'text', text: '这是回答。' },
{ type: 'tool_use', name: 'read_file', input: { path: '/Users/demo/project/src/main.ts' } },
{ type: 'tool_result', content: 'DEBUG: tool output' },
{ type: 'text', text: '```js\nconsole.log(1)\n```' },
],
},
{
id: 'error-1',
role: 'assistant',
content: '错误响应不上传。',
isError: true,
},
{
id: 'system-1',
role: 'system',
content: '系统提示不上传。',
},
];
expect(buildAgentSessionData(
'prj_1',
'ses_1',
messages,
'2026-08-13T10:00:00.000Z',
)).toEqual({
project_id: 'prj_1',
session_id: 'ses_1',
updated_at: '2026-08-13T10:00:00.000Z',
messages: [
{ id: 'user-1', role: 'user', text: '请解释这个问题。\n\n文件在' },
{ id: 'assistant-1', role: 'assistant', text: '这是回答。' },
],
});
});
it('normalizes standalone text by removing inline code and artifact paths', () => {
expect(sanitizeAgentSessionText(
'回答见 `src/main.ts`,产物在 MEDIA:/tmp/out.png。\n[2026-08-13T10:00:00Z] INFO: done',
)).toBe('回答见 ,产物在 。');
});
it('removes unfenced source lines while keeping surrounding prose', () => {
expect(sanitizeAgentSessionText(
'先说明原因。\nconst result = buildResult();\n然后给出结论。',
)).toBe('先说明原因。\n然后给出结论。');
});
it('uploads the latest snapshot for a completed session and persists failures', async () => {
const messages: RawMessage[] = [
{ id: 'user-1', role: 'user', content: '第一个问题' },
{ id: 'assistant-1', role: 'assistant', content: '第一个回答' },
];
queueAgentSessionSync('prj_1', 'ses_1', messages);
await flushPendingAgentSessionSync();
expect(pushSessionDataMock).toHaveBeenCalledWith(
'user-a',
'access-token',
expect.objectContaining({
project_id: 'prj_1',
session_id: 'ses_1',
messages: [
{ id: 'user-1', role: 'user', text: '第一个问题' },
{ id: 'assistant-1', role: 'assistant', text: '第一个回答' },
],
}),
);
pushSessionDataMock.mockRejectedValueOnce(new Error('offline'));
queueAgentSessionSync('prj_1', 'ses_1', [
...messages,
{ id: 'user-2', role: 'user', content: '第二个问题' },
{ id: 'assistant-2', role: 'assistant', content: '第二个回答' },
]);
await flushPendingAgentSessionSync();
const stored = JSON.parse(window.localStorage.getItem('niancode-agent-session-sync-pending') ?? '[]') as Array<{ data?: { session_id?: string } }>;
expect(stored).toEqual(expect.arrayContaining([
expect.objectContaining({ data: expect.objectContaining({ session_id: 'ses_1' }) }),
]));
});
it('never uploads a pending snapshot under a different authenticated account', async () => {
queueAgentSessionSync('prj_1', 'ses_1', [
{ id: 'user-1', role: 'user', content: '账号 A 的问题' },
{ id: 'assistant-1', role: 'assistant', content: '账号 A 的回答' },
]);
getAuthStateMock.mockReturnValue({
getValidAccessToken: vi.fn().mockResolvedValue('account-b-token'),
user: { userId: 'user-b' },
});
await flushPendingAgentSessionSync();
expect(pushSessionDataMock).not.toHaveBeenCalled();
expect(window.localStorage.getItem('niancode-agent-session-sync-pending')).toContain('user-a');
});
});