fix(domain,application,infrastructure): borne le namespace MCP plugin à 64 chars pour Codex/OpenAI — #333 (QA verte)

Cause racine: PluginMcpServerSpec::client_name() encodait chaque octet
non alphanumérique en _XX, produisant pour UnitySoundTool
`idea_plugin__com_2eidea_2eunity_2ddeveloper_2dtools__unity_2ddeveloper_2dtools_2etools`
(86 chars). Codex préfixe `mcp__` => 91 chars, limite API 64 => HTTP 400
`Invalid input.namespace string too long` sur chaque `codex exec resume`
dont l'historique contenait un outil Unity, aucun agent_message, UI en
fallback générique « réponse non finalisée ».

- domain: client_name() borné à 59 bytes ASCII (=> 64 avec mcp__) —
  stem lisible (minuscules, non-alnum => '_', tronqué) + suffixe
  déterministe = 16 premiers hex de SHA-256(plugin_id + NUL + server_id)
  pour conserver la résistance aux collisions après troncature; tests
  Unity (59 exact), stabilité, slug ponctuation, collisions sur stems
  tronqués identiques
- application: tests de projection TOML/JSON Codex et Claude adaptés au
  vrai namespace legacy encodé (86 chars) — vérifie que les tables
  utilisateur (mcp_servers.user_server, user-server) sont préservées
  pendant le retrait des tables managées périmées
- infrastructure: parse_event Codex traite turn.failed/error comme
  ReplyEvent::Error avec le message fournisseur (fini le fallback
  générique muet), classification lisible MCP_NAMESPACE_TOO_LONG: pour
  les erreurs de longueur de namespace; tests

Après déploiement: nouvelle conversation/restart requis pour les agents
Codex existants — les historiques contiennent déjà le namespace
invalide. Aucun push.
This commit is contained in:
2026-09-12 07:57:03 +02:00
parent 25506e160a
commit a94167ada0
5 changed files with 144 additions and 42 deletions

View File

@ -5581,9 +5581,9 @@ mod tests {
#[test]
fn codex_plugin_server_projection_removes_stale_managed_tables() {
let current = assigned_plugin_server();
let stale = "idea_plugin__stale__server";
let stale = "idea_plugin__com_2eidea_2eunity_2ddeveloper_2dtools__unity_2ddeveloper_2dtools_2etools";
let existing = format!(
"user_key = \"keep\"\n[mcp_servers.\"{stale}\"]\ncommand = \"old\"\n\n[preview]\nenabled = true\n"
"user_key = \"keep\"\n[mcp_servers.user_server]\ncommand = \"user\"\n\n[mcp_servers.\"{stale}\"]\ncommand = \"old\"\n\n[preview]\nenabled = true\n"
);
let declaration = codex_mcp_declaration(
domain::profile::McpTransport::Stdio,
@ -5593,6 +5593,7 @@ mod tests {
let rendered = codex_config_toml(Some(&existing), &declaration, "/run", "/project");
assert!(!rendered.contains(stale));
assert!(rendered.contains("user_key = \"keep\""));
assert!(rendered.contains("[mcp_servers.user_server]\ncommand = \"user\""));
assert!(rendered.contains("[preview]\nenabled = true"));
assert_eq!(rendered.matches(&current.client_name()).count(), 1);
}
@ -5600,10 +5601,12 @@ mod tests {
#[test]
fn internal_restart_refreshes_plugin_servers_without_replacing_real_idea_wiring() {
let current = assigned_plugin_server();
let stale = "idea_plugin__com_2eidea_2eunity_2ddeveloper_2dtools__unity_2ddeveloper_2dtools_2etools";
let existing_json = br#"{
"mcpServers": {
"idea": { "command": "/opt/Idea.AppImage", "args": ["mcp-server", "--endpoint", "/live.sock"] },
"idea_plugin__stale__server": { "command": "old" }
"user-server": { "command": "user" },
"idea_plugin__com_2eidea_2eunity_2ddeveloper_2dtools__unity_2ddeveloper_2dtools_2etools": { "command": "old" }
}
}"#;
let claude = reconcile_plugin_mcp_json(existing_json, std::slice::from_ref(&current))
@ -5613,18 +5616,18 @@ mod tests {
claude["mcpServers"]["idea"]["command"],
"/opt/Idea.AppImage"
);
assert!(claude["mcpServers"]
.get("idea_plugin__stale__server")
.is_none());
assert!(claude["mcpServers"].get(stale).is_none());
assert_eq!(claude["mcpServers"]["user-server"]["command"], "user");
assert_eq!(
claude["mcpServers"][current.client_name()]["command"],
current.command
);
let existing_toml = "[mcp_servers.idea]\ncommand = \"/opt/Idea.AppImage\"\nargs = [\"mcp-server\", \"--endpoint\", \"/live.sock\"]\n\n[mcp_servers.\"idea_plugin__stale__server\"]\ncommand = \"old\"\n";
let codex = merge_codex_plugin_mcp_tables(existing_toml, std::slice::from_ref(&current));
let existing_toml = format!("[mcp_servers.idea]\ncommand = \"/opt/Idea.AppImage\"\nargs = [\"mcp-server\", \"--endpoint\", \"/live.sock\"]\n\n[mcp_servers.user_server]\ncommand = \"user\"\n\n[mcp_servers.\"{stale}\"]\ncommand = \"old\"\n");
let codex = merge_codex_plugin_mcp_tables(&existing_toml, std::slice::from_ref(&current));
assert!(codex.contains("command = \"/opt/Idea.AppImage\""));
assert!(!codex.contains("idea_plugin__stale__server"));
assert!(codex.contains("[mcp_servers.user_server]\ncommand = \"user\""));
assert!(!codex.contains(stale));
assert!(codex.contains(&current.client_name()));
}