diff --git a/backend/api/routers/system.py b/backend/api/routers/system.py index 0666e1f1..fcaec4ab 100644 --- a/backend/api/routers/system.py +++ b/backend/api/routers/system.py @@ -476,7 +476,12 @@ async def clear_tauri_logs(): cleared.append(p) except OSError: failed += 1 - return {"cleared": cleared, "failed": failed} + if failed: + raise HTTPException( + status_code=500, + detail="One or more desktop log files could not be cleared. Close any app using them and retry.", + ) + return {"cleared": cleared, "failed": 0} @router.get("/sysinfo", response_model=SysinfoResponse) def get_sys_info(): diff --git a/frontend/src/test/LogsFooterClearFailure.test.jsx b/frontend/src/test/LogsFooterClearFailure.test.jsx new file mode 100644 index 00000000..68f09a2d --- /dev/null +++ b/frontend/src/test/LogsFooterClearFailure.test.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const { clearTauriLogs, toastError, toastSuccess } = vi.hoisted(() => ({ + clearTauriLogs: vi.fn(), + toastError: vi.fn(), + toastSuccess: vi.fn(), +})); + +vi.mock('../api/system', () => ({ + clearSystemLogs: vi.fn(), + clearTauriLogs, +})); +vi.mock('../api/hooks', () => ({ + useSystemLogs: () => ({ data: null, refetch: vi.fn() }), + useTauriLogs: () => ({ data: null, refetch: vi.fn() }), + useVisibleNotifications: () => ({ notifications: [] }), + isDismissibleNotification: () => false, +})); +vi.mock('../components/NetworkToggle', () => ({ default: () => null })); +vi.mock('react-hot-toast', () => ({ + default: Object.assign(vi.fn(), { error: toastError, success: toastSuccess }), +})); + +import LogsFooter from '../components/LogsFooter'; + +function renderFooter() { + return render( + + + , + ); +} + +describe('LogsFooter Tauri cleanup failure', () => { + beforeEach(() => { + localStorage.clear(); + localStorage.setItem('omnivoice.logs.active', 'tauri'); + clearTauriLogs.mockReset(); + toastError.mockReset(); + toastSuccess.mockReset(); + }); + + it('shows failure and never claims the log was cleared', async () => { + clearTauriLogs.mockRejectedValueOnce(new Error('desktop log is locked')); + renderFooter(); + + fireEvent.click(screen.getByRole('button', { name: /expand logs panel/i })); + fireEvent.click(screen.getByRole('button', { name: /clear log/i })); + + await waitFor(() => expect(toastError).toHaveBeenCalled()); + expect(toastSuccess).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/test_destructive_cleanup.py b/tests/test_destructive_cleanup.py index 36a2f012..c54cd6ae 100644 --- a/tests/test_destructive_cleanup.py +++ b/tests/test_destructive_cleanup.py @@ -104,7 +104,8 @@ async def test_tauri_log_clear_reports_truncate_failure(monkeypatch, tmp_path): lambda _path: (_ for _ in ()).throw(PermissionError("locked")), ) - result = await system.clear_tauri_logs() + with pytest.raises(HTTPException) as caught: + await system.clear_tauri_logs() - assert result == {"cleared": [], "failed": 1} - assert str(log) not in str(result) + assert caught.value.status_code == 500 + assert str(log) not in caught.value.detail