fix(agents): show only explicitly delivered chat files
This commit is contained in:
1 parent
922b09bda0
commit
e78459b384
12 files changed
+429
-62
No files matched your search
@@ -3,7 +3,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testi
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CloudComposerAttachments } from '@/pages/CloudAgents/CloudComposerAttachments';
|
||||
import { CloudPendingContext } from '@/pages/CloudAgents/CloudPending';
|
||||
import type { CloudAttachment, CloudUpload } from '../../shared/cloud-agents';
|
||||
import type { CloudAttachment, CloudHistory, CloudUpload } from '../../shared/cloud-agents';
|
||||
|
||||
const api = vi.hoisted(() => ({ call: vi.fn(), upload: vi.fn(), download: vi.fn() }));
|
||||
vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api }));
|
||||
@@ -12,6 +12,11 @@ const photo: CloudAttachment = { file_id: 'photo-id', file_name: '我的画.png'
|
||||
const document: CloudAttachment = { file_id: 'document-id', file_name: '故事.docx', file_size: 1200, path: '/attachments/story', original_path: '/original/story' };
|
||||
const upload: CloudUpload = { object_name: 'temporary/photo-object', file_name: photo.file_name, file_type: 'image/png', parse_supported: true, parse_methods: ['disable', 'rapid_ocr'] };
|
||||
|
||||
function delivered(threadId: string, path: string): CloudHistory {
|
||||
return { thread_id: threadId, next_offset: null, run: null, messages: [{ id: 1, role: 'assistant',
|
||||
content: `[下载文件](sandbox:${path})`, request_id: 'delivery-request', run_id: null, created_at: '',
|
||||
}] };
|
||||
}
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
@@ -23,8 +28,8 @@ function operationCalls(operation: string) {
|
||||
return api.call.mock.calls.filter(([name]) => name === operation);
|
||||
}
|
||||
|
||||
function open({ initialSelected = [], threadId, disabled = false, ensureThread = vi.fn().mockResolvedValue('thread-created') }: {
|
||||
initialSelected?: string[]; threadId?: string; disabled?: boolean; ensureThread?: () => Promise<string>;
|
||||
function open({ initialSelected = [], threadId, disabled = false, deliveryHistory, ensureThread = vi.fn().mockResolvedValue('thread-created') }: {
|
||||
initialSelected?: string[]; threadId?: string; disabled?: boolean; deliveryHistory?: CloudHistory; ensureThread?: () => Promise<string>;
|
||||
} = {}) {
|
||||
const onBusy = vi.fn();
|
||||
const onSelected = vi.fn();
|
||||
@@ -34,7 +39,7 @@ function open({ initialSelected = [], threadId, disabled = false, ensureThread =
|
||||
const [selected, setSelected] = useState(initialSelected);
|
||||
const [text, setText] = useState('保留这些文字');
|
||||
return <CloudPendingContext.Provider value={pending}><form onSubmit={event => { event.preventDefault(); submit(); }}>
|
||||
<CloudComposerAttachments threadId={threadId} disabled={disabled} ensureThread={ensureThread} selected={selected} onBusy={onBusy}
|
||||
<CloudComposerAttachments threadId={threadId} deliveryHistory={deliveryHistory} disabled={disabled} ensureThread={ensureThread} selected={selected} onBusy={onBusy}
|
||||
onSelected={value => { onSelected(value); setSelected(value); }} footer={<span data-testid="composer-footer">发送区</span>}>
|
||||
<textarea aria-label="消息" value={text} onChange={event => setText(event.target.value)} />
|
||||
</CloudComposerAttachments>
|
||||
@@ -106,7 +111,7 @@ describe('CloudComposerAttachments', () => {
|
||||
expect(operationCalls('confirmAttachment')).toHaveLength(0);
|
||||
expect(view.onSelected).not.toHaveBeenCalled();
|
||||
expect(screen.getByLabelText('消息')).toHaveValue('保留这些文字');
|
||||
expect(view.hasPending()).toBe(false);
|
||||
await waitFor(() => expect(view.hasPending()).toBe(false));
|
||||
});
|
||||
|
||||
it('retains the uploaded object and resolved conversation for an explicit confirmation retry', async () => {
|
||||
@@ -263,7 +268,7 @@ describe('CloudComposerAttachments', () => {
|
||||
if (operation === 'files' && ++filesReads === 1) throw new Error('文件列表首次读取中断');
|
||||
return { files: [] };
|
||||
});
|
||||
const view = open({ threadId: 'thread-existing', initialSelected: [document.file_id] });
|
||||
const view = open({ threadId: 'thread-existing', initialSelected: [document.file_id], deliveryHistory: delivered('thread-existing', '/pending-only.txt') });
|
||||
expect(screen.queryByRole('button', { name: /^附件/ })).not.toBeInTheDocument();
|
||||
// A failed companion list opens the library while the upload list is still loading.
|
||||
fireEvent.click(await screen.findByRole('button', { name: '附件', exact: true }));
|
||||
@@ -332,7 +337,7 @@ describe('CloudComposerAttachments', () => {
|
||||
if (input.path === '/') return { files: [{ name: '作品', path: '/作品', directory_path: '/作品', is_dir: true, size: 0 }] };
|
||||
return { files: [{ name: '故事.txt', path: '/作品/故事.txt', directory_path: '/作品', is_dir: false, size: 500 }] };
|
||||
});
|
||||
const view = open({ threadId: 'thread-existing' });
|
||||
const view = open({ threadId: 'thread-existing', deliveryHistory: delivered('thread-existing', '/作品/故事.txt') });
|
||||
await openLibrary(1);
|
||||
expect(api.call).toHaveBeenCalledWith('files', { thread_id: 'thread-existing', path: '/作品' });
|
||||
fireEvent.click(await screen.findByRole('button', { name: '保存 故事.txt' }));
|
||||
@@ -342,4 +347,34 @@ describe('CloudComposerAttachments', () => {
|
||||
expect(view.ensureThread).not.toHaveBeenCalled();
|
||||
expect(view.submit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps file reads stable for equivalent delivery history and rechecks on explicit refresh or revision changes', async () => {
|
||||
const history = delivered('stable-thread', '/stories/story.txt');
|
||||
history.messages[0].content += '\n[下载笔记](sandbox:/stories/notes.txt)';
|
||||
api.call.mockImplementation(async operation => operation === 'attachments' ? { attachments: [] } : { files: [
|
||||
{ name: 'story.txt', path: '/stories/story.txt', directory_path: '/stories', is_dir: false, size: 500 },
|
||||
{ name: 'notes.txt', path: '/stories/notes.txt', directory_path: '/stories', is_dir: false, size: 200 },
|
||||
] });
|
||||
const props = { threadId: 'stable-thread', ensureThread: vi.fn(), selected: [], onSelected: vi.fn(), onBusy: vi.fn(), disabled: false };
|
||||
const view = render(<CloudComposerAttachments {...props} deliveryHistory={history} filesRevision="revision-1" />);
|
||||
await screen.findByRole('button', { name: '附件,共 2 个文件' });
|
||||
expect(operationCalls('files')).toHaveLength(1);
|
||||
const equivalent = { ...history, messages: history.messages.map(item => ({ ...item,
|
||||
content: '文件仍然是这两个。\n[笔记](sandbox:/stories/notes.txt)\n[故事](sandbox:/stories/story.txt)\n[再看故事](sandbox:/stories/story.txt)',
|
||||
})) };
|
||||
view.rerender(<CloudComposerAttachments {...props} deliveryHistory={equivalent} filesRevision="revision-1" />);
|
||||
await act(async () => undefined);
|
||||
expect(operationCalls('files')).toHaveLength(1);
|
||||
expect(screen.getByRole('button', { name: '附件,共 2 个文件' })).toBeVisible();
|
||||
const dialog = await openLibrary(2);
|
||||
await waitFor(() => expect(operationCalls('files')).toHaveLength(2));
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '刷新附件' }));
|
||||
await waitFor(() => expect(operationCalls('files')).toHaveLength(3));
|
||||
view.rerender(<CloudComposerAttachments {...props} deliveryHistory={equivalent} filesRevision="revision-2" />);
|
||||
await waitFor(() => expect(operationCalls('files')).toHaveLength(4));
|
||||
expect(operationCalls('files')).toEqual(Array.from({ length: 4 }, () => ['files', { thread_id: 'stable-thread', path: '/stories' }]));
|
||||
expect(within(dialog).getByRole('button', { name: '保存 story.txt' })).toBeVisible();
|
||||
expect(within(dialog).getByRole('button', { name: '保存 notes.txt' })).toBeVisible();
|
||||
expect(props.ensureThread).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user