Back up unknown legacy Agents, reconnect settled Conversation observation, remove remaining active repository residue, and restore the bundled Python verifier import.
239 lines
8.0 KiB
TypeScript
239 lines
8.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { RawMessage } from '@/types/chat';
|
|
import {
|
|
buildAgentSessionData,
|
|
flushPendingAgentSessionSync,
|
|
queueCodingConversationSessionSync,
|
|
queueAgentSessionSync,
|
|
resetAgentSessionSyncForTests,
|
|
sanitizeAgentSessionText,
|
|
} from '@/lib/agent-session-sync';
|
|
import { createProductSnapshot } from '../fixtures/coding-conversation-product-fixtures';
|
|
|
|
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('uploads natural-language messages from the final product Conversation snapshot', async () => {
|
|
queueCodingConversationSessionSync({
|
|
...createProductSnapshot('conversation-1'),
|
|
conversation: {
|
|
...createProductSnapshot('conversation-1').conversation,
|
|
projectId: 'project-1',
|
|
},
|
|
nodes: [
|
|
{
|
|
kind: 'message',
|
|
id: 'user-1',
|
|
role: 'user',
|
|
status: 'complete',
|
|
blocks: [{ kind: 'text', id: 'user-text', text: '解释一下。', status: 'complete' }],
|
|
},
|
|
{
|
|
kind: 'message',
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
status: 'complete',
|
|
blocks: [
|
|
{ kind: 'thinking', id: 'thought', text: '内部推理', status: 'complete' },
|
|
{ kind: 'text', id: 'answer', text: '这是自然语言回答。', status: 'complete' },
|
|
],
|
|
},
|
|
],
|
|
run: {
|
|
status: 'idle',
|
|
runId: 'run-1',
|
|
settledAt: Date.parse('2026-08-24T08:00:00.000Z'),
|
|
terminalReason: 'completed',
|
|
},
|
|
});
|
|
await flushPendingAgentSessionSync();
|
|
|
|
expect(pushSessionDataMock).toHaveBeenCalledWith(
|
|
'user-a',
|
|
'access-token',
|
|
{
|
|
project_id: 'project-1',
|
|
session_id: 'conversation-1',
|
|
updated_at: '2026-08-24T08:00:00.000Z',
|
|
messages: [
|
|
{ id: 'user-1', role: 'user', text: '解释一下。' },
|
|
{ id: 'assistant-1', role: 'assistant', text: '这是自然语言回答。' },
|
|
],
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not queue a product Conversation without assistant natural language', async () => {
|
|
const base = createProductSnapshot('conversation-1');
|
|
queueCodingConversationSessionSync({
|
|
...base,
|
|
nodes: [{
|
|
kind: 'message',
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
status: 'complete',
|
|
blocks: [{ kind: 'thinking', id: 'thought', text: '内部推理', status: 'complete' }],
|
|
}],
|
|
run: {
|
|
status: 'idle',
|
|
runId: 'run-1',
|
|
settledAt: Date.parse('2026-08-24T08:00:00.000Z'),
|
|
terminalReason: 'completed',
|
|
},
|
|
});
|
|
await flushPendingAgentSessionSync();
|
|
|
|
expect(pushSessionDataMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|