mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
新增更新中心模块,支持管理 APM 和传统 deb 软件更新任务 - 添加更新任务队列管理、状态跟踪和日志记录功能 - 实现更新项忽略配置持久化存储 - 新增更新确认对话框和迁移提示 - 优化主窗口关闭时的任务保护机制 - 添加单元测试覆盖核心逻辑
304 lines
8.2 KiB
TypeScript
304 lines
8.2 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import type { UpdateCenterItem } from "../../../../electron/main/backend/update-center/types";
|
|
import {
|
|
createTaskRunner,
|
|
buildLegacySparkUpgradeCommand,
|
|
installUpdateItem,
|
|
} from "../../../../electron/main/backend/update-center/install";
|
|
import { createUpdateCenterQueue } from "../../../../electron/main/backend/update-center/queue";
|
|
|
|
const childProcessMock = vi.hoisted(() => ({
|
|
spawnCalls: [] as Array<{ command: string; args: string[] }>,
|
|
}));
|
|
|
|
vi.mock("node:child_process", () => ({
|
|
default: {
|
|
spawn: vi.fn((command: string, args: string[] = []) => {
|
|
childProcessMock.spawnCalls.push({ command, args });
|
|
|
|
return {
|
|
stdout: { on: vi.fn() },
|
|
stderr: { on: vi.fn() },
|
|
on: vi.fn(
|
|
(
|
|
event: string,
|
|
callback: ((code?: number) => void) | (() => void),
|
|
) => {
|
|
if (event === "close") {
|
|
callback(0);
|
|
}
|
|
},
|
|
),
|
|
};
|
|
}),
|
|
},
|
|
spawn: vi.fn((command: string, args: string[] = []) => {
|
|
childProcessMock.spawnCalls.push({ command, args });
|
|
|
|
return {
|
|
stdout: { on: vi.fn() },
|
|
stderr: { on: vi.fn() },
|
|
on: vi.fn(
|
|
(event: string, callback: ((code?: number) => void) | (() => void)) => {
|
|
if (event === "close") {
|
|
callback(0);
|
|
}
|
|
},
|
|
),
|
|
};
|
|
}),
|
|
}));
|
|
|
|
const createAptssItem = (): UpdateCenterItem => ({
|
|
pkgname: "spark-weather",
|
|
source: "aptss",
|
|
currentVersion: "1.0.0",
|
|
nextVersion: "2.0.0",
|
|
downloadUrl: "https://example.invalid/spark-weather_2.0.0_amd64.deb",
|
|
fileName: "spark-weather_2.0.0_amd64.deb",
|
|
});
|
|
|
|
const createApmItem = (): UpdateCenterItem => ({
|
|
pkgname: "spark-player",
|
|
source: "apm",
|
|
currentVersion: "1.0.0",
|
|
nextVersion: "2.0.0",
|
|
downloadUrl: "https://example.invalid/spark-player_2.0.0_amd64.deb",
|
|
fileName: "spark-player_2.0.0_amd64.deb",
|
|
});
|
|
|
|
describe("update-center task runner", () => {
|
|
it("runs download then install and marks the task as completed", async () => {
|
|
const queue = createUpdateCenterQueue();
|
|
const item = createAptssItem();
|
|
const steps: string[] = [];
|
|
|
|
queue.setItems([item]);
|
|
const task = queue.enqueueItem(item);
|
|
|
|
const runner = createTaskRunner(queue, {
|
|
runDownload: async (context) => {
|
|
steps.push(`download:${context.task.pkgname}`);
|
|
context.onLog("download-started");
|
|
context.onProgress(40);
|
|
|
|
return {
|
|
filePath: `/tmp/${context.item.fileName}`,
|
|
};
|
|
},
|
|
installItem: async (context) => {
|
|
steps.push(`install:${context.task.pkgname}`);
|
|
context.onLog("install-started");
|
|
},
|
|
});
|
|
|
|
await runner.runNextTask();
|
|
|
|
expect(steps).toEqual(["download:spark-weather", "install:spark-weather"]);
|
|
expect(queue.getSnapshot()).toMatchObject({
|
|
hasRunningTasks: false,
|
|
warnings: [],
|
|
tasks: [
|
|
{
|
|
id: task.id,
|
|
pkgname: "spark-weather",
|
|
status: "completed",
|
|
progress: 100,
|
|
logs: [
|
|
expect.objectContaining({ message: "download-started" }),
|
|
expect.objectContaining({ message: "install-started" }),
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("returns a direct aptss upgrade command instead of spark-update-tool", () => {
|
|
expect(
|
|
buildLegacySparkUpgradeCommand("spark-weather", "/usr/bin/pkexec"),
|
|
).toEqual({
|
|
execCommand: "/usr/bin/pkexec",
|
|
execParams: [
|
|
"/opt/spark-store/extras/shell-caller.sh",
|
|
"aptss",
|
|
"install",
|
|
"-y",
|
|
"spark-weather",
|
|
"--only-upgrade",
|
|
],
|
|
});
|
|
});
|
|
|
|
it("blocks close while a refresh or task is still running", () => {
|
|
const queue = createUpdateCenterQueue();
|
|
const item = createAptssItem();
|
|
|
|
expect(queue.getSnapshot().hasRunningTasks).toBe(false);
|
|
|
|
queue.startRefresh();
|
|
expect(queue.getSnapshot().hasRunningTasks).toBe(true);
|
|
|
|
queue.finishRefresh(["metadata warning"]);
|
|
expect(queue.getSnapshot()).toMatchObject({
|
|
hasRunningTasks: false,
|
|
warnings: ["metadata warning"],
|
|
});
|
|
|
|
const task = queue.enqueueItem(item);
|
|
queue.markActiveTask(task.id, "downloading");
|
|
expect(queue.getSnapshot().hasRunningTasks).toBe(true);
|
|
|
|
queue.finishTask(task.id, "cancelled");
|
|
expect(queue.getSnapshot().hasRunningTasks).toBe(false);
|
|
});
|
|
|
|
it("propagates privilege escalation into the install path", async () => {
|
|
const queue = createUpdateCenterQueue();
|
|
const item = createAptssItem();
|
|
const installCalls: Array<{ superUserCmd?: string; filePath?: string }> =
|
|
[];
|
|
|
|
queue.setItems([item]);
|
|
queue.enqueueItem(item);
|
|
|
|
const runner = createTaskRunner(queue, {
|
|
superUserCmd: "/usr/bin/pkexec",
|
|
runDownload: async () => ({ filePath: "/tmp/spark-weather.deb" }),
|
|
installItem: async (context) => {
|
|
installCalls.push({
|
|
superUserCmd: context.superUserCmd,
|
|
filePath: context.filePath,
|
|
});
|
|
},
|
|
});
|
|
|
|
await runner.runNextTask();
|
|
|
|
expect(installCalls).toEqual([
|
|
{
|
|
superUserCmd: "/usr/bin/pkexec",
|
|
filePath: "/tmp/spark-weather.deb",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("fails fast for apm items without a file path", async () => {
|
|
const queue = createUpdateCenterQueue();
|
|
const item = {
|
|
...createApmItem(),
|
|
downloadUrl: undefined,
|
|
fileName: undefined,
|
|
};
|
|
|
|
queue.setItems([item]);
|
|
const task = queue.enqueueItem(item);
|
|
|
|
const runner = createTaskRunner(queue, {
|
|
installItem: async (context) => {
|
|
throw new Error(`unexpected install for ${context.item.pkgname}`);
|
|
},
|
|
});
|
|
|
|
await runner.runNextTask();
|
|
|
|
expect(queue.getSnapshot()).toMatchObject({
|
|
hasRunningTasks: false,
|
|
tasks: [
|
|
{
|
|
id: task.id,
|
|
status: "failed",
|
|
error: "APM update task requires downloaded package metadata",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("does not duplicate work across concurrent runNextTask calls", async () => {
|
|
const queue = createUpdateCenterQueue();
|
|
const item = createAptssItem();
|
|
let releaseDownload: (() => void) | undefined;
|
|
const downloadGate = new Promise<void>((resolve) => {
|
|
releaseDownload = resolve;
|
|
});
|
|
const runDownload = vi.fn(async () => {
|
|
await downloadGate;
|
|
return { filePath: "/tmp/spark-weather.deb" };
|
|
});
|
|
const installItem = vi.fn(async () => {});
|
|
|
|
queue.setItems([item]);
|
|
queue.enqueueItem(item);
|
|
|
|
const runner = createTaskRunner(queue, {
|
|
runDownload,
|
|
installItem,
|
|
});
|
|
|
|
const firstRun = runner.runNextTask();
|
|
const secondRun = runner.runNextTask();
|
|
releaseDownload?.();
|
|
|
|
const results = await Promise.all([firstRun, secondRun]);
|
|
|
|
expect(runDownload).toHaveBeenCalledTimes(1);
|
|
expect(installItem).toHaveBeenCalledTimes(1);
|
|
expect(results[1]).toBeNull();
|
|
});
|
|
|
|
it("marks the task as failed when install fails", async () => {
|
|
const queue = createUpdateCenterQueue();
|
|
const item = createAptssItem();
|
|
|
|
queue.setItems([item]);
|
|
const task = queue.enqueueItem(item);
|
|
|
|
const runner = createTaskRunner(queue, {
|
|
runDownload: async (context) => {
|
|
context.onProgress(30);
|
|
return { filePath: "/tmp/spark-weather.deb" };
|
|
},
|
|
installItem: async () => {
|
|
throw new Error("install exploded");
|
|
},
|
|
});
|
|
|
|
await runner.runNextTask();
|
|
|
|
expect(queue.getSnapshot()).toMatchObject({
|
|
hasRunningTasks: false,
|
|
tasks: [
|
|
{
|
|
id: task.id,
|
|
status: "failed",
|
|
progress: 30,
|
|
error: "install exploded",
|
|
logs: [expect.objectContaining({ message: "install exploded" })],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("does not fall through to ssinstall for apm file installs", async () => {
|
|
childProcessMock.spawnCalls.length = 0;
|
|
|
|
await installUpdateItem({
|
|
item: createApmItem(),
|
|
filePath: "/tmp/spark-player.deb",
|
|
superUserCmd: "/usr/bin/pkexec",
|
|
});
|
|
|
|
expect(childProcessMock.spawnCalls).toEqual([
|
|
{
|
|
command: "/usr/bin/pkexec",
|
|
args: [
|
|
"/opt/spark-store/extras/shell-caller.sh",
|
|
"apm",
|
|
"ssaudit",
|
|
"/tmp/spark-player.deb",
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
});
|