diff --git a/.ideai/tickets/120/carnet.md b/.ideai/tickets/120/carnet.md index 588f887..e55aaac 100644 --- a/.ideai/tickets/120/carnet.md +++ b/.ideai/tickets/120/carnet.md @@ -1,12 +1,12 @@ --- issueRef: "#120" +version: 9 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1785600025153 +--- +--- +issueRef: "#120" version: 8 -updatedBy: {"kind":"agent","agent_id":"dce19c75-9669-4e45-b8de-9950025157da"} -updatedAt: 1785596006134 ---- ---- -issueRef: "#120" -version: 7 updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} updatedAt: 1785595113109 --- @@ -188,3 +188,77 @@ Le commit du correctif (`5a30ec8`, isolation `usePluginMenus`/`ProjectsView`) da - Ne pas rouvrir de chantier de code avant d'avoir confirme que l'utilisateur reproduit sur le binaire effectivement a jour. - Remplacement de binaire + retest = action Git/deploiement, a executer avant toute nouvelle investigation frontend. + +## Requalification Architect du 2026-08-01 (nouveau symptome CORS apres installation) + +### Nouveau fait utilisateur + +Le symptome visible n'est plus seulement un ecran noir: dans la vue Plugins, IdeA affiche apres installation de `hello-plugin` un bandeau explicite: +- `Certains plugins installes n'ont pas pu etre charges.` +- `com.example.hello-plugin : Cross-origin script load denied by Cross-Origin Resource Sharing policy.` + +Cette observation invalide la piste `LayoutTabs -> PluginLayoutSelectorSection` comme cause racine de ce symptome precis. Le durcissement frontend precedent reste utile: il a empeche le black screen et laisse remonter l'erreur exploitable. + +### Cause racine retenue + +- Le protocole custom `idea-plugin://` repondait sans headers CORS dans `crates/app-tauri/src/plugins.rs` (`plugin_asset_response`). +- Le frontend charge le bundle plugin via `import()` dynamique depuis `frontend/src/plugins/runtime/loader.ts`; ce chargement est un fetch CORS. +- L'origine du document (`tauri://localhost` / `http://tauri.localhost` en prod, `http://localhost:5173` en dev) est distincte de `idea-plugin://...`. +- Faute de `Access-Control-Allow-Origin`, WebKitGTK bloque le module avec exactement le message observe. + +### Couche proprietaire et perimetre de correction + +- Couche proprietaire: backend/infrastructure `app-tauri`, pas frontend produit, pas packaging plugin. +- Correctif attendu: + - ajouter `Access-Control-Allow-Origin` sur les reponses du protocole plugin + - ajouter `Access-Control-Allow-Methods: GET` + - conserver intact le confinement `asset_allowed` + - ajouter un test backend verrouillant ces headers sur `plugin_asset_response` + +## Livraison DevBackend du 2026-08-01 + +- Branche de travail dediee: `feature/ticket120-plugin-asset-cors-headers` +- Correctif implemente dans `crates/app-tauri/src/plugins.rs` +- Headers ajoutes sur les reponses du protocole `idea-plugin://`: + - `Access-Control-Allow-Origin: *` + - `Access-Control-Allow-Methods: GET` +- Factorisation via un builder de reponse commun aux chemins succes/erreur du protocole +- Test ajoute: `plugin_asset_response_includes_cors_headers_for_dynamic_import` + +### Commandes executees par DevBackend +- `cargo fmt -p app-tauri` : OK +- `cargo test -p app-tauri plugin_asset_response_includes_cors_headers_for_dynamic_import` : OK +- `cargo test -p app-tauri plugins::tests` : OK +- `cargo test -p app-tauri` : OK (242 passed, 0 failed, 5 ignored) + +## Validation QA du 2026-08-01 (fix CORS) + +### Verdict +- PASS avec reserve explicite. + +### Validation reelle obtenue +- `cargo test -p app-tauri plugin_asset_response_includes_cors_headers_for_dynamic_import -- --nocapture` : OK +- `cargo test -p app-tauri` : OK +- `cargo test -p infrastructure --test plugin_install_load installs_sdk_hello_plugin_and_loads_runtime_catalog -- --nocapture` : OK +- `cargo test -p infrastructure --test plugin_install_load installs_reference_fixture_and_loads_runtime_catalog -- --nocapture` : OK +- `npm --prefix /home/anthony/Documents/Projects/IdeA/sdk/IdeaSDK run package:hello-plugin` : OK +- `cd /home/anthony/Documents/Projects/IdeA/frontend && npx vitest run src/plugins/runtime/loader.test.ts src/features/plugins/plugins.test.tsx src/features/plugins/menus.test.ts` : OK (27 tests) + +### Reserve QA restante +- Pas de preuve visuelle AppImage/UI de bout en bout dans cet environnement. +- Tentative de build AppImage realisee, mais l'artefact final n'etait pas disponible ensuite dans `target/release/bundle/appimage/`. +- Tentative d'execution d'une AppImage existante bloquee par l'environnement (`No suitable fusermount binary found on the $PATH`). +- Risque residuel exact: le fix est prouve au niveau handler backend, catalogue runtime et chargeur frontend, mais pas sur l'enchainement visuel complet `archive installee -> redemarrage UI reel -> absence du bandeau CORS`. + +## Decision Git du 2026-08-01 + +- Commit local realise sur la branche dediee: `fbe69de` +- Merge local `--no-ff` dans `develop`: `fd2ab4a` +- Motivation: QA a rendu un verdict PASS; la reserve porte sur une limite d'environnement de preuve AppImage/FUSE, pas sur un test rouge. +- La branche `feature/ticket120-plugin-asset-cors-headers` a ete supprimee apres merge. + +## Etat reel a la fin de cette relance + +- Le correctif CORS est livre dans `develop`. +- Le ticket **reste a considerer ouvert fonctionnellement** tant qu'une validation visuelle reelle sur AppImage/redemarrage n'a pas confirme la disparition du bandeau `Cross-origin script load denied by Cross-Origin Resource Sharing policy` et l'activation effective des contributions `hello-plugin`. +- Prochaine preuve attendue hors environnement QA courant: lancer le binaire AppImage reellement utilise par l'utilisateur, installer l'archive `hello-plugin`, redemarrer IdeA, verifier visuellement l'absence du bandeau CORS et la presence des contributions plugin actives. diff --git a/.ideai/tickets/120/issue.md b/.ideai/tickets/120/issue.md index 08b7818..68d1b66 100644 --- a/.ideai/tickets/120/issue.md +++ b/.ideai/tickets/120/issue.md @@ -9,10 +9,10 @@ links: [{"target":"#116","kind":"relatesTo"},{"target":"#43","kind":"relatesTo"} agentRefs: [] attachments: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedBy: {"kind":"agent","agent_id":"dce19c75-9669-4e45-b8de-9950025157da"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} createdAt: 1785534496259 -updatedAt: 1785596006134 -version: 8 +updatedAt: 1785600025153 +version: 9 --- ## Constat utilisateur diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index edba004..c339793 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -1564,7 +1564,7 @@ "kind": "agent", "agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641" }, - "updatedAt": 1785596006134 + "updatedAt": 1785600025153 }, { "issueRef": "#121", diff --git a/frontend/src/features/plugins/plugins.test.tsx b/frontend/src/features/plugins/plugins.test.tsx index ccde192..1c12f31 100644 --- a/frontend/src/features/plugins/plugins.test.tsx +++ b/frontend/src/features/plugins/plugins.test.tsx @@ -7,8 +7,8 @@ import { describe, it, expect } from "vitest"; import { render, screen, waitFor, fireEvent, within } from "@testing-library/react"; import { MockPluginGateway, MockSystemGateway } from "@/adapters/mock"; -import type { PluginInstallResult, PluginRuntimeContributionCatalog } from "@/domain"; -import type { Gateways } from "@/ports"; +import type { PluginInstallResult, PluginReview, PluginRuntimeContributionCatalog } from "@/domain"; +import type { Gateways, ReviewPluginPackageInput } from "@/ports"; import { DIProvider } from "@/app/di"; import { PluginRuntimeRegistry } from "@/plugins/runtime"; import { PluginsPanel } from "./PluginsPanel"; @@ -77,6 +77,25 @@ class FailingRuntimeCatalogPluginGateway extends MockPluginGateway { } } +class BackendShapedReviewPluginGateway extends MockPluginGateway { + async reviewPackage(input: ReviewPluginPackageInput): Promise { + const label = input.path.split("/").pop() ?? input.path; + return { + id: "dev.acme.gitgraph", + displayName: label.replace(/\.(ideaplug|zip|vsix)$/i, ""), + publisher: "Acme", + version: "1.2.3", + description: "Graph", + sourceKind: input.sourceKind, + sourceLabel: input.path, + contentHash: "abc", + trustLevel: "full", + contributionSummary: { topLevelMenus: 1, menuItems: 2, layouts: 3, mcpServers: 4 }, + contributes: { menus: [], menuItems: [], layouts: [], mcpServers: [] }, + } as unknown as PluginReview; + } +} + describe("PluginsPanel", () => { it("shows an empty state with no plugins installed", async () => { renderPanel(); @@ -122,6 +141,30 @@ describe("PluginsPanel", () => { expect(screen.getByText("Activé")).toBeTruthy(); }); + it("keeps install, uninstall and reinstall stable with backend-shaped reviews", async () => { + renderPanel(new BackendShapedReviewPluginGateway()); + await screen.findByText("Aucun plugin installé."); + + fireEvent.click(screen.getByRole("button", { name: "Installer depuis une archive…" })); + let dialog = await screen.findByRole("dialog"); + expect(within(dialog).getByText(/full-trust/)).toBeTruthy(); + fireEvent.click(within(dialog).getByRole("button", { name: "Installer" })); + expect(await screen.findByText("mock-plugin")).toBeTruthy(); + + fireEvent.click(screen.getByRole("button", { name: "Désinstaller" })); + dialog = await screen.findByRole("dialog"); + fireEvent.click(within(dialog).getByRole("button", { name: "Désinstaller" })); + expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy(); + + fireEvent.click(screen.getByRole("button", { name: "Installer depuis une archive…" })); + dialog = await screen.findByRole("dialog"); + expect(within(dialog).getByRole("button", { name: "Installer" })).toBeTruthy(); + fireEvent.click(within(dialog).getByRole("button", { name: "Installer" })); + + expect(await screen.findByText("mock-plugin")).toBeTruthy(); + expect(screen.queryByText("INTERFACE INTERROMPUE")).toBeNull(); + }); + it("renders an invalid plugin returned after install as isolated, without replacing the panel", async () => { renderPanel(new InvalidInstallPluginGateway()); await screen.findByText("Aucun plugin installé.");