feat: replace Code conversation archive with deletion
This commit is contained in:
1 parent
140f6be8db
commit
bf101c1e52
19 files changed
+1001
-292
No files matched your search
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user