fix(orchestrator): LiveStateTurnGuard ramène la LED live-state à Done au drop du futur ask cross-project — #281 (RAII armé après publication Working, désarmé sur sorties explicites mark_target_done_best_effort, Drop spawn best-effort UpdateLiveState Done avec last_delegation=ticket; test régression dropped_ask_future_marks_live_state_done; QA verte: cargo fmt, orchestrator_service 78/78, npm build frontend, AppImage rebuildée 2026-09-08 13:52; réserve documentée: échec préexistant launch_conventionfile_injects_assigned_skills_in_order hors périmètre)

This commit is contained in:
2026-09-08 14:54:09 +02:00
parent 3ff62082eb
commit 566e3c3732
2 changed files with 121 additions and 0 deletions

View File

@ -2865,6 +2865,45 @@ async fn dropped_ask_future_frees_busy_target() {
);
}
/// Régression cross-project/workstate — si le client MCP appelant abandonne un
/// `idea_project_ask_agent`, le futur `ask` est droppé. Le garde Busy libère déjà
/// la FIFO ; il faut aussi nettoyer le live-state, sinon l'UI garde une LED
/// `working`/jaune jusqu'au restart ou à la TTL.
#[tokio::test]
async fn dropped_ask_future_marks_live_state_done() {
let agent = scratch_agent(aid(1), "architect", "agents/architect.md");
let fx = ask_fixture(FakeContexts::with_agent(&agent, "# persona"));
seed_live_pty(&fx.sessions, aid(1), sid(800));
let svc = Arc::clone(&fx.service);
let ask = tokio::spawn(async move { svc.dispatch(&project(), cmd(ASK_JSON)).await });
await_until(|| fx.mailbox.pending(&aid(1)) == 1).await;
await_until(|| {
fx.live
.entry_for(aid(1))
.is_some_and(|entry| entry.status == WorkStatus::Working)
})
.await;
ask.abort();
let _ = ask.await;
await_until(|| {
fx.live
.entry_for(aid(1))
.is_some_and(|entry| entry.status == WorkStatus::Done)
})
.await;
let entry = fx
.live
.entry_for(aid(1))
.expect("target live entry present");
assert_eq!(
entry.last_delegation, entry.ticket,
"drop cleanup preserves the ticket as last_delegation"
);
}
/// Régression (garde RAII) — une **2e délégation** vers une cible dont un 1er `ask` a
/// été abandonné est bien livrée : l'agent n'est pas coincé `Busy`, son tour suivant
/// démarre et peut être résolu par `idea_reply`.