fix(runtime): cancel de background task immédiat et fiable depuis le workstate (#199, QA verte)

Transforme l'annulation d'une background task en contrat immédiat, observable et non-best-effort:

- domain/background_task: états Cancelling/Cancelled/CancelFailed et transitions idempotentes; un kill PTY qui échoue est surfacé en CancelFailed plutôt qu'en succès silencieux.

- infrastructure/runner + store: cancel effectif de la task ciblée, reconcile marque Cancelling même sans live handle, et départage CancelFailed des tâches pending au boot.

- application/background + workstate + orchestrator/wake: chemin de cancel initié depuis le workstate, projection synchronisée de l'état (pas d'attente d'un cycle ambigu).

- frontend: projection workstate de l'état cancelled/cancelling/cancelFailed (cancellingRow/cancelFailedRow/cancelButtons), normalization et services.

Tests QA verts: background_task_cancel, background_task_runner cancel_wins_even_if_output_is_still_available, background_task_store reconcile_marks_cancelling_without_live_handle_cancel_failed_and_pending, workstate.test.tsx.
This commit is contained in:
2026-08-12 15:18:38 +02:00
parent f5e2e302ba
commit 46103397de
24 changed files with 699 additions and 76 deletions

View File

@ -568,9 +568,15 @@ fn background_task(
.unwrap()
.transition(BackgroundTaskState::Waiting, created_at_ms + 20)
.unwrap(),
BackgroundTaskState::Cancelling => base
.transition(BackgroundTaskState::Running, created_at_ms + 10)
.unwrap()
.transition(BackgroundTaskState::Cancelling, created_at_ms + 20)
.unwrap(),
BackgroundTaskState::Completed
| BackgroundTaskState::Failed
| BackgroundTaskState::Cancelled
| BackgroundTaskState::CancelFailed
| BackgroundTaskState::Expired => {
let result = match state {
BackgroundTaskState::Completed => BackgroundTaskResult::Success {
@ -591,13 +597,18 @@ fn background_task(
finished_at_ms: created_at_ms + 20,
reason: "cancelled".to_owned(),
},
BackgroundTaskState::CancelFailed => BackgroundTaskResult::CancelFailed {
finished_at_ms: created_at_ms + 20,
error: "cancel failed".to_owned(),
},
BackgroundTaskState::Expired => BackgroundTaskResult::Expired {
finished_at_ms: created_at_ms + 20,
reason: "expired".to_owned(),
},
BackgroundTaskState::Queued
| BackgroundTaskState::Running
| BackgroundTaskState::Waiting => unreachable!(),
| BackgroundTaskState::Waiting
| BackgroundTaskState::Cancelling => unreachable!(),
};
let running = base
.transition(BackgroundTaskState::Running, created_at_ms + 10)