feat(session): adapter HTTP OpenAI-compatible pour profils locaux/LAN (#14)

Ajoute un adapter de session HTTP OpenAI-compatible, purement additif,
permettant d'intégrer des modèles locaux/LAN comme profils IdeA canoniques
avec parité tool-calling/MCP.

- domain: extension du profil et des ports pour l'adapter OpenAI-compatible
- infrastructure: adapter openai_compat + routage factory
- app-tauri: mapping des outils OpenAI (openai_tools) + wiring state/lib
- application: catalogue d'agents + tests de use-cases profils

Validé QA (backend GO): round-trip byte-identique Claude/Codex, mapping
erreurs, dégradation tools, conversation_id None, conformance un seul Final,
routage factory. Suites vertes domain 467 / application 530 /
infrastructure 523 / app-tauri 247, 0 échec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:09:16 +02:00
parent cb20fabdf4
commit aab4bcafb6
14 changed files with 1794 additions and 25 deletions

View File

@ -217,10 +217,11 @@ async fn first_run_true_when_not_configured_with_reference_catalogue() {
let out = uc.execute().await.unwrap();
assert!(out.is_first_run);
// §17.3/D7: the wizard is seeded only with the *selectable* (structured-
// drivable) profiles — Claude + Codex — not the full 4-profile catalogue.
// drivable) profiles — Claude + Codex + OpenAI-compatible — not the full
// catalogue.
assert_eq!(
out.reference_profiles.len(),
2,
3,
"selectable catalogue seeded"
);
let commands: Vec<&str> = out
@ -230,8 +231,8 @@ async fn first_run_true_when_not_configured_with_reference_catalogue() {
.collect();
assert_eq!(
commands,
vec!["claude", "codex"],
"only Claude/Codex offered"
vec!["claude", "codex", "openai-compatible"],
"only structured profiles offered"
);
// Every seeded profile is selectable (the gate the menu relies on).
assert!(
@ -299,32 +300,57 @@ async fn delete_unknown_is_not_found_error() {
#[tokio::test]
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
// (Claude + Codex + OpenAI-compatible). 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(), 2);
assert_eq!(out.profiles.len(), 3);
let commands: Vec<&str> = out.profiles.iter().map(|p| p.command.as_str()).collect();
assert_eq!(commands, vec!["claude", "codex"]);
assert_eq!(commands, vec!["claude", "codex", "openai-compatible"]);
assert!(out.profiles.iter().all(AgentProfile::is_selectable));
}
/// §17.3/D7 — non-regression: the *raw* catalogue still carries the four profiles
/// §17.3/D7 — non-regression: the *raw* catalogue still carries all reference profiles
/// (data intact). Only the selection-facing use case is filtered.
#[test]
fn raw_catalogue_still_has_all_four_profiles() {
fn raw_catalogue_still_has_all_profiles() {
let commands: Vec<String> = reference_profiles()
.iter()
.map(|p| p.command.clone())
.collect();
assert_eq!(commands, vec!["claude", "codex", "gemini", "aider"]);
assert_eq!(
commands,
vec!["claude", "codex", "openai-compatible", "gemini", "aider"]
);
}
/// §17.3/D7 — `is_selectable` is the single selection predicate: true for the two
#[test]
fn claude_and_codex_reference_profiles_roundtrip_with_byte_identity() {
let profiles = reference_profiles();
let by_command: HashMap<&str, &AgentProfile> =
profiles.iter().map(|p| (p.command.as_str(), p)).collect();
for command in ["claude", "codex"] {
let before = serde_json::to_vec(by_command[command]).expect("serialize reference profile");
let back: AgentProfile =
serde_json::from_slice(&before).expect("deserialize reference profile");
let after = serde_json::to_vec(&back).expect("serialize round-tripped profile");
assert_eq!(
after, before,
"{command} profile JSON bytes must be strictly stable across a serde round-trip"
);
assert!(
!String::from_utf8_lossy(&after).contains("\"chatHttp\""),
"{command} is a historical non-HTTP profile and must not grow chatHttp"
);
}
}
/// §17.3/D7 — `is_selectable` is the single selection predicate: true for the
/// 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() {
fn is_selectable_is_true_only_for_structured_profiles() {
let profiles = reference_profiles();
let by_command: HashMap<&str, &AgentProfile> =
profiles.iter().map(|p| (p.command.as_str(), p)).collect();
@ -336,6 +362,10 @@ fn is_selectable_is_true_only_for_claude_and_codex() {
by_command["codex"].is_selectable(),
"Codex carries a structured adapter ⇒ selectable"
);
assert!(
by_command["openai-compatible"].is_selectable(),
"OpenAI-compatible carries a structured adapter ⇒ selectable"
);
assert!(
!by_command["gemini"].is_selectable(),
"Gemini has no adapter ⇒ not selectable"