import asyncio import io from types import SimpleNamespace import rio import components from n8n_client import WebhookError def view(): return SimpleNamespace(collection_name='my-store', pending_files=(), upload_error='', upload_status='', upload_progress='', is_uploading=False, force_refresh=lambda: None) def pick(state, name, data=b'# Notes'): file = rio.FileInfo(name, len(data), 'text/markdown', io.BytesIO(data)) components.KnowledgeApp.pick_file(state, rio.FilePickEvent(file)) return file def test_partial_failure_retries_only_failed_file_and_retains_bytes(monkeypatch): state = view() pick(state, 'first.md') pick(state, 'second.md') pick(state, 'third.md') calls = [] async def upload(name, contents, collection): calls.append(name) assert contents == b'# Notes' assert collection == 'my-store' if name == 'second.md' and calls.count(name) == 1: raise WebhookError('Временная ошибка') monkeypatch.setattr(components, 'client', SimpleNamespace(upload=upload)) asyncio.run(components.KnowledgeApp.upload_file(state)) assert calls == ['first.md', 'second.md', 'third.md'] assert len(state.pending_files) == 1 assert state.pending_files[0].contents == b'# Notes' assert 'second.md' in state.upload_error assert '2 из 3' in state.upload_status asyncio.run(components.KnowledgeApp.upload_file(state)) assert calls == ['first.md', 'second.md', 'third.md', 'second.md'] assert not state.pending_files assert not state.upload_error assert not state.is_uploading def test_invalid_files_do_not_remove_valid_selection(): state = view() good = pick(state, 'good.md') pick(state, 'bad.txt') pick(state, 'empty.md', b'') pick(state, 'another.md') assert len(state.pending_files) == 2 assert 'bad.txt' in state.upload_error and 'empty.md' in state.upload_error components.KnowledgeApp.remove_file(state, rio.FilePickEvent(good)) assert len(state.pending_files) == 1 assert state.pending_files[0].file.name == 'another.md' def test_busy_queue_is_immutable_and_duplicate_submission_is_ignored(monkeypatch): state = view() first = pick(state, 'first.md') calls = [] async def upload(*args): calls.append(args) pick(state, 'late.md') components.KnowledgeApp.remove_file(state, rio.FilePickEvent(first)) assert len(state.pending_files) == 1 await components.KnowledgeApp.upload_file(state) monkeypatch.setattr(components, 'client', SimpleNamespace(upload=upload)) asyncio.run(components.KnowledgeApp.upload_file(state)) assert len(calls) == 1 assert not state.pending_files def test_read_error_does_not_block_remaining_files(monkeypatch): state = view() broken = pick(state, 'broken.md') state.pending_files[0].file = SimpleNamespace(name='broken.md', read_bytes=broken_read) pick(state, 'good.md') calls = [] async def upload(name, *_): calls.append(name) monkeypatch.setattr(components, 'client', SimpleNamespace(upload=upload)) asyncio.run(components.KnowledgeApp.upload_file(state)) assert calls == ['good.md'] assert not state.pending_files assert 'Выберите его заново' in state.upload_error async def broken_read(): raise OSError('closed stream')