feat: replace Code conversation archive with deletion

This commit is contained in:
鲨鱼辣椒 committed 2026-10-14 11:09:51 +08:00
1 parent 140f6be8db
commit bf101c1e52
19 files changed
+1001 -292

No files matched your search

+123 -1
View File
@@ -1,7 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { RawMessage } from '@/types/chat';
import {
buildAgentSessionData,
forgetCodingConversationSessionSync,
flushPendingAgentSessionSync,
queueCodingConversationSessionSync,
queueAgentSessionSync,
@@ -10,6 +11,23 @@ import {
} from '@/lib/agent-session-sync';
import { createProductSnapshot } from '../fixtures/coding-conversation-product-fixtures';
const PENDING_STORAGE_KEY = 'niancode-agent-session-sync-pending';
function completedSnapshot(conversationId: string) {
const snapshot = createProductSnapshot(conversationId);
snapshot.nodes = [
{
kind: 'message', id: 'user-1', role: 'user', status: 'complete',
blocks: [{ kind: 'text', id: 'question', text: '解释一下。', status: 'complete' }],
},
{
kind: 'message', id: 'assistant-1', role: 'assistant', status: 'complete',
blocks: [{ kind: 'text', id: 'answer', text: '这是回答。', status: 'complete' }],
},
];
return snapshot;
}
const getAuthStateMock = vi.hoisted(() => vi.fn());
const getProfileStateMock = vi.hoisted(() => vi.fn());
const pushSessionDataMock = vi.hoisted(() => vi.fn());
@@ -46,6 +64,11 @@ describe('local Agent session sync', () => {
pushSessionDataMock.mockResolvedValue(undefined);
});
afterEach(() => {
resetAgentSessionSyncForTests();
vi.useRealTimers();
});
it('removes code, paths, logs, tool parts, attachments and errors from session snapshots', () => {
const messages: RawMessage[] = [
{
@@ -274,4 +297,103 @@ describe('local Agent session sync', () => {
expect(pushSessionDataMock).not.toHaveBeenCalled();
expect(window.localStorage.getItem('niancode-agent-session-sync-pending')).toContain('user-a');
});
it('forgets persisted copies of the deleted local conversation across accounts only', async () => {
const data = buildAgentSessionData('project-a', 'deleted-conversation', [
{ id: 'user-1', role: 'user', content: '问题' },
{ id: 'assistant-1', role: 'assistant', content: '回答' },
])!;
window.localStorage.setItem(PENDING_STORAGE_KEY, JSON.stringify([
{ userId: 'user-a', data, retryCount: 2 },
{ userId: 'user-b', data, retryCount: 1 },
{ userId: 'user-b', data: { ...data, session_id: 'kept-conversation' }, retryCount: 1 },
]));
forgetCodingConversationSessionSync('deleted-conversation');
expect(JSON.parse(window.localStorage.getItem(PENDING_STORAGE_KEY) ?? '[]')).toEqual([
{ userId: 'user-b', data: { ...data, session_id: 'kept-conversation' }, retryCount: 1 },
]);
getAuthStateMock.mockReturnValue({
getValidAccessToken: vi.fn().mockResolvedValue('account-b-token'),
user: { userId: 'user-b' },
});
getProfileStateMock.mockReturnValue({
profilesByUserId: { 'user-b': { displayName: '另一个用户' } },
pushSessionData: pushSessionDataMock,
});
queueCodingConversationSessionSync(completedSnapshot('deleted-conversation'));
await flushPendingAgentSessionSync();
expect(pushSessionDataMock).toHaveBeenCalledTimes(1);
expect(pushSessionDataMock).toHaveBeenCalledWith(
'user-b', 'account-b-token', expect.objectContaining({ session_id: 'kept-conversation' }),
);
});
it('clears a deleted conversation debounce while another conversation still uploads', async () => {
vi.useFakeTimers();
queueCodingConversationSessionSync(completedSnapshot('deleted-conversation'));
queueCodingConversationSessionSync(completedSnapshot('kept-conversation'));
forgetCodingConversationSessionSync('deleted-conversation');
expect(vi.getTimerCount()).toBe(1);
expect(window.localStorage.getItem(PENDING_STORAGE_KEY)).not.toContain('deleted-conversation');
await vi.advanceTimersByTimeAsync(1_000);
expect(pushSessionDataMock).toHaveBeenCalledTimes(1);
expect(pushSessionDataMock).toHaveBeenCalledWith(
'user-a', 'access-token', expect.objectContaining({ session_id: 'kept-conversation' }),
);
});
it('does not send a deleted conversation after its access-token wait completes', async () => {
vi.useFakeTimers();
let resolveToken!: (token: string) => void;
getAuthStateMock.mockReturnValue({
getValidAccessToken: vi.fn().mockImplementation(() => new Promise<string>((resolve) => {
resolveToken = resolve;
})),
user: { userId: 'user-a' },
});
queueCodingConversationSessionSync(completedSnapshot('deleted-conversation'));
const flushing = flushPendingAgentSessionSync();
forgetCodingConversationSessionSync('deleted-conversation');
queueCodingConversationSessionSync(completedSnapshot('deleted-conversation'));
resolveToken('access-token');
await flushing;
await vi.advanceTimersByTimeAsync(300_000);
expect(pushSessionDataMock).not.toHaveBeenCalled();
expect(window.localStorage.getItem(PENDING_STORAGE_KEY)).toBe('[]');
expect(vi.getTimerCount()).toBe(0);
});
it.each(['success', 'failure'] as const)(
'does not revive a deleted conversation after in-flight upload %s',
async (outcome) => {
vi.useFakeTimers();
let finishUpload!: () => void;
pushSessionDataMock.mockImplementationOnce(() => new Promise<void>((resolve, reject) => {
finishUpload = () => outcome === 'success' ? resolve() : reject(new Error('offline'));
}));
queueCodingConversationSessionSync(completedSnapshot('deleted-conversation'));
const flushing = flushPendingAgentSessionSync();
await Promise.resolve();
expect(pushSessionDataMock).toHaveBeenCalledTimes(1);
queueCodingConversationSessionSync(completedSnapshot('deleted-conversation'));
forgetCodingConversationSessionSync('deleted-conversation');
finishUpload();
await flushing;
queueCodingConversationSessionSync(completedSnapshot('deleted-conversation'));
await vi.advanceTimersByTimeAsync(300_000);
await flushPendingAgentSessionSync();
expect(pushSessionDataMock).toHaveBeenCalledTimes(1);
expect(window.localStorage.getItem(PENDING_STORAGE_KEY)).toBe('[]');
expect(vi.getTimerCount()).toBe(0);
},
);
});