fix(orchestrator): la délégation simple vers UX renvoie son Final structuré (#207, QA verte)

Une demande idea_ask_agent vers UX échouait avec « finished without a structured Final response » alors qu'Architect répondait. Deux causes corrigées:

- session/process: un tour CLI terminé en code non nul SANS stdout (ex. erreur 401/auth) renvoyait une sortie vide au lieu du stderr réel; l'orchestrateur concluait alors à un « no Final ». On remonte désormais le stderr effectif (drain sync + async + helper process_exit_message + test dédié).

- orchestrator/service: la branche « ask waiting on background » masquait le premier Final structuré; la réponse synchrone redevient le premier Final (suppression de active_rendezvous_has_background_tasks / mark_rendezvous_task_waiting).

Nettoyage runtime corollaire (#204): arrêt du live runtime stale (PTY + structuré) d'un agent recréé afin que la délégation route vers l'agent courant (LiveSessions, stop_live_agent_runtime_in_project, wiring backend via DeleteAgent::with_runtime_cleanup, lifecycle, workstate cascade).
This commit is contained in:
2026-08-12 14:09:06 +02:00
parent ba176e3cf2
commit 646b9d9284
11 changed files with 673 additions and 203 deletions

View File

@ -186,6 +186,58 @@ mod tests {
assert!(matches!(err, AgentSessionError::Start(_)), "vu: {err:?}");
}
#[tokio::test]
async fn run_turn_nonzero_without_stdout_surfaces_stderr() {
use std::io::Write as _;
use std::sync::atomic::{AtomicU64, Ordering};
static C: AtomicU64 = AtomicU64::new(0);
let mut path = std::env::temp_dir();
path.push(format!(
"idea-empty-failing-cli-{}-{}",
std::process::id(),
C.fetch_add(1, Ordering::Relaxed)
));
{
let mut file = std::fs::File::create(&path).expect("write fake cli");
file.write_all(
b"#!/bin/sh\nprintf '%s\\n' '401 Unauthorized: Missing bearer' >&2\nexit 1\n",
)
.expect("write fake cli");
file.sync_all().expect("sync fake cli");
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&path, perms).unwrap();
}
let spec = super::process::SpawnLine {
command: path.to_string_lossy().into_owned(),
args: Vec::new(),
cwd: "/".to_owned(),
env: Vec::new(),
stdin: None,
sandbox: None,
};
let err = run_turn(&spec, None, None, None)
.await
.expect_err("empty nonzero process must be a session error");
match err {
AgentSessionError::Io(message) => {
assert!(
message.contains("401 Unauthorized: Missing bearer"),
"stderr must be preserved, got: {message}"
);
}
other => panic!("expected Io, got {other:?}"),
}
let _ = std::fs::remove_file(&path);
}
// -- parse_event Claude (format RÉEL vérifié 2026-06-09) --------------
#[test]