mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-05-30 01:31:06 +08:00
fix(update-center): load aptss updates reliably
This commit is contained in:
@@ -52,10 +52,7 @@ const categoryCache = new Map<string, Promise<StoreAppMetadataMap>>();
|
||||
|
||||
const APTSS_LIST_UPGRADABLE_COMMAND = {
|
||||
command: "bash",
|
||||
args: [
|
||||
"-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",
|
||||
],
|
||||
args: ["-lc", "env LANGUAGE=en_US aptss list --upgradable"],
|
||||
};
|
||||
|
||||
const DPKG_QUERY_INSTALLED_COMMAND = {
|
||||
|
||||
@@ -6,10 +6,9 @@ import type {
|
||||
UpdateSource,
|
||||
} 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 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 epochMatch = version.match(/^(\d+):(.*)$/);
|
||||
@@ -190,18 +189,27 @@ const parseUpgradableOutput = (
|
||||
const items: UpdateCenterItem[] = [];
|
||||
|
||||
for (const line of output.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("Listing")) {
|
||||
const trimmed = line
|
||||
.replace(
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/\x1b\[[0-9;]*m/g,
|
||||
"",
|
||||
)
|
||||
.trim();
|
||||
if (!trimmed || trimmed.startsWith("Listing") || !trimmed.includes("/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = trimmed.match(UPGRADABLE_PATTERN);
|
||||
if (!match) {
|
||||
const tokens = trimmed.split(/\s+/);
|
||||
if (tokens.length < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, pkgname, nextVersion, currentVersion] = match;
|
||||
const arch = trimmed.split(/\s+/)[2];
|
||||
const pkgname = tokens[0]?.split("/")[0] ?? "";
|
||||
const nextVersion = tokens[1] ?? "";
|
||||
const arch = tokens[2] ?? "";
|
||||
const currentVersion =
|
||||
trimmed.match(CURRENT_VERSION_PATTERN)?.[1] ?? tokens[5] ?? "";
|
||||
if (!pkgname || nextVersion === currentVersion) {
|
||||
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>>;
|
||||
|
||||
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 =
|
||||
"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", () => {
|
||||
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]";
|
||||
|
||||
@@ -69,6 +69,18 @@ describe("updateCenter store", () => {
|
||||
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 () => {
|
||||
const snapshot = createSnapshot({
|
||||
items: [
|
||||
|
||||
@@ -59,6 +59,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
const searchQuery = ref("");
|
||||
const selectedTaskKeys = ref(new Set<string>());
|
||||
const snapshot = ref<UpdateCenterSnapshot>(EMPTY_SNAPSHOT);
|
||||
let lastStoreFilter: StoreFilter = "both";
|
||||
|
||||
const resetSessionState = (): void => {
|
||||
showCloseConfirm.value = false;
|
||||
@@ -131,13 +132,17 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
};
|
||||
|
||||
const open = async (storeFilter: StoreFilter = "both"): Promise<void> => {
|
||||
lastStoreFilter = storeFilter;
|
||||
resetSessionState();
|
||||
const nextSnapshot = await window.updateCenter.open(storeFilter);
|
||||
applySnapshot(nextSnapshot);
|
||||
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);
|
||||
applySnapshot(nextSnapshot);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user