164 lines
5.6 KiB
TypeScript
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');
|
|
});
|
|
});
|