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:
@ -86,6 +86,12 @@ fn running_task(id: u128, project: u128, owner: u128, now_ms: u64) -> Background
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn cancelling_task(id: u128, project: u128, owner: u128, now_ms: u64) -> BackgroundTask {
|
||||
running_task(id, project, owner, now_ms)
|
||||
.transition(BackgroundTaskState::Cancelling, now_ms + 2)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn completed_task(id: u128, project: u128, owner: u128, now_ms: u64) -> BackgroundTask {
|
||||
running_task(id, project, owner, now_ms)
|
||||
.complete(BackgroundTaskResult::Success {
|
||||
@ -337,6 +343,31 @@ async fn reconcile_marks_running_without_live_handle_failed_and_pending() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_marks_cancelling_without_live_handle_cancel_failed_and_pending() {
|
||||
let tmp = TempDir::new();
|
||||
let store = FsBackgroundTaskStore::new(&tmp.project_path());
|
||||
let lost = cancelling_task(1, 10, 100, 1_000);
|
||||
|
||||
store.create(&lost).await.unwrap();
|
||||
|
||||
let report = store.reconcile_boot(&[], 2_000).await.unwrap();
|
||||
|
||||
assert_eq!(report.failed_task_ids, vec![lost.id]);
|
||||
assert_eq!(report.delivery_pending_task_ids, vec![lost.id]);
|
||||
|
||||
let lost_after = store.get(lost.id).await.unwrap().unwrap();
|
||||
assert_eq!(lost_after.state, BackgroundTaskState::CancelFailed);
|
||||
assert!(lost_after.has_pending_completion_delivery());
|
||||
assert!(matches!(
|
||||
lost_after.result,
|
||||
Some(BackgroundTaskResult::CancelFailed {
|
||||
finished_at_ms: 2_000,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_uses_monotonic_timestamp_when_clock_is_behind() {
|
||||
let tmp = TempDir::new();
|
||||
|
||||
Reference in New Issue
Block a user