feat(plugins): add batch install update and uninstall actions

This commit is contained in:
弈秋 authored and GitHub committed 2026-09-17 19:27:53 +08:00
1 parent df8a659cc1
commit 84019b4271
13 files changed
+988 -138

No files matched your search

@@ -0,0 +1,50 @@
import { describe, expect, it, vi } from "vitest";
import { isBatchSelectableListing, runBatch } from "@/lib/plugins/pluginBatch";
describe("runBatch", () => {
it("runs items sequentially and records all successes", async () => {
const order: string[] = [];
const outcome = await runBatch(
[{ id: "a" }, { id: "b" }, { id: "c" }],
(item) => item.id,
async (item) => {
order.push(item.id);
},
);
expect(order).toEqual(["a", "b", "c"]);
expect(outcome.succeeded).toEqual(["a", "b", "c"]);
expect(outcome.failed).toEqual([]);
});
it("captures a failing item, continues, and does not roll back", async () => {
const ran: string[] = [];
const outcome = await runBatch(
[{ id: "a" }, { id: "b" }, { id: "c" }],
(item) => item.id,
async (item) => {
ran.push(item.id);
if (item.id === "b") throw new Error("boom");
},
);
expect(ran).toEqual(["a", "b", "c"]);
expect(outcome.succeeded).toEqual(["a", "c"]);
expect(outcome.failed).toEqual([{ name: "b", error: "boom" }]);
});
it("returns an empty outcome for empty input", async () => {
const action = vi.fn();
const outcome = await runBatch([], (item: { id: string }) => item.id, action);
expect(action).not.toHaveBeenCalled();
expect(outcome.succeeded).toEqual([]);
expect(outcome.failed).toEqual([]);
});
});
describe("isBatchSelectableListing", () => {
it("allows install and update, blocks installed and unsupported", () => {
expect(isBatchSelectableListing("install")).toBe(true);
expect(isBatchSelectableListing("update")).toBe(true);
expect(isBatchSelectableListing("installed")).toBe(false);
expect(isBatchSelectableListing("unsupported")).toBe(false);
});
});
@@ -0,0 +1,46 @@
// Pure, testable helpers for batch plugin operations (install / update / uninstall).
// The component owns selection state and UI; this module owns the sequential execution
// and result aggregation so partial failures can be reported without aborting the batch.
export interface BatchItemOutcome {
name: string;
ok: boolean;
error?: string;
}
export interface BatchOutcome {
results: BatchItemOutcome[];
succeeded: string[];
failed: { name: string; error: string }[];
}
/**
* Run `action` over `items` sequentially, recording per-item success/failure. A failing item is
* captured and the batch continues; nothing is rolled back. `nameOf` supplies a human label used
* in the summary. Empty input yields an empty outcome.
*/
export async function runBatch<T>(items: readonly T[], nameOf: (item: T) => string, action: (item: T) => Promise<void>): Promise<BatchOutcome> {
const results: BatchItemOutcome[] = [];
for (const item of items) {
const name = nameOf(item);
try {
await action(item);
results.push({ name, ok: true });
} catch (cause) {
results.push({ name, ok: false, error: cause instanceof Error ? cause.message : String(cause) });
}
}
return {
results,
succeeded: results.filter((result) => result.ok).map((result) => result.name),
failed: results.filter((result) => !result.ok).map((result) => ({ name: result.name, error: result.error ?? "" })),
};
}
/**
* Which marketplace listing statuses are actionable in a batch (a selectable plugin that is not yet
* installed, or has an update). Installed / unsupported listings are not batch-selectable.
*/
export function isBatchSelectableListing(status: string): boolean {
return status === "install" || status === "update";
}