Requalification Architect du 2026-08-01: le crash INTERFACE INTERROMPUE au chargement d'un plugin (menu + item) remonte via ProjectsView -> usePluginMenus -> MenuBar jusqu'à RootErrorBoundary, quel que soit le plugin (ancien ou reconstruit via SDK). - usePluginMenus isole la résolution/conversion des contributions plugin : toute erreur retombe sur [] au lieu de propager. - ProjectsView sépare les menus natifs des menus enrichis par plugin et rend MenuBar derrière une error boundary locale (fallback menus natifs seuls). - hello-plugin (SDK) et les tests d'installation associés durcis en cohérence. Bookkeeping ticket #120 uniquement (issue.md, carnet.md) ; les fichiers counter.json/index.json et le dossier tickets/122/ restent hors commit car une collision de numérotation #122 existe entre cette base et feature/ticket120-hello-plugin-install-path-audit (deux tickets différents revendiquent #122) — à arbitrer avant de committer le bookkeeping global. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
import { renderHook } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import type { MenuTargetId } from "@/domain";
|
|
import type { PluginRuntimeRegistry, WhenContext } from "@/plugins/runtime";
|
|
import { usePluginMenus } from "./usePluginMenus";
|
|
|
|
const NO_CONTEXT: WhenContext = {
|
|
projectOpen: false,
|
|
gitRepository: false,
|
|
agentSelected: false,
|
|
terminalFocused: false,
|
|
layoutCellFocused: false,
|
|
};
|
|
|
|
function throwingRegistry(overrides: Record<string, unknown>): PluginRuntimeRegistry {
|
|
return {
|
|
runCommand: vi.fn(async () => {}),
|
|
topLevelMenus: vi.fn(() => []),
|
|
menuItems: vi.fn(() => []),
|
|
...overrides,
|
|
} as unknown as PluginRuntimeRegistry;
|
|
}
|
|
|
|
describe("usePluginMenus", () => {
|
|
it("falls back to no plugin top-level menus when plugin menu resolution throws", () => {
|
|
const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
try {
|
|
const registry = throwingRegistry({
|
|
topLevelMenus: vi.fn(() => {
|
|
throw new Error("broken top-level contribution");
|
|
}),
|
|
});
|
|
|
|
const { result } = renderHook(() => usePluginMenus(registry, NO_CONTEXT));
|
|
|
|
expect(result.current.topLevelMenus).toEqual([]);
|
|
expect(consoleWarn).toHaveBeenCalledWith(
|
|
"[plugins] menu contribution rejected",
|
|
expect.objectContaining({
|
|
scope: "top-level-menus",
|
|
reason: "broken top-level contribution",
|
|
}),
|
|
);
|
|
} finally {
|
|
consoleWarn.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("falls back to no plugin items for a native menu when item resolution throws", () => {
|
|
const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
try {
|
|
const registry = throwingRegistry({
|
|
menuItems: vi.fn(() => {
|
|
throw new Error("broken item contribution");
|
|
}),
|
|
});
|
|
|
|
const { result } = renderHook(() => usePluginMenus(registry, NO_CONTEXT));
|
|
const items = result.current.itemsFor("panels" as MenuTargetId);
|
|
|
|
expect(items).toEqual([]);
|
|
expect(consoleWarn).toHaveBeenCalledWith(
|
|
"[plugins] menu contribution rejected",
|
|
expect.objectContaining({
|
|
scope: "items",
|
|
targetMenuId: "panels",
|
|
reason: "broken item contribution",
|
|
}),
|
|
);
|
|
} finally {
|
|
consoleWarn.mockRestore();
|
|
}
|
|
});
|
|
});
|