From 7499570766055512fbc2911de795aeacd0ae5f83 Mon Sep 17 00:00:00 2001 From: pixelpaws Date: Thu, 25 Sep 2025 14:50:06 +0800 Subject: [PATCH] fix(updates): avoid network stack traces offline --- py/routes/update_routes.py | 18 +++++- static/js/managers/UpdateService.js | 15 +++-- tests/frontend/managers/updateService.test.js | 45 ++++++++++++++ tests/routes/test_update_routes.py | 59 +++++++++++++++++++ 4 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 tests/frontend/managers/updateService.test.js create mode 100644 tests/routes/test_update_routes.py diff --git a/py/routes/update_routes.py b/py/routes/update_routes.py index a25e085e..11d77ed7 100644 --- a/py/routes/update_routes.py +++ b/py/routes/update_routes.py @@ -5,12 +5,16 @@ import git import zipfile import shutil import tempfile -from aiohttp import web +import asyncio +from aiohttp import web, ClientError from typing import Dict, List from ..services.downloader import get_downloader logger = logging.getLogger(__name__) +NETWORK_EXCEPTIONS = (ClientError, OSError, asyncio.TimeoutError) + + class UpdateRoutes: """Routes for handling plugin update checks""" @@ -63,6 +67,12 @@ class UpdateRoutes: 'nightly': nightly }) + except NETWORK_EXCEPTIONS as e: + logger.warning("Network unavailable during update check: %s", e) + return web.json_response({ + 'success': False, + 'error': 'Network unavailable for update check' + }) except Exception as e: logger.error(f"Failed to check for updates: {e}", exc_info=True) return web.json_response({ @@ -283,6 +293,9 @@ class UpdateRoutes: return version, changelog + except NETWORK_EXCEPTIONS as e: + logger.warning("Unable to reach GitHub for nightly version: %s", e) + return "main", [] except Exception as e: logger.error(f"Error fetching nightly version: {e}", exc_info=True) return "main", [] @@ -448,6 +461,9 @@ class UpdateRoutes: return version, changelog + except NETWORK_EXCEPTIONS as e: + logger.warning("Unable to reach GitHub for release info: %s", e) + return "v0.0.0", [] except Exception as e: logger.error(f"Error fetching remote version: {e}", exc_info=True) return "v0.0.0", [] diff --git a/static/js/managers/UpdateService.js b/static/js/managers/UpdateService.js index b1d29960..56a83ccf 100644 --- a/static/js/managers/UpdateService.js +++ b/static/js/managers/UpdateService.js @@ -82,11 +82,15 @@ export class UpdateService { } } - async checkForUpdates() { + async checkForUpdates({ force = false } = {}) { + if (!force && !this.updateNotificationsEnabled) { + return; + } + // Check if we should perform an update check const now = Date.now(); - const forceCheck = this.lastCheckTime === 0; - + const forceCheck = force || this.lastCheckTime === 0; + if (!forceCheck && now - this.lastCheckTime < this.updateCheckInterval) { // If we already have update info, just update the UI if (this.updateAvailable) { @@ -94,7 +98,7 @@ export class UpdateService { } return; } - + try { // Call backend API to check for updates with nightly flag const response = await fetch(`/api/lm/check-updates?nightly=${this.nightlyMode}`); @@ -435,8 +439,7 @@ export class UpdateService { } async manualCheckForUpdates() { - this.lastCheckTime = 0; // Reset last check time to force check - await this.checkForUpdates(); + await this.checkForUpdates({ force: true }); // Ensure badge visibility is updated after manual check this.updateBadgeVisibility(); } diff --git a/tests/frontend/managers/updateService.test.js b/tests/frontend/managers/updateService.test.js new file mode 100644 index 00000000..b5d289e8 --- /dev/null +++ b/tests/frontend/managers/updateService.test.js @@ -0,0 +1,45 @@ +import { describe, beforeEach, afterEach, expect, it, vi } from 'vitest'; +import { UpdateService } from '../../../static/js/managers/UpdateService.js'; + +function createFetchResponse(payload) { + return { + json: vi.fn().mockResolvedValue(payload) + }; +} + +describe('UpdateService passive checks', () => { + let service; + let fetchMock; + + beforeEach(() => { + fetchMock = vi.fn().mockResolvedValue(createFetchResponse({ + success: true, + current_version: 'v1.0.0', + latest_version: 'v1.0.0', + git_info: { short_hash: 'abc123' } + })); + global.fetch = fetchMock; + + service = new UpdateService(); + service.updateNotificationsEnabled = false; + service.lastCheckTime = 0; + service.nightlyMode = false; + }); + + afterEach(() => { + delete global.fetch; + }); + + it('skips passive update checks when notifications are disabled', async () => { + await service.checkForUpdates(); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('allows manual checks even when notifications are disabled', async () => { + await service.checkForUpdates({ force: true }); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith('/api/lm/check-updates?nightly=false'); + }); +}); diff --git a/tests/routes/test_update_routes.py b/tests/routes/test_update_routes.py new file mode 100644 index 00000000..dd9c9658 --- /dev/null +++ b/tests/routes/test_update_routes.py @@ -0,0 +1,59 @@ +import logging +from aiohttp import ClientError +import pytest + +from py.routes import update_routes + + +class OfflineDownloader: + async def make_request(self, *_, **__): + return False, "Cannot connect to host" + + +class RaisingDownloader: + async def make_request(self, *_, **__): + raise ClientError("offline") + + +async def _stub_downloader(instance): + return instance + + +@pytest.mark.asyncio +async def test_get_remote_version_offline_logs_without_traceback(monkeypatch, caplog): + caplog.set_level(logging.WARNING) + monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(OfflineDownloader())) + + version, changelog = await update_routes.UpdateRoutes._get_remote_version() + + assert version == "v0.0.0" + assert changelog == [] + assert "Failed to fetch GitHub release" in caplog.text + assert "Cannot connect to host" in caplog.text + assert "Traceback" not in caplog.text + + +@pytest.mark.asyncio +async def test_get_remote_version_network_error_logs_warning(monkeypatch, caplog): + caplog.set_level(logging.WARNING) + monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(RaisingDownloader())) + + version, changelog = await update_routes.UpdateRoutes._get_remote_version() + + assert version == "v0.0.0" + assert changelog == [] + assert "Unable to reach GitHub for release info" in caplog.text + assert "Traceback" not in caplog.text + + +@pytest.mark.asyncio +async def test_get_nightly_version_network_error_logs_warning(monkeypatch, caplog): + caplog.set_level(logging.WARNING) + monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(RaisingDownloader())) + + version, changelog = await update_routes.UpdateRoutes._get_nightly_version() + + assert version == "main" + assert changelog == [] + assert "Unable to reach GitHub for nightly version" in caplog.text + assert "Traceback" not in caplog.text