fix(plugins): preserve uninstall retries and bound mock private paths (#2073)

Related #1947

Uninstall keeps a plugin registered but stopped until its private data, secrets and package are cleaned up; a failure is shown and Uninstall can be retried, also after a restart (a durable cleanup record outside the package restores the target). While cleanup is pending, enable, reload, consent, permission changes, reinstall, replacement and rollback are refused. Secret-deletion errors are reported, and cleanup never follows a symlinked ancestor outside plugin storage. The SDK mock checks private-path components and rejects traversal independently of filesystem grants.

Co-authored-by: arnaud-ritti <[email protected]>
Цей коміт міститься в:
Arnaud Ritti authored and GitHub committed 2026-11-08 08:29:19 +08:00
1 джерело a2599a6f79
коміт 99102d8a40
73 files changed
+794 -49

No files matched your search

+38
Переглянути файл
@@ -723,6 +723,44 @@ describe("PluginsPage", () => {
expect(notes.at(-1)).toMatchObject({ title: "Uninstalled Hello" });
});
it("keeps failed cleanup visible and lets Uninstall retry", async () => {
const list = backend.list;
const uninstall = backend.uninstall;
let failed = false;
vi.spyOn(backend, "list").mockImplementation(async () => {
const snapshot = await list();
if (failed) {
snapshot.plugins = snapshot.plugins.map((p) =>
p.id === "hello" ? { ...p, status: "failed", error: "Private data is locked" } : p,
);
}
return snapshot;
});
const remove = vi.spyOn(backend, "uninstall").mockImplementation(async (id) => {
if (!failed) {
failed = true;
throw new Error("Private data is locked");
}
await uninstall(id);
});
const user = userEvent.setup();
render(<PluginsPage />);
await screen.findByRole("heading", { level: 2 });
await user.click(screen.getByRole("button", { name: "Uninstall…" }));
await user.click(screen.getByRole("button", { name: "Confirm uninstall Hello" }));
await screen.findByText("Private data is locked");
expect(notes.at(-1)).toMatchObject({
level: "error",
title: "Could not uninstall Hello",
body: "Private data is locked",
});
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("Hello");
await user.click(screen.getByRole("button", { name: "Confirm uninstall Hello" }));
await waitFor(() => expect(screen.getByRole("heading", { level: 2 })).not.toHaveTextContent("Hello"));
expect(remove).toHaveBeenCalledTimes(2);
expect(notes.at(-1)).toMatchObject({ title: "Uninstalled Hello" });
});
it("shows What's New from the change notes", async () => {
const user = userEvent.setup();
setPluginsBackend(