fix(backend,orchestrator,chat): tour humain agent_send survit au self-restart — #290 (le tour humain direct agent_send/ws chat.send s'enregistre au RestartRendezvousCoordinator via StructuredHumanTurnGuard: Final/Error de l'ancien provider devient transition de contrôle et non le Final logique; transition_after_provider_terminal_async attend Ready puis accept_ready acquitte le handoff et libère le scheduler, Resume => drain de la session de remplacement avec SELF_RESTART_CONTINUATION_PROMPT, seul le terminal du remplacement est transmis au chat; Failed/Cancelled surfacés explicitement avec libération Busy/FIFO; send_with_tap en erreur pendant restart actif traité comme teardown attendu (stream vide puis attente du remplacement, plus de Final prématuré ni chat bloqué); StopLiveAgent câblé with_restart_rendezvous: stop utilisateur annule le rendez-vous restart; RestartRendezvousRegistration rendu public (state/changed/accept_ready); QA verte: backend self_restart_contract_tests 3/3 dont 2 nouveaux (old terminal ignoré + handoff accepté, failure explicite + busy libéré), application orchestrator_service restart 8/8 dont registration directe du rendez-vous, workstate_actions 9/9, app-tauri chat_bridge 31/31, web-server websocket_chat_send 3/3, cargo check --workspace, fmt --check, diff --check — reconfirmés par Git avant commit; réserves hors périmètre préexistantes: 7 échecs fixture PTY web-server + 2 échecs plugin application; carnet #290 + clôture store #289 inclus; scripts/ hors périmètre; aucun push)

This commit is contained in:
2026-09-09 08:38:29 +02:00
parent 321ebdaab9
commit d03300a20f
16 changed files with 660 additions and 213 deletions

View File

@ -5716,6 +5716,60 @@ async fn same_project_stop_during_restart_releases_ask_and_leaves_no_replacement
);
}
#[tokio::test]
async fn direct_human_turn_registration_holds_restart_until_replacement_handoff() {
let coordinator = Arc::new(application::RestartRendezvousCoordinator::new());
let key = rkey(1);
let restart_id = "restart-human-turn".to_owned();
let mut turn = coordinator.register(key);
assert!(
coordinator.begin(key, restart_id.clone()),
"a registered direct human turn owns self-restart continuation"
);
assert!(matches!(
turn.state(),
application::RestartRendezvousState::Restarting { .. }
));
coordinator.ready(key, &restart_id);
turn.changed().await.expect("ready transition remains live");
assert_eq!(
turn.state(),
application::RestartRendezvousState::Ready {
restart_id: restart_id.clone()
}
);
turn.accept_ready(&restart_id);
coordinator
.wait_for_handoff(key, &restart_id)
.await
.expect("scheduler is released only after the replacement is accepted");
assert_eq!(turn.state(), application::RestartRendezvousState::Idle);
}
#[tokio::test]
async fn direct_human_turn_registration_surfaces_restart_failure() {
let coordinator = Arc::new(application::RestartRendezvousCoordinator::new());
let key = rkey(1);
let restart_id = "restart-human-failure".to_owned();
let mut turn = coordinator.register(key);
assert!(coordinator.begin(key, restart_id.clone()));
coordinator.fail(key, &restart_id, "replacement launch failed".to_owned());
turn.changed()
.await
.expect("failure transition remains live");
assert_eq!(
turn.state(),
application::RestartRendezvousState::Failed {
restart_id,
message: "replacement launch failed".to_owned(),
}
);
}
/// 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.