Lot en un seul commit: les deux features partagent les mêmes contrats (domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes surfaces UI — la séparation par fichiers était impossible sans chirurgie de hunks risquant des états intermédiaires non compilables (convention lot des batches #310-#313 et #315). #325 — icône configurable par projet, watermark CLI custom: - domaine: ProjectIconSet (devicon, simple-icons, lucide, idea), ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon optionnel sur ProjectSettings; sérialisation camelCase et try_from strict - application: UpdateProjectSettings.execute accepte un patch theme_color OU icon (absent = inchangé, null = défaut produit), tests in-dependants avec store mémoire - backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto avec icon optionnel (désérialisation additive), commandes update_project_settings alignées Tauri/HTTP, tests DTO et HTTP (défaut null, set, clear, nom invalide rejeté) - frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en grille multi-lignes dans Paramètres projet, affichage de l'icône dans ProjectTabs et watermark grand format à faible opacité en fond de CLI custom; ports ProjectSettings.icon, Partial<ProjectSettings>, adapters tauri/mock/http et tests alignés #326 — localisation FR/EN et traduction des textes IdeA: - domaine: IdeaLanguage (fr | en), champ language optionnel sur IdeaUserSettings (absence = défaut produit, série additive validée) - backend/web-server: UserSettingsDto.language, update_user_settings accepte language, tests HTTP alignés; fixtures structured_launch_d3 mises à jour - frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression des sources visibles), GeneralSettings avec sélecteur de langue dans les paramètres, setupI18n dans vitest; migration de l'ensemble des textes hardcodés vers le catalogue sur toutes les surfaces (agents, layout, projets, tickets, sprints, web, plugins, permissions, git, templates, skills, mémoire, model servers, embedder, first-run, devices, scheduled tasks, workstate, dialogs), FR conservé par défaut; recherche d'icônes #325 en anglais conformément au ticket QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
209 lines
7.7 KiB
TypeScript
209 lines
7.7 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { DIProvider } from "@/app/di";
|
|
import {
|
|
createMockGateways,
|
|
MockPluginGateway,
|
|
} from "@/adapters/mock";
|
|
import type { ProjectPluginManifest } from "@/domain";
|
|
import { SkillsPanel } from "@/features/skills";
|
|
import { McpPanel } from "@/features/permissions";
|
|
import { ProjectPluginsPanel } from "./ProjectPluginsPanel";
|
|
|
|
function manifest(): ProjectPluginManifest {
|
|
return {
|
|
ideaPluginManifestVersion: 1,
|
|
id: "dev.acme.unity",
|
|
displayName: "Unity Plugin",
|
|
publisher: "Acme",
|
|
version: "1.2.3",
|
|
main: "dist/index.js",
|
|
trustLevel: "full",
|
|
capabilities: ["ui", "mcp"],
|
|
activationScope: "app",
|
|
contributes: {
|
|
skills: [{
|
|
id: "unity.debug",
|
|
name: "unity-debug",
|
|
description: "Diagnostiquer les builds Unity.",
|
|
kind: "workflow",
|
|
path: "skills/unity-debug/SKILL.md",
|
|
}],
|
|
menus: [],
|
|
menuItems: [],
|
|
layouts: [],
|
|
mcpServers: [
|
|
{
|
|
id: "unity-tools",
|
|
displayName: "Unity Tools",
|
|
transport: "stdio",
|
|
tools: [{ name: "unity_build", description: "Build the Unity project." }],
|
|
},
|
|
{ id: "legacy-tools", displayName: "Legacy Tools", transport: "stdio" },
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
function gatewaysWithPlugin() {
|
|
const plugin = new MockPluginGateway();
|
|
plugin._seedProjectPluginManifest(manifest());
|
|
return { ...createMockGateways(), plugin };
|
|
}
|
|
|
|
describe("project plugin settings #282", () => {
|
|
it("lists globally installed plugins, details their capabilities and toggles project activation", async () => {
|
|
const gateways = gatewaysWithPlugin();
|
|
render(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectPluginsPanel projectId="project-1" />
|
|
</DIProvider>,
|
|
);
|
|
|
|
expect(await screen.findByText("Unity Plugin")).toBeTruthy();
|
|
expect(screen.getByText("Installé globalement")).toBeTruthy();
|
|
fireEvent.click(screen.getByText("Voir les capacités"));
|
|
expect(screen.getByText("unity-debug")).toBeTruthy();
|
|
expect(screen.getByText("Unity Tools")).toBeTruthy();
|
|
|
|
const toggle = screen.getByRole("checkbox", { name: "Activé pour ce projet" });
|
|
fireEvent.click(toggle);
|
|
await waitFor(() => expect((toggle as HTMLInputElement).checked).toBe(true));
|
|
expect((await gateways.plugin.listProjectSettings("project-1")).settings.plugins).toEqual([
|
|
{ pluginId: "dev.acme.unity", enabled: true },
|
|
]);
|
|
});
|
|
|
|
it("offers an obvious entry point to assign plugin capabilities to agents", async () => {
|
|
const gateways = gatewaysWithPlugin();
|
|
const onAssignAgents = vi.fn();
|
|
render(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectPluginsPanel
|
|
projectId="project-1"
|
|
onAssignAgents={onAssignAgents}
|
|
/>
|
|
</DIProvider>,
|
|
);
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: "Gérer les skills" }));
|
|
expect(onAssignAgents).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("assigns plugin skills from the unified Skills panel and keeps tombstones removable", async () => {
|
|
const gateways = gatewaysWithPlugin();
|
|
const agent = await gateways.agent.createAgent("project-1", {
|
|
name: "Dev",
|
|
profileId: "profile-1",
|
|
});
|
|
gateways.plugin._seedProjectPluginSettings("project-1", {
|
|
version: 1,
|
|
plugins: [{ pluginId: "dev.acme.unity", enabled: true }],
|
|
agents: [{
|
|
agentId: agent.id,
|
|
skills: [{ pluginId: "missing.plugin", skillId: "removed-skill" }],
|
|
tools: [],
|
|
}],
|
|
});
|
|
render(
|
|
<DIProvider gateways={gateways}>
|
|
<SkillsPanel projectId="project-1" />
|
|
</DIProvider>,
|
|
);
|
|
|
|
const skill = await screen.findByRole("checkbox", { name: /unity-debug/ });
|
|
fireEvent.click(skill);
|
|
await waitFor(() => expect((skill as HTMLInputElement).checked).toBe(true));
|
|
expect(screen.getByText(/removed-skill/).closest("div")?.textContent).toContain(
|
|
"Indisponible",
|
|
);
|
|
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
|
|
const assigned = settings.agents.find((entry) => entry.agentId === agent.id);
|
|
expect(assigned?.skills).toContainEqual({ pluginId: "dev.acme.unity", skillId: "unity.debug" });
|
|
});
|
|
|
|
it("lists catalogued plugin tools in MCP and removes free-form assignment", async () => {
|
|
const gateways = gatewaysWithPlugin();
|
|
const agent = await gateways.agent.createAgent("project-1", {
|
|
name: "Main",
|
|
profileId: "profile-1",
|
|
});
|
|
gateways.plugin._seedProjectPluginSettings("project-1", {
|
|
version: 1,
|
|
plugins: [{ pluginId: "dev.acme.unity", enabled: true }],
|
|
agents: [{ agentId: agent.id, skills: [], tools: [] }],
|
|
});
|
|
|
|
render(
|
|
<DIProvider gateways={gateways}>
|
|
<McpPanel projectId="project-1" />
|
|
</DIProvider>,
|
|
);
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: /Main/ }));
|
|
const staticTool = await screen.findByRole("checkbox", { name: /unity_build/ });
|
|
fireEvent.click(staticTool);
|
|
await waitFor(() => expect((staticTool as HTMLInputElement).checked).toBe(true));
|
|
expect(screen.queryByPlaceholderText("Nom exact du tool")).toBeNull();
|
|
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
|
|
expect(settings.agents.find((entry) => entry.agentId === agent.id)?.tools).toContainEqual({
|
|
pluginId: "dev.acme.unity", serverId: "unity-tools", toolName: "unity_build",
|
|
});
|
|
});
|
|
|
|
it("keeps disabled and missing plugin assignments removable", async () => {
|
|
const gateways = gatewaysWithPlugin();
|
|
const agent = await gateways.agent.createAgent("project-1", {
|
|
name: "QA",
|
|
profileId: "profile-1",
|
|
});
|
|
gateways.plugin._seedProjectPluginSettings("project-1", {
|
|
version: 1,
|
|
plugins: [{ pluginId: "dev.acme.unity", enabled: false }],
|
|
agents: [{
|
|
agentId: agent.id,
|
|
skills: [{ pluginId: "missing.plugin", skillId: "gone-skill" }],
|
|
tools: [
|
|
{ pluginId: "dev.acme.unity", serverId: "unity-tools", toolName: "unity_build" },
|
|
{ pluginId: "dev.acme.unity", serverId: "legacy-tools", toolName: "legacy_ping" },
|
|
{ pluginId: "missing.plugin", serverId: "gone-server", toolName: "gone_tool" },
|
|
],
|
|
}],
|
|
});
|
|
|
|
render(
|
|
<DIProvider gateways={gateways}>
|
|
<McpPanel projectId="project-1" />
|
|
</DIProvider>,
|
|
);
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: /QA/ }));
|
|
const disabledAssigned = await screen.findByRole("checkbox", { name: /unity_build/ });
|
|
expect((disabledAssigned as HTMLInputElement).checked).toBe(true);
|
|
expect((disabledAssigned as HTMLInputElement).disabled).toBe(false);
|
|
fireEvent.click(disabledAssigned);
|
|
await waitFor(() => expect((disabledAssigned as HTMLInputElement).checked).toBe(false));
|
|
|
|
const runtimeOnly = screen.getByText(/legacy_ping/).closest("div")!;
|
|
expect(runtimeOnly.textContent).toContain("Indisponible");
|
|
expect(screen.queryByRole("checkbox", { name: /legacy_ping/ })).toBeNull();
|
|
fireEvent.click(runtimeOnly.querySelector("button")!);
|
|
await waitFor(async () => {
|
|
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
|
|
expect(settings.agents[0]?.tools).not.toContainEqual(
|
|
expect.objectContaining({ toolName: "legacy_ping" }),
|
|
);
|
|
});
|
|
|
|
const stale = screen.getByText(/gone_tool/).closest("div")!;
|
|
fireEvent.click(stale.querySelector("button")!);
|
|
await waitFor(async () => {
|
|
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
|
|
expect(settings.agents[0]?.tools).not.toContainEqual(
|
|
expect.objectContaining({ toolName: "gone_tool" }),
|
|
);
|
|
});
|
|
});
|
|
});
|