fix(background-task): découpler la détection de fin de tâche du drain output PTY (#2)

Le runner de tâches de fond détectait la fin d'un process via l'EOF du drain de
sortie PTY, couplant le cycle de vie du process au flux d'output. Un output
encore ouvert (ou drainé ailleurs) pouvait masquer ou retarder la détection de
fin.

Ajoute wait/try_wait au port figé PtyPort :
- wait : bloquant, rend un ExitStatus idempotent ;
- try_wait : non bloquant, rend Option<ExitStatus> ;
- exit status mémorisé pour garder wait/try_wait/kill cohérents.
Implémentation dans PortablePtyAdapter (état d'exit mémorisé). Le
CommandBackgroundRunner détecte désormais la fin via pty.wait (select sur
cancel/deadline) au lieu de l'EOF du drain. Tous les fakes PtyPort du workspace
sont complétés en conséquence.

Le tee live UI reste hors périmètre (traité en #58). Aucun breaking IPC/front.

Tests : infrastructure/tests/background_task_runner.rs (nouveau) + pty_adapter.rs
verts, non-régression application/app-tauri OK (hors échecs réseau du sandbox).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 23:42:40 +02:00
parent c6e34483d3
commit 62b71776f5
15 changed files with 550 additions and 37 deletions

View File

@ -180,6 +180,34 @@ async fn scrollback_retains_recent_output_for_repaint() {
let _ = pty.kill(&handle).await;
}
#[tokio::test]
async fn wait_reports_short_command_exit_without_output_drain() {
let pty = PortablePtyAdapter::new();
let handle = pty
.spawn(sh_spec("printf wait-decoupled"), size())
.await
.expect("spawn");
let status = pty.wait(&handle).await.expect("wait succeeds");
assert_eq!(status.code, Some(0));
let sb = pty
.scrollback(&handle)
.expect("scrollback readable after wait");
let text = String::from_utf8_lossy(&sb);
assert!(
text.contains("wait-decoupled"),
"reader thread still collects output independently, got {text:?}"
);
let cleanup = pty.kill(&handle).await.expect("cleanup kill succeeds");
assert_eq!(cleanup, status, "cleanup returns memorized exit status");
assert!(
pty.scrollback(&handle).is_err(),
"cleanup purges the session"
);
}
#[tokio::test]
async fn scrollback_is_bounded_to_cap_and_keeps_most_recent_bytes() {
// Emit clearly more than 100 KB of deterministic output, then assert the
@ -228,6 +256,8 @@ async fn write_resize_kill_on_unknown_handle_are_not_found() {
assert_eq!(pty.write(&ghost, b"x"), Err(PtyError::NotFound));
assert_eq!(pty.resize(&ghost, size()), Err(PtyError::NotFound));
assert!(pty.subscribe_output(&ghost).is_err());
assert_eq!(pty.try_wait(&ghost), Err(PtyError::NotFound));
assert_eq!(pty.wait(&ghost).await, Err(PtyError::NotFound));
assert_eq!(pty.kill(&ghost).await, Err(PtyError::NotFound));
}