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:
@ -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.
|
||||
|
||||
Reference in New Issue
Block a user