fix(runtime): detection non bloquante, bornée dans le temps, HTTP pour les profils locaux (#28)

Le first-run wizard restait figé : `CliAgentRuntime::detect` construisait un
runtime tokio courant-thread via `futures_block_on` pour piloter un
`ProcessSpawner` async. Appelé depuis le runtime async de Tauri, ce
`block_on` imbriqué panique ; la commande IPC ne répond alors jamais, la
promesse `detectProfiles` reste pendante et `busy` ne redescend plus.

Ce commit s'attaque à la cause côté backend :

- `AgentRuntime::detect` devient `async fn` (`#[async_trait]`) : le port cesse
  de mentir sur sa nature. Il pilote un spawner async, il est async. Le
  `futures_block_on` disparaît, et avec lui le runtime imbriqué.
- La sonde CLI est bornée par `tokio::time::timeout(DETECTION_TIMEOUT)` :
  un binaire qui ne rend jamais la main dégrade la détection en `Err`, il
  ne gèle plus l'appelant.
- Les profils `StructuredAdapter::OpenAiCompatible` n'ont pas de CLI à
  spawner : les sonder revenait à tester un binaire inexistant. Ils sont
  désormais sondés par un GET HTTP sur l'endpoint `/models` dérivé de
  `chat_http.endpoint`, borné par le même timeout.

`DetectProfiles` séquence les `await` sur les candidats. Les fakes
`AgentRuntime` des crates application/infrastructure/app-tauri suivent la
nouvelle signature.

Tests: cargo test -p domain (241), -p application (81), -p infrastructure
(269), -p app-tauri (63+15) — verts. `rg futures_block_on crates` sans match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 08:11:14 +02:00
parent 3c6cd04bd8
commit 3abf2fce98
12 changed files with 143 additions and 52 deletions

View File

@ -72,14 +72,11 @@ impl DetectProfiles {
&self,
input: DetectProfilesInput,
) -> Result<DetectProfilesOutput, AppError> {
let results = input
.candidates
.into_iter()
.map(|profile| {
let available = self.runtime.detect(&profile).unwrap_or(false);
ProfileAvailability { profile, available }
})
.collect();
let mut results = Vec::with_capacity(input.candidates.len());
for profile in input.candidates {
let available = self.runtime.detect(&profile).await.unwrap_or(false);
results.push(ProfileAvailability { profile, available });
}
Ok(DetectProfilesOutput { results })
}
}