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

@ -216,7 +216,20 @@ async fn first_run_true_when_not_configured_with_reference_catalogue() {
let out = uc.execute().await.unwrap();
assert!(out.is_first_run);
assert_eq!(out.reference_profiles.len(), 4, "catalogue seeded");
// §17.3/D7: the wizard is seeded only with the *selectable* (structured-
// drivable) profiles — Claude + Codex — not the full 4-profile catalogue.
assert_eq!(out.reference_profiles.len(), 2, "selectable catalogue seeded");
let commands: Vec<&str> = out
.reference_profiles
.iter()
.map(|p| p.command.as_str())
.collect();
assert_eq!(commands, vec!["claude", "codex"], "only Claude/Codex offered");
// Every seeded profile is selectable (the gate the menu relies on).
assert!(
out.reference_profiles.iter().all(AgentProfile::is_selectable),
"seeded profiles must all be selectable"
);
}
#[tokio::test]
@ -274,9 +287,53 @@ async fn delete_unknown_is_not_found_error() {
// ---------------------------------------------------------------------------
#[tokio::test]
async fn reference_profiles_use_case_returns_four() {
async fn reference_profiles_use_case_returns_only_selectable() {
// §17.3/D7: the selection use case exposes only structured-drivable profiles
// (Claude + Codex). Gemini/Aider stay in the raw catalogue (see
// `catalogue_*` tests below) but are not offered to selection/creation.
let out = ReferenceProfiles::new().execute().await.unwrap();
assert_eq!(out.profiles.len(), 4);
assert_eq!(out.profiles.len(), 2);
let commands: Vec<&str> = out.profiles.iter().map(|p| p.command.as_str()).collect();
assert_eq!(commands, vec!["claude", "codex"]);
assert!(out.profiles.iter().all(AgentProfile::is_selectable));
}
/// §17.3/D7 — non-regression: the *raw* catalogue still carries the four profiles
/// (data intact). Only the selection-facing use case is filtered.
#[test]
fn raw_catalogue_still_has_all_four_profiles() {
let commands: Vec<String> = reference_profiles()
.iter()
.map(|p| p.command.clone())
.collect();
assert_eq!(commands, vec!["claude", "codex", "gemini", "aider"]);
}
/// §17.3/D7 — `is_selectable` is the single selection predicate: true for the two
/// structured-drivable profiles, false for the two PTY-only ones. This assertion
/// would flip (and fail) the moment Gemini/Aider gained an adapter or Claude/Codex
/// lost theirs — i.e. it actually constrains behaviour.
#[test]
fn is_selectable_is_true_only_for_claude_and_codex() {
let profiles = reference_profiles();
let by_command: HashMap<&str, &AgentProfile> =
profiles.iter().map(|p| (p.command.as_str(), p)).collect();
assert!(
by_command["claude"].is_selectable(),
"Claude carries a structured adapter ⇒ selectable"
);
assert!(
by_command["codex"].is_selectable(),
"Codex carries a structured adapter ⇒ selectable"
);
assert!(
!by_command["gemini"].is_selectable(),
"Gemini has no adapter ⇒ not selectable"
);
assert!(
!by_command["aider"].is_selectable(),
"Aider has no adapter ⇒ not selectable"
);
}
#[test]