import { afterEach, describe, expect, it, vi } from "vitest"; interface CommandResult { code: number; stdout: string; stderr: string; } type RemoteStoreResponse = | Record | Array>; const APTSS_LIST_UPGRADABLE_KEY = "bash -lc env LANGUAGE=en_US /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf list --upgradable -o Dir::Etc::sourcelist=/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list -o Dir::Etc::sourceparts=/dev/null -o APT::Get::List-Cleanup=0"; const DPKG_QUERY_INSTALLED_KEY = "dpkg-query -W -f=${Package}\t${db:Status-Want} ${db:Status-Status} ${db:Status-Eflag}\n"; const APM_PRINT_URIS_KEY = "bash -lc amber-pm-debug /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf download spark-weather --print-uris"; const APTSS_WEATHER_PRINT_URIS_KEY = "bash -lc /usr/bin/apt download spark-weather --print-uris -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf -o Dir::Etc::sourcelist=/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list -o Dir::Etc::sourceparts=/dev/null"; const APTSS_NOTES_PRINT_URIS_KEY = "bash -lc /usr/bin/apt download spark-notes --print-uris -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf -o Dir::Etc::sourcelist=/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list -o Dir::Etc::sourceparts=/dev/null"; const loadUpdateCenterModule = async ( remoteStore: Record, ) => { vi.resetModules(); vi.doMock("node:fs", () => { const existsSync = () => false; const readdirSync = () => [] as string[]; const readFileSync = () => { throw new Error("Unexpected icon file read"); }; return { default: { existsSync, readdirSync, readFileSync, }, existsSync, readdirSync, readFileSync, }; }); vi.doMock("node:child_process", async () => { const actual = await vi.importActual( "node:child_process", ); return { ...actual, spawnSync: (command: string, args: string[]) => { if (command === "dpkg" && args[0] === "-L") { return { status: 1, error: undefined, output: null, pid: 0, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), }; } return actual.spawnSync(command, args); }, }; }); vi.stubGlobal( "fetch", vi.fn(async (input: string | URL) => { const url = String(input); const body = remoteStore[url]; return { ok: body !== undefined, async json() { if (body === undefined) { throw new Error(`Unexpected fetch for ${url}`); } return body; }, }; }), ); return await import("../../../../electron/main/backend/update-center/index"); }; afterEach(() => { vi.doUnmock("node:fs"); vi.doUnmock("node:child_process"); vi.unstubAllGlobals(); }); describe("update-center load items", () => { it("enriches apm migration items with download metadata and remote fallback icons", async () => { const commandResults = new Map([ [ APTSS_LIST_UPGRADABLE_KEY, { code: 0, stdout: "spark-weather/stable 2.0.0 amd64 [upgradable from: 1.0.0]", stderr: "", }, ], [ "apm list --upgradable", { code: 0, stdout: "spark-weather/main 3.0.0 amd64 [upgradable from: 1.5.0]", stderr: "", }, ], [ DPKG_QUERY_INSTALLED_KEY, { code: 0, stdout: "spark-weather\tinstall ok installed\n", stderr: "", }, ], [ "apm list --installed", { code: 0, stdout: "", stderr: "", }, ], [ APM_PRINT_URIS_KEY, { code: 0, stdout: "'https://example.invalid/spark-weather_3.0.0_amd64.deb' spark-weather_3.0.0_amd64.deb 123456 SHA512:deadbeef", stderr: "", }, ], [ APTSS_WEATHER_PRINT_URIS_KEY, { code: 0, stdout: "'https://example.invalid/spark-weather_2.0.0_amd64.deb' spark-weather_2.0.0_amd64.deb 123456 SHA512:deadbeef", stderr: "", }, ], ]); const { loadUpdateCenterItems } = await loadUpdateCenterModule({ "https://erotica.spark-app.store/amd64-store/categories.json": { tools: { zh: "Tools" }, }, "https://erotica.spark-app.store/amd64-store/tools/applist.json": [ { Name: "Spark Weather", Pkgname: "spark-weather" }, ], "https://erotica.spark-app.store/amd64-apm/categories.json": { tools: { zh: "Tools" }, }, "https://erotica.spark-app.store/amd64-apm/tools/applist.json": [ { Name: "Spark Weather", Pkgname: "spark-weather" }, ], }); const result = await loadUpdateCenterItems(async (command, args) => { const key = `${command} ${args.join(" ")}`; const match = commandResults.get(key); if (!match) { throw new Error(`Missing mock for ${key}`); } return match; }); expect(result.warnings).toEqual([]); expect(result.items).toContainEqual({ pkgname: "spark-weather", source: "apm", currentVersion: "1.5.0", nextVersion: "3.0.0", arch: "amd64", category: "tools", name: "Spark Weather", remoteIcon: "https://erotica.spark-app.store/amd64-apm/tools/spark-weather/icon.png", downloadUrl: "https://example.invalid/spark-weather_3.0.0_amd64.deb", fileName: "spark-weather_3.0.0_amd64.deb", size: 123456, sha512: "deadbeef", isMigration: true, migrationSource: "aptss", migrationTarget: "apm", aptssVersion: "2.0.0", }); }); it("degrades to aptss-only results when apm commands fail", async () => { const { loadUpdateCenterItems } = await loadUpdateCenterModule({ "https://erotica.spark-app.store/amd64-store/categories.json": { office: { zh: "Office" }, }, "https://erotica.spark-app.store/amd64-store/office/applist.json": [ { Name: "Spark Notes", Pkgname: "spark-notes" }, ], }); const result = await loadUpdateCenterItems(async (command, args) => { const key = `${command} ${args.join(" ")}`; if (key === APTSS_LIST_UPGRADABLE_KEY) { return { code: 0, stdout: "spark-notes/stable 2.0.0 amd64 [upgradable from: 1.0.0]", stderr: "", }; } if (key === DPKG_QUERY_INSTALLED_KEY) { return { code: 0, stdout: "spark-notes\tinstall ok installed\n", stderr: "", }; } if (key === APTSS_NOTES_PRINT_URIS_KEY) { return { code: 0, stdout: "'https://example.invalid/spark-notes_2.0.0_amd64.deb' spark-notes_2.0.0_amd64.deb 654321 SHA512:beadfeed", stderr: "", }; } if (key === APTSS_NOTES_PRINT_URIS_KEY) { return { code: 0, stdout: "'https://example.invalid/spark-notes_2.0.0_amd64.deb' spark-notes_2.0.0_amd64.deb 654321 SHA512:beadfeed", stderr: "", }; } if (key === "apm list --upgradable" || key === "apm list --installed") { return { code: 127, stdout: "", stderr: "apm: command not found", }; } throw new Error(`Unexpected command ${key}`); }); expect(result.items).toEqual([ { pkgname: "spark-notes", source: "aptss", currentVersion: "1.0.0", nextVersion: "2.0.0", arch: "amd64", category: "office", name: "Spark Notes", remoteIcon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png", downloadUrl: "https://example.invalid/spark-notes_2.0.0_amd64.deb", fileName: "spark-notes_2.0.0_amd64.deb", size: 654321, sha512: "beadfeed", }, ]); expect(result.warnings).toEqual([ "apm upgradable query failed: apm: command not found", "apm installed query failed: apm: command not found", ]); }); it("retries category lookup after an earlier fetch failure in the same process", async () => { const remoteStore: Record = {}; const { loadUpdateCenterItems } = await loadUpdateCenterModule(remoteStore); const runCommand = async (command: string, args: string[]) => { const key = `${command} ${args.join(" ")}`; if (key === APTSS_LIST_UPGRADABLE_KEY) { return { code: 0, stdout: "spark-notes/stable 2.0.0 amd64 [upgradable from: 1.0.0]", stderr: "", }; } if (key === DPKG_QUERY_INSTALLED_KEY) { return { code: 0, stdout: "spark-notes\tinstall ok installed\n", stderr: "", }; } if (key === APTSS_NOTES_PRINT_URIS_KEY) { return { code: 0, stdout: "'https://example.invalid/spark-notes_2.0.0_amd64.deb' spark-notes_2.0.0_amd64.deb 654321 SHA512:beadfeed", stderr: "", }; } if (key === "apm list --upgradable" || key === "apm list --installed") { return { code: 127, stdout: "", stderr: "apm: command not found", }; } throw new Error(`Unexpected command ${key}`); }; const firstResult = await loadUpdateCenterItems(runCommand); expect(firstResult.items).toEqual([ { pkgname: "spark-notes", source: "aptss", currentVersion: "1.0.0", nextVersion: "2.0.0", arch: "amd64", downloadUrl: "https://example.invalid/spark-notes_2.0.0_amd64.deb", fileName: "spark-notes_2.0.0_amd64.deb", size: 654321, sha512: "beadfeed", }, ]); remoteStore["https://erotica.spark-app.store/amd64-store/categories.json"] = { office: { zh: "Office" }, }; remoteStore[ "https://erotica.spark-app.store/amd64-store/office/applist.json" ] = [{ Name: "Spark Notes", Pkgname: "spark-notes" }]; const secondResult = await loadUpdateCenterItems(runCommand); expect(secondResult.items).toEqual([ { pkgname: "spark-notes", source: "aptss", currentVersion: "1.0.0", nextVersion: "2.0.0", arch: "amd64", category: "office", name: "Spark Notes", remoteIcon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png", downloadUrl: "https://example.invalid/spark-notes_2.0.0_amd64.deb", fileName: "spark-notes_2.0.0_amd64.deb", size: 654321, sha512: "beadfeed", }, ]); }); it("keeps successfully loaded categories when another category applist fetch fails", async () => { const { loadUpdateCenterItems } = await loadUpdateCenterModule({ "https://erotica.spark-app.store/amd64-store/categories.json": { office: { zh: "Office" }, tools: { zh: "Tools" }, }, "https://erotica.spark-app.store/amd64-store/office/applist.json": [ { Name: "Spark Notes", Pkgname: "spark-notes" }, ], }); const result = await loadUpdateCenterItems(async (command, args) => { const key = `${command} ${args.join(" ")}`; if (key === APTSS_LIST_UPGRADABLE_KEY) { return { code: 0, stdout: "spark-notes/stable 2.0.0 amd64 [upgradable from: 1.0.0]", stderr: "", }; } if (key === DPKG_QUERY_INSTALLED_KEY) { return { code: 0, stdout: "spark-notes\tinstall ok installed\n", stderr: "", }; } if (key === APTSS_NOTES_PRINT_URIS_KEY) { return { code: 0, stdout: "'https://example.invalid/spark-notes_2.0.0_amd64.deb' spark-notes_2.0.0_amd64.deb 654321 SHA512:beadfeed", stderr: "", }; } if (key === "apm list --upgradable" || key === "apm list --installed") { return { code: 127, stdout: "", stderr: "apm: command not found", }; } throw new Error(`Unexpected command ${key}`); }); expect(result.items).toEqual([ { pkgname: "spark-notes", source: "aptss", currentVersion: "1.0.0", nextVersion: "2.0.0", arch: "amd64", category: "office", name: "Spark Notes", remoteIcon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png", downloadUrl: "https://example.invalid/spark-notes_2.0.0_amd64.deb", fileName: "spark-notes_2.0.0_amd64.deb", size: 654321, sha512: "beadfeed", }, ]); expect(result.warnings).toEqual([ "apm upgradable query failed: apm: command not found", "apm installed query failed: apm: command not found", ]); }); });