fix(backend,orchestrator,chat): l'objectif suspendu guide le premier tour post-restart — #290 regression partielle (register_suspended_turn capture la demande utilisateur exacte au point d'entrée chat agent_send/chat.send avec source diagnostique, RestartRendezvousCoordinator conserve tour/objectif/restart_attempt_id, take_continuation consommable exactement une fois et seulement par la tentative courante: suspended_restart_continuation_prompt place l'objectif utilisateur avant toute consigne generique et StructuredHumanTurnTransition::Resume porte le prompt host-provided injecte au runtime de remplacement; nettoyage du tour suspendu sur succes, echec, annulation et abandon/expiration; diagnostics projet/agent/turn_id/restart_attempt_id/source; rendez-vous inter-agents #289 inchanges; QA verte reconfirmee par Git: cargo check --workspace, backend self_restart_contract_tests 3/3, orchestrator_service restart 10/10, workstate_actions 9/9, app-tauri chat_bridge 31/31, web-server websocket_chat_send 3/3, fmt --check, diff --check; restants: rebuild AppImage + validation live reprise objectif apres restart; carnet #290 iteration regression + passage store qa inclus; scripts/ hors périmètre; aucun push)

This commit is contained in:
2026-09-09 09:17:36 +02:00
parent 93a1c23a29
commit 18c1b01afb
10 changed files with 426 additions and 41 deletions

View File

@ -5770,6 +5770,96 @@ async fn direct_human_turn_registration_surfaces_restart_failure() {
);
}
#[tokio::test]
async fn suspended_human_objective_is_bound_to_latest_attempt_and_taken_once() {
let coordinator = Arc::new(application::RestartRendezvousCoordinator::new());
let key = rkey(1);
let turn = coordinator.register_suspended_turn(
key,
"human-turn-290".to_owned(),
"redémarre puis confirme que tu as redémarré".to_owned(),
"agent_send".to_owned(),
);
assert!(coordinator.begin(key, "attempt-old".to_owned()));
assert!(coordinator.begin(key, "attempt-current".to_owned()));
coordinator.ready(key, "attempt-old");
assert!(
turn.take_continuation("attempt-old").is_none(),
"an older restart attempt cannot inject its handoff"
);
coordinator.ready(key, "attempt-current");
let continuation = turn
.take_continuation("attempt-current")
.expect("the current attempt owns the suspended objective");
assert_eq!(continuation.turn_id, "human-turn-290");
assert_eq!(
continuation.objective_text,
"redémarre puis confirme que tu as redémarré"
);
assert_eq!(continuation.source, "agent_send");
assert!(
turn.take_continuation("attempt-current").is_none(),
"the same restart attempt cannot inject twice"
);
}
#[tokio::test]
async fn dropping_suspended_turn_during_restart_cleans_handoff_and_unblocks_scheduler() {
let coordinator = Arc::new(application::RestartRendezvousCoordinator::new());
let key = rkey(1);
let turn = coordinator.register_suspended_turn(
key,
"human-turn-expired".to_owned(),
"objectif expiré".to_owned(),
"agent_send".to_owned(),
);
let restart_id = "attempt-expired";
assert!(coordinator.begin(key, restart_id.to_owned()));
drop(turn);
let error = coordinator
.wait_for_handoff(key, restart_id)
.await
.expect_err("dropping the bounded turn cancels and unblocks restart");
assert!(error.contains("cancelled"));
coordinator.ready(key, restart_id);
assert!(
coordinator.take_continuation(key, restart_id).is_none(),
"an expired objective is cleaned and cannot be injected"
);
coordinator.finish(key, restart_id);
}
#[tokio::test]
async fn cancelling_suspended_human_restart_cleans_handoff_and_unblocks_scheduler() {
let coordinator = Arc::new(application::RestartRendezvousCoordinator::new());
let key = rkey(1);
let turn = coordinator.register_suspended_turn(
key,
"human-turn-cancelled".to_owned(),
"objectif annulé".to_owned(),
"agent_send".to_owned(),
);
let restart_id = "attempt-cancelled";
assert!(coordinator.begin(key, restart_id.to_owned()));
assert!(coordinator.cancel(key));
let error = coordinator
.wait_for_handoff(key, restart_id)
.await
.expect_err("cancel must unblock the detached restart scheduler");
assert!(error.contains("cancelled"));
assert!(
turn.take_continuation(restart_id).is_none(),
"cancelled objective cannot reach a replacement runtime"
);
drop(turn);
coordinator.finish(key, restart_id);
}
/// Régression #287 : la sonde transcript peut être stable (notamment inexistante pour
/// un provider non-Claude), tandis que `send_with_tap` publie réellement l'activité du
/// tour. Chaque événement non terminal doit réarmer la fenêtre jusqu'au `Final` tardif.