完善客户端模块与工作区能力
This commit is contained in:
163
tests/unit/agent-session-sync.test.ts
Normal file
163
tests/unit/agent-session-sync.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user