// @vitest-environment jsdom import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { MAX_CHAT_COMPOSER_ATTACHMENTS, useDirectProjectChatController, } from '../src/view/project-development/chat/controller/useDirectProjectChatController'; import type { DirectCodexTurnAttachment } from '../src/view/project-development/chat/conversation/directCodexTurnAttachments'; // jsdom 的 Blob/File 没实现 `arrayBuffer`,而附件导入链路要按字节读文件。 if (typeof Blob.prototype.arrayBuffer !== 'function') { Blob.prototype.arrayBuffer = () => Promise.resolve(new Uint8Array([120]).buffer); } function files(count: number, prefix: string) { return Array.from( { length: count }, (_, index) => new File(['x'], `${prefix}-${index}.png`, { type: 'image/png' }), ); } afterEach(() => { cleanup(); vi.restoreAllMocks(); window.__TAURI__ = undefined; }); describe('聊天输入盒的附件上限', () => { it('并发导入也守得住上限:在飞的条数先占名额', async () => { let uploadSeq = 0; const invoke = vi.fn(async (command: string, args?: unknown) => { if (command !== 'upload_local_asset') { throw new Error(`unexpected invoke ${command}`); } const fileName = (args as { fileName?: string }).fileName ?? 'file'; return { localPath: `assets/upload-${(uploadSeq += 1)}-${fileName}` }; }); window.__TAURI__ = { core: { invoke: invoke as never } }; const { result } = renderHook(() => useDirectProjectChatController({ assets: [], enabled: false, ensureConversationReadAllowed: async () => true, ensureConversationWriteAllowed: async () => true, onRuntimeError: () => undefined, projectId: 'project-1', projectPath: 'C:/project', refreshManifest: () => undefined, }), ); // 两次并发导入各带 5 个文件:第一次占住 5 个名额,第二次只剩 3 个名额。 const imported: DirectCodexTurnAttachment[][] = [[], []]; await act(async () => { const first = result.current.uploadFiles(files(5, 'first'), 0); const second = result.current.uploadFiles(files(5, 'second'), 0); [imported[0], imported[1]] = await Promise.all([first, second]); }); await waitFor(() => { expect(result.current.attachmentsImporting).toBe(false); }); expect(imported[0]).toHaveLength(5); expect(imported[1]).toHaveLength(3); expect(imported[0].length + imported[1].length).toBe( MAX_CHAT_COMPOSER_ATTACHMENTS, ); }); });