fix(ticket116): isoler crash plugin hello-plugin + erreur explicite UI

This commit is contained in:
2026-07-31 14:17:56 +02:00
parent e042ced724
commit d4e61a86f0
9 changed files with 417 additions and 64 deletions

View File

@ -3,7 +3,7 @@
* acceptance criteria: "ordre déterministe, disabledReason, plugin disabled
* absent, command handler appelé").
*/
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import type { PluginContributionDto } from "@/domain";
import { PluginRuntimeRegistry, type LoadedPlugin, type WhenContext } from "@/plugins/runtime";
@ -19,11 +19,15 @@ const NO_CONTEXT: WhenContext = {
};
function stubPlugin(pluginId: string, displayName: string, contributes: PluginContributionDto): LoadedPlugin {
const declaredCommandIds = contributes.menuItems.flatMap((item) => {
if (item === null || typeof item !== "object") return [];
return typeof item.command === "string" ? [item.command] : [];
});
return {
pluginId,
displayName,
contributes,
commands: new PluginCommandRegistry(pluginId, new Set(contributes.menuItems.map((i) => i.command))),
commands: new PluginCommandRegistry(pluginId, new Set(declaredCommandIds)),
layouts: new PluginLayoutRegistry(pluginId, new Set(contributes.layouts.map((l) => l.type))),
menu: new PluginMenuRegistry(pluginId),
dispose: async () => {},
@ -67,6 +71,7 @@ describe("resolveTopLevelMenus", () => {
...empty(),
menus: [
{ id: "bad.menu", label: "Good", topLevel: true },
null,
{ id: "bad.empty", label: "", topLevel: true },
{ id: undefined, label: "No id", topLevel: true },
{ id: "bad.no-label", label: undefined, topLevel: true },
@ -167,6 +172,7 @@ describe("resolveMenuItems", () => {
label: "Say Hello",
command: "hello-plugin.sayHello",
},
null,
{
id: "hello-plugin.broken.item",
targetMenuId: "hello-plugin.menu",
@ -199,4 +205,29 @@ describe("resolveMenuItems", () => {
await registry.runCommand("dev.acme", "dev.acme.a.cmd");
expect(ran).toBe(true);
});
it("confines a throwing command handler to the plugin command dispatch", async () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const registry = new PluginRuntimeRegistry();
const plugin = stubPlugin("dev.acme", "Acme", {
...empty(),
menuItems: [
{ id: "dev.acme.a", targetMenuId: "panels", label: "Open A", command: "dev.acme.a.cmd" },
],
});
plugin.commands.register("dev.acme.a.cmd", () => {
throw new Error("boom");
});
registry.add(plugin);
await expect(registry.runCommand("dev.acme", "dev.acme.a.cmd")).resolves.toBeUndefined();
expect(consoleError).toHaveBeenCalledWith(
'[plugin:dev.acme] command "dev.acme.a.cmd" failed',
expect.any(Error),
);
} finally {
consoleError.mockRestore();
}
});
});