fix(custom-cli): le bouton Stop annule le tour courant sans fermer la session et cascade l'annulation aux agents délégués — stop_agent suit l'arbre des dépendances actives (cancel_current_turn + préemption runtime + enfants d'exécution), TurnCancel tue le process du tour en cours, autorisation requester/sous-arbre sur idea_stop_agent, fixtures DTO runtime_state/node_id alignées — #257 (QA verte)

This commit is contained in:
2026-08-28 22:57:32 +02:00
parent 9505a3305d
commit 1aabc3d702
12 changed files with 534 additions and 110 deletions

View File

@ -1034,14 +1034,18 @@ async fn stop_agent_kills_the_right_session() {
}
#[tokio::test]
async fn stop_agent_stops_structured_session() {
async fn stop_agent_cancels_structured_turn_without_closing_session() {
let agent = scratch_agent(aid(1), "dev-backend", "agents/dev-backend.md");
let contexts = FakeContexts::with_agent(&agent, "# persona");
let fx = ask_fixture(contexts);
let cancel_called = Arc::new(Mutex::new(0_u32));
let shutdown_called = Arc::new(Mutex::new(0_u32));
let session = Arc::new(CompletionSession {
id: sid(9010),
agent: agent.id,
completions: fx.mailbox.completions(),
cancel_called: Some(Arc::clone(&cancel_called)),
shutdown_called: Some(Arc::clone(&shutdown_called)),
}) as Arc<dyn AgentSession>;
fx.structured
.insert_in_project(project_id(), session, agent.id, nid(9010));
@ -1062,10 +1066,60 @@ async fn stop_agent_stops_structured_session() {
fx.pty.kills().is_empty(),
"structured stop must not kill PTY"
);
assert_eq!(*cancel_called.lock().unwrap(), 1);
assert_eq!(
*shutdown_called.lock().unwrap(),
0,
"structured stop must not shutdown the session"
);
assert!(
fx.structured
.session_for_agent_in_project(project_id(), &agent.id)
.is_some(),
"structured session is preserved for later turns"
);
}
#[tokio::test]
async fn stop_agent_from_unrelated_agent_is_rejected() {
let agent = scratch_agent(aid(1), "dev-backend", "agents/dev-backend.md");
let contexts = FakeContexts::with_agent(&agent, "# persona");
let fx = ask_fixture(contexts);
let cancel_called = Arc::new(Mutex::new(0_u32));
let shutdown_called = Arc::new(Mutex::new(0_u32));
let session = Arc::new(CompletionSession {
id: sid(9010),
agent: agent.id,
completions: fx.mailbox.completions(),
cancel_called: Some(Arc::clone(&cancel_called)),
shutdown_called: Some(Arc::clone(&shutdown_called)),
}) as Arc<dyn AgentSession>;
fx.structured
.insert_in_project(project_id(), session, agent.id, nid(9010));
let requester = uuid::Uuid::from_u128(99);
let err = fx
.service
.dispatch(
&project(),
cmd(&format!(
r#"{{ "type":"agent.stop", "requestedBy":"{requester}", "targetAgent":"dev-backend" }}"#
)),
)
.await
.unwrap_err();
assert_eq!(err.code(), "INVALID");
assert_eq!(
*cancel_called.lock().unwrap(),
0,
"unrelated agent must not cancel target work"
);
assert_eq!(*shutdown_called.lock().unwrap(), 0);
assert!(fx
.structured
.session_for_agent_in_project(project_id(), &agent.id)
.is_none());
.is_some());
}
#[tokio::test]
@ -1162,6 +1216,8 @@ async fn ask_agent_after_delete_recreate_same_name_uses_new_agent_id() {
id: sid(9001),
agent: old.id,
completions: fx.mailbox.completions(),
cancel_called: None,
shutdown_called: None,
}) as Arc<dyn AgentSession>;
fx.structured
.insert_in_project(project_id(), old_session, old.id, nid(9001));
@ -1617,6 +1673,8 @@ struct CompletionSession {
id: SessionId,
agent: AgentId,
completions: Arc<CompletionBus>,
cancel_called: Option<Arc<Mutex<u32>>>,
shutdown_called: Option<Arc<Mutex<u32>>>,
}
#[async_trait]
@ -1643,7 +1701,16 @@ impl AgentSession for CompletionSession {
)),
}
}
async fn cancel_current_turn(&self) -> Result<(), AgentSessionError> {
if let Some(cancel_called) = &self.cancel_called {
*cancel_called.lock().unwrap() += 1;
}
Ok(())
}
async fn shutdown(&self) -> Result<(), AgentSessionError> {
if let Some(shutdown_called) = &self.shutdown_called {
*shutdown_called.lock().unwrap() += 1;
}
Ok(())
}
}
@ -1701,6 +1768,8 @@ impl AgentSessionFactory for CompletionFactory {
id,
agent,
completions: Arc::clone(&self.completions),
cancel_called: None,
shutdown_called: None,
}))
}
}
@ -1728,6 +1797,8 @@ fn seed_structured_sessions(
id: SessionId::from_uuid(Uuid::from_u128(8_000 + idx as u128)),
agent: entry.agent_id,
completions: Arc::clone(&completions),
cancel_called: None,
shutdown_called: None,
}) as Arc<dyn AgentSession>;
structured.insert(session, entry.agent_id, nid(8_000 + idx as u128));
}