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:
@ -318,8 +318,9 @@ impl FakeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AgentRuntime for FakeRuntime {
|
||||
fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
async fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
Ok(true)
|
||||
}
|
||||
fn prepare_invocation(
|
||||
|
||||
@ -344,8 +344,9 @@ impl FakeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AgentRuntime for FakeRuntime {
|
||||
fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
async fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
Ok(true)
|
||||
}
|
||||
fn prepare_invocation(
|
||||
|
||||
@ -262,8 +262,9 @@ impl SkillStore for RecordingSkills {
|
||||
}
|
||||
|
||||
struct FakeRuntime;
|
||||
#[async_trait]
|
||||
impl AgentRuntime for FakeRuntime {
|
||||
fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
async fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
Ok(true)
|
||||
}
|
||||
fn prepare_invocation(
|
||||
|
||||
@ -86,9 +86,11 @@ struct StubRuntime {
|
||||
by_command: HashMap<String, DetectResult>,
|
||||
}
|
||||
|
||||
struct YieldingRuntime;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentRuntime for StubRuntime {
|
||||
fn detect(&self, profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
async fn detect(&self, profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
match self.by_command.get(&profile.command) {
|
||||
Some(DetectResult::Available) => Ok(true),
|
||||
Some(DetectResult::Missing) | None => Ok(false),
|
||||
@ -107,6 +109,24 @@ impl AgentRuntime for StubRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AgentRuntime for YieldingRuntime {
|
||||
async fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
tokio::task::yield_now().await;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn prepare_invocation(
|
||||
&self,
|
||||
_profile: &AgentProfile,
|
||||
_ctx: &PreparedContext,
|
||||
_cwd: &ProjectPath,
|
||||
_session: &SessionPlan,
|
||||
) -> Result<SpawnSpec, RuntimeError> {
|
||||
unreachable!("not used in these tests")
|
||||
}
|
||||
}
|
||||
|
||||
fn profile(id: u128, name: &str, command: &str) -> AgentProfile {
|
||||
AgentProfile::new(
|
||||
ProfileId::from_uuid(uuid::Uuid::from_u128(id)),
|
||||
@ -167,6 +187,20 @@ async fn detect_error_degrades_to_unavailable_not_hard_failure() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn detect_execute_awaits_async_runtime_without_nested_runtime_panic() {
|
||||
let detect = DetectProfiles::new(Arc::new(YieldingRuntime));
|
||||
let out = detect
|
||||
.execute(DetectProfilesInput {
|
||||
candidates: vec![profile(1, "Claude", "claude")],
|
||||
})
|
||||
.await
|
||||
.expect("async detection must complete inside a multi-thread Tokio runtime");
|
||||
|
||||
assert_eq!(out.results.len(), 1);
|
||||
assert!(out.results[0].available);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ConfigureProfiles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -284,8 +284,9 @@ impl FakeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AgentRuntime for FakeRuntime {
|
||||
fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
async fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
|
||||
Ok(true)
|
||||
}
|
||||
fn prepare_invocation(
|
||||
|
||||
Reference in New Issue
Block a user