feat(agent): menu de profils restreint Claude/Codex + retrait custom (D7) — §17

Dernier lot de §17 : seuls les profils pilotables en mode structuré sont
proposés à la sélection/création.

- domain : AgentProfile::is_selectable() = structured_adapter.is_some(),
  source unique de vérité du prédicat.
- infrastructure : AgentSessionFactory::supports délègue à is_selectable
  (supports et is_selectable ne peuvent plus diverger).
- application : selectable_reference_profiles() = reference_profiles()
  filtré ; ReferenceProfiles et FirstRunState exposent la liste filtrée
  (Claude/Codex). reference_profiles() brut reste à 4 (data intacte) ⇒
  un agent Gemini/Aider/custom legacy déjà configuré continue de tourner.
- frontend : bloc AddCustomProfile retiré du wizard first-run, action
  addCustom retirée du viewmodel ; wizard n'affiche que la liste filtrée.

Tests (QA) : is_selectable (table de vérité), cohérence stricte
is_selectable<->supports, liste exposée=2 / data brute=4, garde
anti-régression Vitest sur l'absence du bloc custom — validées par
mutation. cargo test --workspace : 820 passed. npx vitest run : 344 passed.

§17 COMPLET (D0->D7). Suivi restant : D6b (surfacer reply dans le writer
wire .response.json pour la délégation par protocole fichier).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:57:51 +02:00
parent dd1194abe8
commit 6de4e5a6e0
15 changed files with 324 additions and 245 deletions

View File

@ -50,7 +50,10 @@ fn seed_conversation_id(session: &SessionPlan) -> Option<String> {
#[async_trait]
impl AgentSessionFactory for StructuredSessionFactory {
fn supports(&self, profile: &AgentProfile) -> bool {
profile.structured_adapter.is_some()
// Source unique de vérité (§17.3, D7) : sélectionnabilité = pilotable en
// structuré. `is_selectable` et `supports` partagent ainsi le **même**
// prédicat de domaine, ils ne peuvent pas diverger.
profile.is_selectable()
}
async fn start(

View File

@ -352,6 +352,43 @@ mod tests {
assert!(!factory.supports(&tui));
}
/// §17.3/D7 — **strict coherence**: `AgentProfile::is_selectable` (the menu's
/// selection gate) and `StructuredSessionFactory::supports` (the runtime's
/// routing gate) must agree on **every** reference profile. If they ever
/// diverged, the wizard could offer a profile the runtime cannot drive (or
/// hide one it can). Asserting profile-by-profile over the real catalogue —
/// including the expected `claude=codex=true`, `gemini=aider=false` truth
/// table — means this fails the moment either gate changes without the other.
#[tokio::test]
async fn supports_and_is_selectable_agree_on_every_reference_profile() {
use std::collections::HashMap;
let factory = StructuredSessionFactory::new();
let profiles = application::reference_profiles();
let mut expected: HashMap<&str, bool> = HashMap::new();
expected.insert("claude", true);
expected.insert("codex", true);
expected.insert("gemini", false);
expected.insert("aider", false);
assert_eq!(profiles.len(), 4, "catalogue has the four reference profiles");
for profile in &profiles {
let selectable = profile.is_selectable();
assert_eq!(
selectable,
factory.supports(profile),
"is_selectable and supports must agree for `{}`",
profile.command
);
assert_eq!(
Some(&selectable),
expected.get(profile.command.as_str()),
"unexpected selectability for `{}`",
profile.command
);
}
}
#[tokio::test]
async fn factory_routes_claude_and_codex() {
let factory = StructuredSessionFactory::new();