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'); }); });