diff --git a/.ideai/tickets/157/carnet.md b/.ideai/tickets/157/carnet.md index be0e2e14..c2595f0e 100644 --- a/.ideai/tickets/157/carnet.md +++ b/.ideai/tickets/157/carnet.md @@ -1,11 +1,54 @@ --- issueRef: "#157" -version: 21 +version: 23 updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} -updatedAt: 1787556963342 +updatedAt: 1787557525852 --- # Journal d'itération (le plus récent en haut) +## Itération 6 — 2026-08-24 — Fix scoped par tour, QA verte, merge local sur `develop` + +### Cadrage confirmé + +- **UX**: chaque sollicitation ouvre un nouveau contexte visuel de tour; les cartes du tour 1 restent dans l'historique et le tour 2 doit créer une nouvelle activité distincte, même si le provider réutilise le même `progressId`. +- **Architect**: `progressId` n'est pas une identité visuelle globale de conversation. La borne correcte de dédoublonnage est le tour courant; clé minimale `(turnScope, progressId)`. Pas de réouverture backend nécessaire pour ce fix. +- **Git**: branche de cycle `fix/157-custom-cli-progressid-reuse` créée depuis `develop` @ `d9af4b5`. + +### Correctif implémenté + +Fix frontend ciblé dans `frontend/src/features/agents/CustomAgentChatView.tsx`: + +- la clé d'upsert de progress n'utilise plus `progressId` seul; +- elle est désormais scopée par tour via `progress:${progressScope}:${progressId}`; +- conséquence: un même `progressId` continue de fusionner correctement les mises à jour d'une activité **dans un même tour**, mais crée une nouvelle carte distincte s'il est réutilisé au tour suivant. + +Test garde-fou ajouté/validé dans `frontend/src/features/agents/CustomAgentChatView.test.tsx`: + +- `keeps second-turn progress visible when the provider reuses the same progressId across turns` + +### Validation QA réelle + +QA a rejoué indépendamment les commandes suivantes sur la branche de fix: + +- `cd /home/anthony/Documents/Projects/IdeA/frontend && npm test -- --run src/features/agents/CustomAgentChatView.test.tsx -t "keeps second-turn progress visible when the provider reuses the same progressId across turns"` → **1 passed** +- `cd /home/anthony/Documents/Projects/IdeA/frontend && npm test -- --run src/features/agents/CustomAgentChatView.test.tsx` → **63 passed** +- `cd /home/anthony/Documents/Projects/IdeA/frontend && npm run typecheck` → **vert** (`tsc --noEmit`) + +Verdict QA: **VERT** sur la repro initiale et sur la suite ciblée. Le bug repro d'écrasement du 1er tour par le 2e est résolu. + +### Intégration Git locale + +- Commit fix: `4db264e` +- Commit miroir tickets: `6b57add` +- Merge local vers `develop`: `592f248` (`merge --no-ff`) +- Aucun push. + +### Réserve restante + +- Le ticket reste en **QA** et n'est pas fermé à ce stade, car la réserve historique de validation live AppImage/UI bout-en-bout demeure. Cette itération clôt la **repro automatisée** demandée par l'utilisateur, mais pas encore la preuve live finale. + +--- + ## Itération 5 — 2026-08-24 — Réouverture ciblée sur régression 2e sollicitation / progressId réutilisé entre tours ### Nouveau signal utilisateur diff --git a/.ideai/tickets/157/issue.md b/.ideai/tickets/157/issue.md index 216437f9..69bfa1d3 100644 --- a/.ideai/tickets/157/issue.md +++ b/.ideai/tickets/157/issue.md @@ -2,7 +2,7 @@ id: "d49aaae5-0d03-43fe-a51c-2ab5e921045c" number: 157 title: "CLI custom: surface all available agent progress, MCP activity, and inter-agent delegation flow" -status: "inProgress" +status: "qa" priority: "high" sprint: null links: [{"target":"#156","kind":"dependsOn"}] @@ -11,7 +11,7 @@ attachments: [] createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} createdAt: 1785965383914 -updatedAt: 1787556963342 -version: 21 +updatedAt: 1787557525852 +version: 23 --- Régression/remontée utilisateur (profil OpenCode/GLM) sur la CLI custom : aucun message de progress ne s'affiche pendant le traitement de l'agent, et aucune bulle/carte d'appel de tool MCP n'apparaît quand l'agent invoque un outil MCP. Périmètre initial (surfacer progress + activité MCP + délégations inter-agent) à valider à nouveau sur le profil OpenCode. \ No newline at end of file diff --git a/.ideai/tickets/244/carnet.md b/.ideai/tickets/244/carnet.md index 57e53a90..bc6ac008 100644 --- a/.ideai/tickets/244/carnet.md +++ b/.ideai/tickets/244/carnet.md @@ -1,12 +1,14 @@ --- issueRef: "#244" -version: 6 +version: 7 updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} -updatedAt: 1787481685136 +updatedAt: 1787564256353 --- -2026-08-23 -- Reprise du ticket après demande utilisateur explicite: il faut corriger la cause applicative avant de se contenter de réaligner les templates. -- Travail déjà fait plus tôt: réalignement manuel des templates globaux sur les defaults projet. Ce réalignement ne suffit pas comme résolution si la cause racine de non-persistance subsiste. -- Nouvelle consigne de traitement: cycle correctif complet (Architect -> Git -> DevBackend -> QA -> Git), sans UX a priori car pas de surface touchée prouvée. -- Hypothèse de départ à vérifier: la persistance/backfill des permissions template ne se déclenche pas dans un des chemins runtime actuels, ou une mise à jour de template écrase/omet encore ces champs dans un cas précis. -- Prochaine étape: cadrage Architect sur la cause probable et le plus petit correctif sûr. \ No newline at end of file +2026-08-24 +- Repro QA confirmée sur le ticket #244. +- Scénario minimal: ouvrir Templates, éditer un template, cocher une permission MCP (ex. `idea_ticket_read`), sauvegarder, fermer, rouvrir: la case est de nouveau décochée. +- QA a démontré que le bug ne vient pas du pur UI local: le test frontend de réouverture avec `MockTemplateGateway` passe. +- QA a isolé une rouge backend de contrat: `update_template` ne désérialise pas actuellement la clé frontend `mcpToolPermissions`; le DTO backend n'accepte que `defaultMcpToolPermissions` / `defaultMcpToolPolicy`. +- Test rouge de référence: `cargo test -p app-tauri --test dto_templates update_template_request_does_not_currently_deserialise_frontend_mcp_tool_permissions_key -- --exact`. +- Conclusion de reprise: traiter ce ticket comme un correctif backend de contrat/persistance, puis faire revalider frontend+backend par QA. +- Prochaine étape: cadrage Architect, décision Git, implémentation DevBackend, revalidation QA, intégration Git. \ No newline at end of file diff --git a/.ideai/tickets/244/issue.md b/.ideai/tickets/244/issue.md index 5d87a2f0..f94e5488 100644 --- a/.ideai/tickets/244/issue.md +++ b/.ideai/tickets/244/issue.md @@ -11,7 +11,7 @@ attachments: [] createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} createdAt: 1787480706836 -updatedAt: 1787481685136 -version: 6 +updatedAt: 1787564256353 +version: 7 --- Les permissions MCP/system définies sur les templates ne semblent pas être sauvegardées correctement. Vérifier la persistance, puis remettre les permissions des templates existants pour qu'elles correspondent aux permissions effectives des agents du projet courant (Git, Architect, DevBackend, DevFrontend, QA, UX, Main, Context). \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 34e682be..867f9e66 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -2058,7 +2058,7 @@ "issueRef": "#157", "path": "157", "title": "CLI custom: surface all available agent progress, MCP activity, and inter-agent delegation flow", - "status": "inProgress", + "status": "qa", "priority": "high", "sprint": null, "assignedAgentIds": [ @@ -2068,7 +2068,7 @@ "kind": "agent", "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" }, - "updatedAt": 1787556963342 + "updatedAt": 1787557525852 }, { "issueRef": "#158", @@ -3293,7 +3293,7 @@ "kind": "agent", "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" }, - "updatedAt": 1787481685136 + "updatedAt": 1787564256353 }, { "issueRef": "#245", diff --git a/crates/app-tauri/tests/dto_templates.rs b/crates/app-tauri/tests/dto_templates.rs index 3e93b00d..7db93c3c 100644 --- a/crates/app-tauri/tests/dto_templates.rs +++ b/crates/app-tauri/tests/dto_templates.rs @@ -255,6 +255,24 @@ fn create_template_request_deserialises_template_defaults() { ); } +#[test] +fn create_template_request_deserialises_frontend_mcp_tool_permissions_key() { + let profile_id = Uuid::from_u128(22).to_string(); + let raw = json!({ + "name": "Backend Template", + "content": "# My context", + "defaultProfileId": profile_id, + "mcpToolPermissions": { "allowedTools": ["idea_ticket_read"] } + }); + let dto: CreateTemplateRequestDto = serde_json::from_value(raw).unwrap(); + assert_eq!( + dto.default_mcp_tool_policy, + Some(McpToolPolicy { + allowed_tools: vec!["idea_ticket_read".to_owned()] + }) + ); +} + #[test] fn update_template_request_deserialises_camelcase() { let template_id = Uuid::from_u128(30).to_string(); @@ -291,6 +309,22 @@ fn update_template_request_deserialises_metadata_and_nullable_clears() { assert_eq!(dto.default_mcp_tool_policy, Some(None)); } +#[test] +fn update_template_request_does_not_currently_deserialise_frontend_mcp_tool_permissions_key() { + let template_id = Uuid::from_u128(33).to_string(); + let raw = json!({ + "templateId": template_id, + "mcpToolPermissions": { "allowedTools": ["idea_ticket_read"] } + }); + let dto: UpdateTemplateRequestDto = serde_json::from_value(raw).unwrap(); + assert_eq!( + dto.default_mcp_tool_policy, + Some(Some(McpToolPolicy { + allowed_tools: vec!["idea_ticket_read".to_owned()] + })) + ); +} + #[test] fn create_agent_from_template_request_deserialises_camelcase() { let project_id = Uuid::from_u128(40).to_string(); diff --git a/crates/backend/src/dto.rs b/crates/backend/src/dto.rs index ffaebb88..f0f49250 100644 --- a/crates/backend/src/dto.rs +++ b/crates/backend/src/dto.rs @@ -4281,8 +4281,7 @@ impl From for TemplateDto { } /// Request DTO for `create_template`. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] pub struct CreateTemplateRequestDto { /// Display name. pub name: String, @@ -4291,20 +4290,54 @@ pub struct CreateTemplateRequestDto { /// Default runtime profile id for agents created from this template. pub default_profile_id: String, /// Optional default agent name for agents created from this template. - #[serde(default)] pub default_agent_name: Option, /// Optional agent-level system permission override for created agents. - #[serde(default)] pub default_system_permissions: Option, /// Optional agent-level MCP tool policy override for created agents. - #[serde( - default, - rename = "defaultMcpToolPermissions", - alias = "defaultMcpToolPolicy" - )] pub default_mcp_tool_policy: Option, } +impl<'de> Deserialize<'de> for CreateTemplateRequestDto { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut value = Value::deserialize(deserializer)?; + let object = value + .as_object_mut() + .ok_or_else(|| serde::de::Error::custom("expected create_template object"))?; + let name = take_required_string(object, "name").map_err(serde::de::Error::custom)?; + let content = take_required_string(object, "content").map_err(serde::de::Error::custom)?; + let default_profile_id = + take_required_string(object, "defaultProfileId").map_err(serde::de::Error::custom)?; + let default_agent_name = take_optional_value::(object, "defaultAgentName") + .map_err(serde::de::Error::custom)? + .flatten(); + let default_system_permissions = + take_optional_value::(object, "defaultSystemPermissions") + .map_err(serde::de::Error::custom)? + .flatten(); + let default_mcp_tool_policy = take_optional_value_preferred_aliases::( + object, + &[ + "mcpToolPermissions", + "defaultMcpToolPermissions", + "defaultMcpToolPolicy", + ], + ) + .map_err(serde::de::Error::custom)? + .flatten(); + Ok(Self { + name, + content, + default_profile_id, + default_agent_name, + default_system_permissions, + default_mcp_tool_policy, + }) + } +} + impl CreateTemplateRequestDto { /// Converts to the use-case input, parsing the profile id. /// @@ -4366,10 +4399,13 @@ impl<'de> Deserialize<'de> for UpdateTemplateRequestDto { let default_system_permissions = take_optional_value::(object, "defaultSystemPermissions") .map_err(serde::de::Error::custom)?; - let default_mcp_tool_policy = take_optional_value_alias::( + let default_mcp_tool_policy = take_optional_value_preferred_aliases::( object, - "defaultMcpToolPermissions", - "defaultMcpToolPolicy", + &[ + "mcpToolPermissions", + "defaultMcpToolPermissions", + "defaultMcpToolPolicy", + ], ) .map_err(serde::de::Error::custom)?; Ok(Self { @@ -4434,23 +4470,25 @@ where } } -fn take_optional_value_alias( +fn take_optional_value_preferred_aliases( object: &mut serde_json::Map, - primary_key: &str, - legacy_key: &str, + keys: &[&str], ) -> Result>, String> where T: DeserializeOwned, { - let primary = take_optional_value(object, primary_key)?; - let legacy = take_optional_value(object, legacy_key)?; - match (primary, legacy) { - (Some(value), None) | (None, Some(value)) => Ok(Some(value)), - (None, None) => Ok(None), - (Some(_), Some(_)) => Err(format!( - "{primary_key} and {legacy_key} cannot both be provided" - )), + for key in keys { + let parsed = take_optional_value(object, key)?; + if parsed.is_some() { + for ignored_key in keys { + if ignored_key != key { + object.remove(*ignored_key); + } + } + return Ok(parsed); + } } + Ok(None) } /// Parses a template-id string (UUID) coming from the frontend. diff --git a/frontend/src/features/templates/templates.test.tsx b/frontend/src/features/templates/templates.test.tsx index b35e5fc6..ea3cc561 100644 --- a/frontend/src/features/templates/templates.test.tsx +++ b/frontend/src/features/templates/templates.test.tsx @@ -239,6 +239,46 @@ describe("TemplatesPanel (with MockTemplateGateway)", () => { expect(templates[0].contentMd).toBe("# Updated content"); }); + it("reopening an edited template preserves saved MCP tool permissions in the editor", async () => { + const agent = new MockAgentGateway(); + const tmpl = new MockTemplateGateway(agent); + await tmpl.createTemplate({ + name: "Permissioned", + content: "# Initial", + defaultProfileId: "", + mcpToolPermissions: null, + }); + renderTemplatesPanel({ agent, template: tmpl }); + + await screen.findByText("Permissioned"); + + fireEvent.click(screen.getByRole("button", { name: "edit Permissioned" })); + await waitFor(() => { + expect(screen.getByLabelText("template MCP tool idea_ticket_read")).toBeTruthy(); + }); + + const checkbox = screen.getByLabelText( + "template MCP tool idea_ticket_read", + ) as HTMLInputElement; + expect(checkbox.checked).toBe(false); + fireEvent.click(checkbox); + expect(checkbox.checked).toBe(true); + fireEvent.click(screen.getByRole("button", { name: "Save template" })); + + await waitFor(() => { + expect(screen.queryByLabelText("template editor")).toBeNull(); + }); + + fireEvent.click(screen.getByRole("button", { name: "edit Permissioned" })); + await waitFor(() => { + expect(screen.getByLabelText("template MCP tool idea_ticket_read")).toBeTruthy(); + }); + + expect( + (screen.getByLabelText("template MCP tool idea_ticket_read") as HTMLInputElement).checked, + ).toBe(true); + }); + it("deleting a template removes it from the list", async () => { renderTemplatesPanel(); await createTemplate("ToDelete");