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 = {
|
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 = {
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user