fix(update-center): load aptss updates reliably

This commit is contained in:
2026-04-16 13:32:23 +08:00
parent 0b784af3d7
commit 309b9bc003
8 changed files with 233 additions and 14 deletions
+1 -4
View File
@@ -52,10 +52,7 @@ const categoryCache = new Map<string, Promise<StoreAppMetadataMap>>();
const APTSS_LIST_UPGRADABLE_COMMAND = { const APTSS_LIST_UPGRADABLE_COMMAND = {
command: "bash", command: "bash",
args: [ args: ["-lc", "env LANGUAGE=en_US aptss list --upgradable"],
"-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_COMMAND = { const DPKG_QUERY_INSTALLED_COMMAND = {
+16 -8
View File
@@ -6,10 +6,9 @@ import type {
UpdateSource, UpdateSource,
} from "./types"; } from "./types";
const UPGRADABLE_PATTERN =
/^(\S+)\/\S+\s+([^\s]+)\s+\S+\s+\[(?:upgradable from|from):\s*([^\]]+)\]$/i;
const PRINT_URIS_PATTERN = /'([^']+)'\s+(\S+)\s+(\d+)\s+SHA512:([^\s]+)/; const PRINT_URIS_PATTERN = /'([^']+)'\s+(\S+)\s+(\d+)\s+SHA512:([^\s]+)/;
const APM_INSTALLED_PATTERN = /^(\S+)\/\S+(?:,\S+)?\s+\S+\s+\S+\s+\[[^\]]+\]$/; const APM_INSTALLED_PATTERN = /^(\S+)\/\S+(?:,\S+)?\s+\S+\s+\S+\s+\[[^\]]+\]$/;
const CURRENT_VERSION_PATTERN = /\[(?:upgradable from|from):\s*([^\]\s]+)\]/i;
const splitVersion = (version: string) => { const splitVersion = (version: string) => {
const epochMatch = version.match(/^(\d+):(.*)$/); const epochMatch = version.match(/^(\d+):(.*)$/);
@@ -190,18 +189,27 @@ const parseUpgradableOutput = (
const items: UpdateCenterItem[] = []; const items: UpdateCenterItem[] = [];
for (const line of output.split("\n")) { for (const line of output.split("\n")) {
const trimmed = line.trim(); const trimmed = line
if (!trimmed || trimmed.startsWith("Listing")) { .replace(
// eslint-disable-next-line no-control-regex
/\x1b\[[0-9;]*m/g,
"",
)
.trim();
if (!trimmed || trimmed.startsWith("Listing") || !trimmed.includes("/")) {
continue; continue;
} }
const match = trimmed.match(UPGRADABLE_PATTERN); const tokens = trimmed.split(/\s+/);
if (!match) { if (tokens.length < 3) {
continue; continue;
} }
const [, pkgname, nextVersion, currentVersion] = match; const pkgname = tokens[0]?.split("/")[0] ?? "";
const arch = trimmed.split(/\s+/)[2]; const nextVersion = tokens[1] ?? "";
const arch = tokens[2] ?? "";
const currentVersion =
trimmed.match(CURRENT_VERSION_PATTERN)?.[1] ?? tokens[5] ?? "";
if (!pkgname || nextVersion === currentVersion) { if (!pkgname || nextVersion === currentVersion) {
continue; continue;
} }
@@ -0,0 +1,78 @@
import { fireEvent, render, screen } from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import App from "@/App.vue";
const invoke = vi.fn();
const updateCenterOpen = vi.fn();
vi.mock("axios", () => {
const get = vi.fn(async () => ({ data: [] }));
return {
default: {
create: () => ({ get }),
},
};
});
describe("App update center runtime", () => {
beforeEach(() => {
invoke.mockReset();
updateCenterOpen.mockReset();
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "both";
if (channel === "check-spark-available") return true;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
return [];
});
Object.assign(window.ipcRenderer, {
invoke,
on: vi.fn(),
off: vi.fn(),
send: vi.fn(),
});
Object.assign(window, {
updateCenter: {
open: updateCenterOpen.mockResolvedValue({
items: [],
tasks: [],
warnings: [],
hasRunningTasks: false,
}),
refresh: vi.fn(),
ignore: vi.fn(),
unignore: vi.fn(),
start: vi.fn(),
cancel: vi.fn(),
getState: vi.fn(),
onState: vi.fn(),
offState: vi.fn(),
},
});
window.apm_store.arch = "amd64";
vi.stubGlobal(
"matchMedia",
vi.fn(() => ({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})),
);
});
it("opens update center with an empty snapshot without throwing", async () => {
render(App);
await fireEvent.click(await screen.findByText("软件更新"));
expect(updateCenterOpen).toHaveBeenCalledWith("both");
expect(await screen.findByText("暂无可展示的更新任务")).toBeTruthy();
});
});
@@ -0,0 +1,100 @@
import { fireEvent, render, screen } from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import App from "@/App.vue";
const invoke = vi.fn();
const open = vi.fn();
vi.mock("axios", () => {
const get = vi.fn(async (url: string) => {
if (url.includes("categories.json")) {
return { data: {} };
}
if (url.includes("homelinks.json") || url.includes("homelist.json")) {
return { data: [] };
}
if (url.includes("applist.json")) {
return { data: [] };
}
return { data: [] };
});
return {
default: {
create: () => ({ get }),
},
};
});
vi.mock("@/modules/updateCenter", () => ({
createUpdateCenterStore: () => ({
isOpen: { value: false },
showCloseConfirm: { value: false },
showMigrationConfirm: { value: false },
searchQuery: { value: "" },
selectedTaskKeys: { value: new Set<string>() },
snapshot: {
value: { items: [], tasks: [], warnings: [], hasRunningTasks: false },
},
filteredItems: { value: [] },
allSelected: { value: false },
someSelected: { value: false },
bind: vi.fn(),
unbind: vi.fn(),
open,
refresh: vi.fn(),
ignoreItem: vi.fn(),
unignoreItem: vi.fn(),
toggleSelection: vi.fn(),
toggleSelectAll: vi.fn(),
getSelectedItems: vi.fn(() => []),
closeNow: vi.fn(),
startSelected: vi.fn(),
requestClose: vi.fn(),
}),
}));
describe("App update center entry", () => {
beforeEach(() => {
invoke.mockReset();
open.mockReset();
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "both";
if (channel === "check-spark-available") return true;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
return [];
});
Object.assign(window.ipcRenderer, {
invoke,
on: vi.fn(),
off: vi.fn(),
send: vi.fn(),
});
window.apm_store.arch = "amd64";
vi.stubGlobal(
"matchMedia",
vi.fn(() => ({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})),
);
});
it("opens update center when clicking the sidebar action", async () => {
render(App);
await fireEvent.click(await screen.findByText("软件更新"));
expect(open).toHaveBeenCalledWith("both");
});
});
@@ -11,7 +11,7 @@ type RemoteStoreResponse =
| Array<Record<string, unknown>>; | Array<Record<string, unknown>>;
const APTSS_LIST_UPGRADABLE_KEY = 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"; "bash -lc env LANGUAGE=en_US aptss list --upgradable";
const DPKG_QUERY_INSTALLED_KEY = const DPKG_QUERY_INSTALLED_KEY =
"dpkg-query -W -f=${Package}\t${db:Status-Want} ${db:Status-Status} ${db:Status-Eflag}\n"; "dpkg-query -W -f=${Package}\t${db:Status-Want} ${db:Status-Status} ${db:Status-Eflag}\n";
@@ -28,6 +28,25 @@ describe("update-center query", () => {
]); ]);
}); });
it("parses aptss wrapper output with ansi noise before package lines", () => {
const output = [
"\u001b[1;32m信息:正在使用非 Root 权限模式启动!若出现问题,请尝试使用 Root 权限执行命令。\u001b[0m",
"正在列表...",
"spark-weather/stable 2.0.0 amd64 [upgradable from: 1.9.0]",
"",
].join("\n");
expect(parseAptssUpgradableOutput(output)).toEqual([
{
pkgname: "spark-weather",
source: "aptss",
currentVersion: "1.9.0",
nextVersion: "2.0.0",
arch: "amd64",
},
]);
});
it("parses the legacy from variant in upgradable output", () => { it("parses the legacy from variant in upgradable output", () => {
const aptssOutput = "spark-clock/stable 1.2.0 amd64 [from: 1.1.0]"; const aptssOutput = "spark-clock/stable 1.2.0 amd64 [from: 1.1.0]";
const apmOutput = "spark-player/main 2.0.0 amd64 [from: 1.5.0]"; const apmOutput = "spark-player/main 2.0.0 amd64 [from: 1.5.0]";
@@ -69,6 +69,18 @@ describe("updateCenter store", () => {
expect(store.filteredItems.value).toEqual(snapshot.items); expect(store.filteredItems.value).toEqual(snapshot.items);
}); });
it("reuses the last store filter when refreshing without an explicit filter", async () => {
const snapshot = createSnapshot();
open.mockResolvedValue(snapshot);
refresh.mockResolvedValue(snapshot);
const store = createUpdateCenterStore();
await store.open("apm");
await store.refresh();
expect(refresh).toHaveBeenCalledWith("apm");
});
it("starts only the selected non-ignored items", async () => { it("starts only the selected non-ignored items", async () => {
const snapshot = createSnapshot({ const snapshot = createSnapshot({
items: [ items: [
+6 -1
View File
@@ -59,6 +59,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
const searchQuery = ref(""); const searchQuery = ref("");
const selectedTaskKeys = ref(new Set<string>()); const selectedTaskKeys = ref(new Set<string>());
const snapshot = ref<UpdateCenterSnapshot>(EMPTY_SNAPSHOT); const snapshot = ref<UpdateCenterSnapshot>(EMPTY_SNAPSHOT);
let lastStoreFilter: StoreFilter = "both";
const resetSessionState = (): void => { const resetSessionState = (): void => {
showCloseConfirm.value = false; showCloseConfirm.value = false;
@@ -131,13 +132,17 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
}; };
const open = async (storeFilter: StoreFilter = "both"): Promise<void> => { const open = async (storeFilter: StoreFilter = "both"): Promise<void> => {
lastStoreFilter = storeFilter;
resetSessionState(); resetSessionState();
const nextSnapshot = await window.updateCenter.open(storeFilter); const nextSnapshot = await window.updateCenter.open(storeFilter);
applySnapshot(nextSnapshot); applySnapshot(nextSnapshot);
isOpen.value = true; isOpen.value = true;
}; };
const refresh = async (storeFilter: StoreFilter = "both"): Promise<void> => { const refresh = async (
storeFilter: StoreFilter = lastStoreFilter,
): Promise<void> => {
lastStoreFilter = storeFilter;
const nextSnapshot = await window.updateCenter.refresh(storeFilter); const nextSnapshot = await window.updateCenter.refresh(storeFilter);
applySnapshot(nextSnapshot); applySnapshot(nextSnapshot);
}; };