fix(sources): hide unavailable update and management entries

This commit is contained in:
2026-04-16 13:04:54 +08:00
parent e1ec526cb9
commit 0b784af3d7
16 changed files with 667 additions and 58 deletions
+37
View File
@@ -0,0 +1,37 @@
import { render, screen } from "@testing-library/vue";
import { describe, expect, it } from "vitest";
import AppSidebar from "@/components/AppSidebar.vue";
const renderSidebar = (
overrides: Partial<InstanceType<typeof AppSidebar>["$props"]> = {},
) => {
return render(AppSidebar, {
props: {
categories: {},
activeCategory: "all",
categoryCounts: { all: 0 },
themeMode: "auto",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
...overrides,
},
});
};
describe("AppSidebar", () => {
it("shows management and update entries when at least one source is usable", () => {
renderSidebar({ sparkAvailable: true, apmAvailable: false });
expect(screen.getByText("应用管理")).toBeTruthy();
expect(screen.getByText("软件更新")).toBeTruthy();
});
it("hides management and update entries when both sources are unavailable", () => {
renderSidebar({ sparkAvailable: false, apmAvailable: false });
expect(screen.queryByText("应用管理")).toBeNull();
expect(screen.queryByText("软件更新")).toBeNull();
});
});
@@ -35,6 +35,7 @@ describe("InstalledAppsModal", () => {
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
},
});
@@ -54,6 +55,7 @@ describe("InstalledAppsModal", () => {
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
},
});
@@ -71,6 +73,7 @@ describe("InstalledAppsModal", () => {
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
},
});
@@ -92,6 +95,7 @@ describe("InstalledAppsModal", () => {
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
},
});
@@ -113,6 +117,7 @@ describe("InstalledAppsModal", () => {
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
},
});
@@ -129,6 +134,7 @@ describe("InstalledAppsModal", () => {
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
},
});
+80
View File
@@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";
import {
getEffectiveStoreFilter,
getAllowedInstalledOrigin,
getDefaultInstalledOrigin,
isOriginEnabled,
isOriginUsable,
} from "@/modules/storeFilter";
describe("storeFilter helpers", () => {
it("reports whether an origin is enabled by the current store filter", () => {
expect(isOriginEnabled("both", "spark")).toBe(true);
expect(isOriginEnabled("both", "apm")).toBe(true);
expect(isOriginEnabled("spark", "spark")).toBe(true);
expect(isOriginEnabled("spark", "apm")).toBe(false);
expect(isOriginEnabled("apm", "apm")).toBe(true);
expect(isOriginEnabled("apm", "spark")).toBe(false);
});
it("chooses the default installed origin from the active store filter", () => {
expect(getDefaultInstalledOrigin("spark", { spark: true, apm: true })).toBe(
"spark",
);
expect(getDefaultInstalledOrigin("apm", { spark: true, apm: true })).toBe(
"apm",
);
expect(getDefaultInstalledOrigin("both", { spark: true, apm: true })).toBe(
"apm",
);
expect(getDefaultInstalledOrigin("both", { spark: true, apm: false })).toBe(
"spark",
);
expect(
getDefaultInstalledOrigin("both", { spark: false, apm: false }),
).toBe(null);
});
it("redirects disallowed installed origins to an allowed one", () => {
expect(
getAllowedInstalledOrigin("spark", "apm", { spark: true, apm: true }),
).toBe("spark");
expect(
getAllowedInstalledOrigin("apm", "spark", { spark: true, apm: true }),
).toBe("apm");
expect(
getAllowedInstalledOrigin("both", "apm", { spark: true, apm: false }),
).toBe("spark");
expect(
getAllowedInstalledOrigin("both", "spark", { spark: false, apm: false }),
).toBeNull();
});
it("computes the effective runtime store filter from source availability", () => {
expect(getEffectiveStoreFilter("both", { spark: true, apm: true })).toBe(
"both",
);
expect(getEffectiveStoreFilter("both", { spark: true, apm: false })).toBe(
"spark",
);
expect(getEffectiveStoreFilter("both", { spark: false, apm: true })).toBe(
"apm",
);
expect(getEffectiveStoreFilter("both", { spark: false, apm: false })).toBe(
null,
);
});
it("only treats enabled and installed origins as usable", () => {
expect(isOriginUsable("both", "spark", { spark: true, apm: false })).toBe(
true,
);
expect(isOriginUsable("both", "apm", { spark: true, apm: false })).toBe(
false,
);
expect(isOriginUsable("spark", "apm", { spark: true, apm: true })).toBe(
false,
);
});
});
@@ -25,6 +25,9 @@ const APTSS_WEATHER_PRINT_URIS_KEY =
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 WHICH_APTSS_KEY = "which aptss";
const WHICH_APM_KEY = "which apm";
const loadUpdateCenterModule = async (
remoteStore: Record<string, RemoteStoreResponse>,
) => {
@@ -106,6 +109,22 @@ afterEach(() => {
describe("update-center load items", () => {
it("enriches apm migration items with download metadata and remote fallback icons", async () => {
const commandResults = new Map<string, CommandResult>([
[
WHICH_APTSS_KEY,
{
code: 0,
stdout: "/usr/bin/aptss\n",
stderr: "",
},
],
[
WHICH_APM_KEY,
{
code: 0,
stdout: "/usr/bin/apm\n",
stderr: "",
},
],
[
APTSS_LIST_UPGRADABLE_KEY,
{
@@ -217,6 +236,14 @@ describe("update-center load items", () => {
const result = await loadUpdateCenterItems(async (command, args) => {
const key = `${command} ${args.join(" ")}`;
if (key === WHICH_APTSS_KEY) {
return { code: 0, stdout: "/usr/bin/aptss\n", stderr: "" };
}
if (key === WHICH_APM_KEY) {
return { code: 127, stdout: "", stderr: "apm: command not found" };
}
if (key === APTSS_LIST_UPGRADABLE_KEY) {
return {
code: 0,
@@ -279,10 +306,7 @@ describe("update-center load items", () => {
sha512: "beadfeed",
},
]);
expect(result.warnings).toEqual([
"apm upgradable query failed: apm: command not found",
"apm installed query failed: apm: command not found",
]);
expect(result.warnings).toEqual([]);
});
it("retries category lookup after an earlier fetch failure in the same process", async () => {
@@ -292,6 +316,14 @@ describe("update-center load items", () => {
const runCommand = async (command: string, args: string[]) => {
const key = `${command} ${args.join(" ")}`;
if (key === WHICH_APTSS_KEY) {
return { code: 0, stdout: "/usr/bin/aptss\n", stderr: "" };
}
if (key === WHICH_APM_KEY) {
return { code: 127, stdout: "", stderr: "apm: command not found" };
}
if (key === APTSS_LIST_UPGRADABLE_KEY) {
return {
code: 0,
@@ -387,6 +419,14 @@ describe("update-center load items", () => {
const result = await loadUpdateCenterItems(async (command, args) => {
const key = `${command} ${args.join(" ")}`;
if (key === WHICH_APTSS_KEY) {
return { code: 0, stdout: "/usr/bin/aptss\n", stderr: "" };
}
if (key === WHICH_APM_KEY) {
return { code: 127, stdout: "", stderr: "apm: command not found" };
}
if (key === APTSS_LIST_UPGRADABLE_KEY) {
return {
code: 0,
@@ -440,9 +480,122 @@ describe("update-center load items", () => {
sha512: "beadfeed",
},
]);
expect(result.warnings).toEqual([
"apm upgradable query failed: apm: command not found",
"apm installed query failed: apm: command not found",
expect(result.warnings).toEqual([]);
});
it("skips aptss commands when the store filter disables Spark", async () => {
const { loadUpdateCenterItems } = await loadUpdateCenterModule({
"https://erotica.spark-app.store/amd64-apm/categories.json": {
tools: { zh: "Tools" },
},
"https://erotica.spark-app.store/amd64-apm/tools/applist.json": [
{ Name: "Spark Clock", Pkgname: "spark-clock" },
],
});
const runCommand = vi.fn(async (command: string, args: string[]) => {
const key = `${command} ${args.join(" ")}`;
if (key === WHICH_APM_KEY) {
return { code: 0, stdout: "/usr/bin/apm\n", stderr: "" };
}
if (key === "apm list --upgradable") {
return {
code: 0,
stdout: "spark-clock/main 2.0.0 amd64 [upgradable from: 1.0.0]",
stderr: "",
};
}
if (key === "apm list --installed") {
return {
code: 0,
stdout: "",
stderr: "",
};
}
if (
key ===
"bash -lc amber-pm-debug /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf download spark-clock --print-uris"
) {
return {
code: 0,
stdout:
"'https://example.invalid/spark-clock_2.0.0_amd64.deb' spark-clock_2.0.0_amd64.deb 1234 SHA512:feedface",
stderr: "",
};
}
throw new Error(`Unexpected command ${key}`);
});
await loadUpdateCenterItems(runCommand, "apm");
expect(runCommand).not.toHaveBeenCalledWith(
"bash",
expect.arrayContaining([
expect.stringContaining("apt list --upgradable"),
]),
);
expect(runCommand).not.toHaveBeenCalledWith(
"dpkg-query",
expect.any(Array),
);
});
it("skips apm commands when the store filter disables APM", 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 runCommand = vi.fn(async (command: string, args: string[]) => {
const key = `${command} ${args.join(" ")}`;
if (key === WHICH_APTSS_KEY) {
return { code: 0, stdout: "/usr/bin/aptss\n", stderr: "" };
}
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: "",
};
}
throw new Error(`Unexpected command ${key}`);
});
await loadUpdateCenterItems(runCommand, "spark");
expect(runCommand).not.toHaveBeenCalledWith("apm", [
"list",
"--upgradable",
]);
expect(runCommand).not.toHaveBeenCalledWith("apm", ["list", "--installed"]);
});
});
@@ -157,8 +157,8 @@ describe("update-center/ipc", () => {
await cancelHandler?.({}, "aptss:spark-weather");
expect(getStateHandler?.()).toEqual(snapshot);
expect(service.open).toHaveBeenCalledTimes(1);
expect(service.refresh).toHaveBeenCalledTimes(1);
expect(service.open).toHaveBeenCalledWith("both");
expect(service.refresh).toHaveBeenCalledWith("both");
expect(service.ignore).toHaveBeenCalledWith({
packageName: "spark-weather",
newVersion: "2.0.0",
@@ -176,6 +176,51 @@ describe("update-center/ipc", () => {
expect(send).toHaveBeenCalledWith("update-center-state", snapshot);
});
it("forwards store filter payloads to open and refresh", async () => {
const handle = vi.fn();
const snapshot: UpdateCenterServiceState = {
items: [],
tasks: [],
warnings: [],
hasRunningTasks: false,
};
const service = {
open: vi.fn().mockResolvedValue(snapshot),
refresh: vi.fn().mockResolvedValue(snapshot),
ignore: vi.fn().mockResolvedValue(undefined),
unignore: vi.fn().mockResolvedValue(undefined),
start: vi.fn().mockResolvedValue(undefined),
cancel: vi.fn().mockResolvedValue(undefined),
getState: vi.fn().mockReturnValue(snapshot),
subscribe: vi.fn(() => () => undefined),
};
registerUpdateCenterIpc({ handle }, service);
const openHandler = handle.mock.calls.find(
([channel]: [string]) => channel === "update-center-open",
)?.[1] as
| ((
event: unknown,
storeFilter?: "spark" | "apm" | "both",
) => Promise<UpdateCenterServiceState>)
| undefined;
const refreshHandler = handle.mock.calls.find(
([channel]: [string]) => channel === "update-center-refresh",
)?.[1] as
| ((
event: unknown,
storeFilter?: "spark" | "apm" | "both",
) => Promise<UpdateCenterServiceState>)
| undefined;
await openHandler?.({}, "apm");
await refreshHandler?.({}, "spark");
expect(service.open).toHaveBeenCalledWith("apm");
expect(service.refresh).toHaveBeenCalledWith("spark");
});
it("service subscribers receive state updates after refresh start and ignore", async () => {
let ignoredEntries = new Set<string>();
const send = vi.fn();
@@ -61,9 +61,9 @@ describe("updateCenter store", () => {
open.mockResolvedValue(snapshot);
const store = createUpdateCenterStore();
await store.open();
await store.open("apm");
expect(open).toHaveBeenCalledTimes(1);
expect(open).toHaveBeenCalledWith("apm");
expect(store.isOpen.value).toBe(true);
expect(store.snapshot.value).toEqual(snapshot);
expect(store.filteredItems.value).toEqual(snapshot.items);