fix(plugins): isole les contributions plugin en erreur et durcit menus.ts

Le chargement de l'archive hello-plugin (build/hello-plugin-0.1.0.zip)
vidait la fenêtre principale : une contribution plugin fautive
remontait jusqu'au rendu global au lieu de rester locale à la cellule.
Ajoute un boundary local dans PluginLayoutCellView/PluginLayoutSelectorSection
et durcit menus.ts/loader.ts/registry.ts contre les entrées de menu ou
contributions malformées.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 09:08:14 +02:00
parent eec3e0a5bc
commit aa850374ce
9 changed files with 316 additions and 58 deletions

View File

@ -2,7 +2,7 @@
* F4 — `PluginLayoutCellView` (ticket #43, carnet §10 F4 acceptance criteria:
* "rendu composant mock, state roundtrip, fallback sans mutation du layout").
*/
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import type { CustomPluginLayoutCell } from "@/domain";
@ -39,7 +39,11 @@ function MockLayoutComponent(props: PluginLayoutProps) {
);
}
function stubPlugin(): LoadedPlugin {
function ThrowingLayoutComponent(_props: PluginLayoutProps): JSX.Element {
throw new Error("plugin layout render failed");
}
function stubPlugin(component = MockLayoutComponent): LoadedPlugin {
const contributes = {
menus: [],
menuItems: [],
@ -47,7 +51,7 @@ function stubPlugin(): LoadedPlugin {
mcpServers: [],
};
const layouts = new PluginLayoutRegistry("dev.acme.gitgraph", new Set(["dev.acme.gitgraph.layout"]));
layouts.register({ type: "dev.acme.gitgraph.layout", component: MockLayoutComponent });
layouts.register({ type: "dev.acme.gitgraph.layout", component });
return {
pluginId: "dev.acme.gitgraph",
displayName: "Git Graph",
@ -110,4 +114,18 @@ describe("PluginLayoutCellView", () => {
// The cell object itself is untouched — the domain identity/state survive.
expect(testCell).toEqual(cell());
});
it("isolates a plugin layout render failure to the plugin cell fallback", () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const registry = new PluginRuntimeRegistry();
registry.add(stubPlugin(ThrowingLayoutComponent));
renderCell(registry);
expect(screen.getByText("Layout indisponible")).toBeTruthy();
expect(screen.queryByTestId("state")).toBeNull();
} finally {
consoleError.mockRestore();
}
});
});