From 01627f37a1e950aaeb96084ce3b027c6373a1a02 Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Thu, 26 Feb 2026 10:04:05 +0400 Subject: [PATCH] fix: restore Linux AppImage updater routing and fallback port reporting --- api/download.js | 27 ++++--- src-tauri/sidecar/local-api-server.mjs | 1 + src-tauri/sidecar/local-api-server.test.mjs | 34 +++++++++ src/app/desktop-updater.ts | 4 + tests/download-handler.test.mjs | 82 +++++++++++++++++++++ 5 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 tests/download-handler.test.mjs diff --git a/api/download.js b/api/download.js index 36db3df88..794ea8248 100644 --- a/api/download.js +++ b/api/download.js @@ -12,23 +12,28 @@ const PLATFORM_PATTERNS = { 'linux-appimage': (name) => name.endsWith('_amd64.AppImage'), }; -const VARIANT_PREFIXES = { - full: ['world-monitor'], - world: ['world-monitor'], - tech: ['tech-monitor'], - finance: ['finance-monitor'], +const VARIANT_IDENTIFIERS = { + full: ['worldmonitor'], + world: ['worldmonitor'], + tech: ['techmonitor'], + finance: ['financemonitor'], }; +function canonicalAssetName(name) { + return String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, ''); +} + function findAssetForVariant(assets, variant, platformMatcher) { - const prefixes = VARIANT_PREFIXES[variant] ?? null; - if (!prefixes) return null; + const identifiers = VARIANT_IDENTIFIERS[variant] ?? null; + if (!identifiers) return null; return assets.find((asset) => { - const assetName = String(asset?.name || '').toLowerCase(); - const hasVariantPrefix = prefixes.some((prefix) => - assetName.startsWith(`${prefix.toLowerCase()}_`) || assetName.startsWith(`${prefix.toLowerCase()}-`) + const assetName = String(asset?.name || ''); + const normalizedAssetName = canonicalAssetName(assetName); + const hasVariantIdentifier = identifiers.some((identifier) => + normalizedAssetName.includes(identifier) ); - return hasVariantPrefix && platformMatcher(String(asset?.name || '')); + return hasVariantIdentifier && platformMatcher(assetName); }) ?? null; } diff --git a/src-tauri/sidecar/local-api-server.mjs b/src-tauri/sidecar/local-api-server.mjs index 382bc5bff..637e91351 100644 --- a/src-tauri/sidecar/local-api-server.mjs +++ b/src-tauri/sidecar/local-api-server.mjs @@ -1245,6 +1245,7 @@ export async function createLocalApiServer(options = {}) { const address = server.address(); const boundPort = typeof address === 'object' && address?.port ? address.port : context.port; + context.port = boundPort; const portFile = process.env.LOCAL_API_PORT_FILE; if (portFile) { diff --git a/src-tauri/sidecar/local-api-server.test.mjs b/src-tauri/sidecar/local-api-server.test.mjs index ae2ca4a78..cdcea5312 100644 --- a/src-tauri/sidecar/local-api-server.test.mjs +++ b/src-tauri/sidecar/local-api-server.test.mjs @@ -1347,3 +1347,37 @@ test('traffic log strips query strings from entries to protect privacy', async ( await localApi.cleanup(); } }); + +test('service-status reports bound fallback port after EADDRINUSE recovery', async () => { + const blocker = createServer((_req, res) => { + res.writeHead(200, { 'content-type': 'text/plain' }); + res.end('occupied'); + }); + await listen(blocker, '127.0.0.1', 46123); + + const localApi = await setupApiDir({}); + const app = await createLocalApiServer({ + port: 46123, + apiDir: localApi.apiDir, + logger: { log() {}, warn() {}, error() {} }, + }); + const { port } = await app.start(); + + try { + assert.notEqual(port, 46123); + + const response = await fetch(`http://127.0.0.1:${port}/api/service-status`); + assert.equal(response.status, 200); + const body = await response.json(); + + assert.equal(body.local.port, port); + const localService = body.services.find((service) => service.id === 'local-api'); + assert.equal(localService.description, `Running on 127.0.0.1:${port}`); + } finally { + await app.close(); + await localApi.cleanup(); + await new Promise((resolve, reject) => { + blocker.close((error) => (error ? reject(error) : resolve())); + }); + } +}); diff --git a/src/app/desktop-updater.ts b/src/app/desktop-updater.ts index 0bfec9b88..96d739118 100644 --- a/src/app/desktop-updater.ts +++ b/src/app/desktop-updater.ts @@ -133,6 +133,10 @@ export class DesktopUpdater implements AppModule { return null; } + if (normalizedOs === 'linux') { + return normalizedArch === 'x86_64' ? 'linux-appimage' : null; + } + return null; } diff --git a/tests/download-handler.test.mjs b/tests/download-handler.test.mjs new file mode 100644 index 000000000..e4d90ffcb --- /dev/null +++ b/tests/download-handler.test.mjs @@ -0,0 +1,82 @@ +import { strict as assert } from 'node:assert'; +import test from 'node:test'; +import handler from '../api/download.js'; + +const RELEASES_PAGE = 'https://github.com/koala73/worldmonitor/releases/latest'; + +function makeGitHubReleaseResponse(assets) { + return new Response(JSON.stringify({ assets }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); +} + +test('matches full variant for dotted World.Monitor AppImage asset names', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => makeGitHubReleaseResponse([ + { + name: 'World.Monitor_2.5.7_amd64.AppImage', + browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage', + }, + ]); + + try { + const response = await handler( + new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=full') + ); + assert.equal(response.status, 302); + assert.equal( + response.headers.get('location'), + 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage' + ); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('matches tech variant for dashed Tech-Monitor AppImage asset names', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => makeGitHubReleaseResponse([ + { + name: 'Tech-Monitor_2.5.7_amd64.AppImage', + browser_download_url: 'https://downloads.example/Tech-Monitor_2.5.7_amd64.AppImage', + }, + { + name: 'World.Monitor_2.5.7_amd64.AppImage', + browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage', + }, + ]); + + try { + const response = await handler( + new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=tech') + ); + assert.equal(response.status, 302); + assert.equal( + response.headers.get('location'), + 'https://downloads.example/Tech-Monitor_2.5.7_amd64.AppImage' + ); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('falls back to release page when requested variant has no matching asset', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => makeGitHubReleaseResponse([ + { + name: 'World.Monitor_2.5.7_amd64.AppImage', + browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage', + }, + ]); + + try { + const response = await handler( + new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=finance') + ); + assert.equal(response.status, 302); + assert.equal(response.headers.get('location'), RELEASES_PAGE); + } finally { + globalThis.fetch = originalFetch; + } +});