fix(cross-project): ask/cancel inter-projets routent par agentId résolu, plus par nom — agents homonymes désambiguïsés par projectId+agentId, tests ask/cancel names_collide — #260/#263 (QA verte)

This commit is contained in:
2026-09-07 16:55:37 +02:00
parent b0f8fe4495
commit 032fbaeeef
2 changed files with 130 additions and 5 deletions

View File

@ -106,6 +106,16 @@ impl FakeContexts {
.find(|e| &e.agent_id == agent)
.map(|e| e.md_path.clone())
}
fn add_agent(&self, agent: &Agent, content: &str) {
let mut inner = self.0.lock().unwrap();
inner
.manifest
.entries
.push(ManifestEntry::from_agent(agent));
inner
.contents
.insert(agent.context_path.clone(), content.to_owned());
}
}
#[async_trait]
@ -932,6 +942,94 @@ async fn cross_project_ask_rejects_non_contactable_agent_before_launch() {
assert!(fx.pty.spawns().is_empty(), "agent must not be launched");
}
#[tokio::test]
async fn cross_project_ask_routes_by_agent_id_when_names_collide() {
let first =
scratch_agent(aid(1), "Architect", "agents/architect-first.md").with_contactable(true);
let second =
scratch_agent(aid(2), "Architect", "agents/architect-second.md").with_contactable(true);
let contexts = FakeContexts::with_agent(&first, "# first");
contexts.add_agent(&second, "# second");
let fx = ask_fixture(contexts);
let target_project = other_project();
fx.mailbox
.completions()
.push(first.id, TestCompletion::Replied("wrong first".to_owned()));
fx.mailbox.completions().push(
second.id,
TestCompletion::Replied("right second".to_owned()),
);
let out = timeout(
TEST_GUARD,
fx.service.ask_project_agent_for_mcp(
target_project.id,
second.id,
"work".to_owned(),
project().id,
aid(99),
"corr-homonym-ask".to_owned(),
),
)
.await
.expect("cross-project ask must not hang")
.expect("cross-project ask ok");
assert_eq!(out.reply.as_deref(), Some("right second"));
assert_eq!(fx.mailbox.pending(&first.id), 0);
assert_eq!(fx.mailbox.pending(&second.id), 0);
}
#[tokio::test]
async fn cross_project_cancel_routes_by_agent_id_when_names_collide() {
let first =
scratch_agent(aid(1), "Architect", "agents/architect-first.md").with_contactable(true);
let second =
scratch_agent(aid(2), "Architect", "agents/architect-second.md").with_contactable(true);
let contexts = FakeContexts::with_agent(&first, "# first");
contexts.add_agent(&second, "# second");
let fx = ask_fixture(contexts);
let target_project = other_project();
let first_cancelled = Arc::new(Mutex::new(0_u32));
let second_cancelled = Arc::new(Mutex::new(0_u32));
let first_session = Arc::new(CompletionSession {
id: sid(9101),
agent: first.id,
completions: fx.mailbox.completions(),
cancel_called: Some(Arc::clone(&first_cancelled)),
shutdown_called: None,
}) as Arc<dyn AgentSession>;
let second_session = Arc::new(CompletionSession {
id: sid(9102),
agent: second.id,
completions: fx.mailbox.completions(),
cancel_called: Some(Arc::clone(&second_cancelled)),
shutdown_called: None,
}) as Arc<dyn AgentSession>;
fx.structured
.insert_in_project(target_project.id, first_session, first.id, nid(9101));
fx.structured
.insert_in_project(target_project.id, second_session, second.id, nid(9102));
timeout(
TEST_GUARD,
fx.service.cancel_project_agent_for_mcp(
target_project.id,
second.id,
project().id,
aid(99),
"corr-homonym-cancel".to_owned(),
),
)
.await
.expect("cross-project cancel must not hang")
.expect("cross-project cancel ok");
assert_eq!(*first_cancelled.lock().unwrap(), 0);
assert_eq!(*second_cancelled.lock().unwrap(), 1);
}
#[tokio::test]
async fn spawn_unknown_agent_creates_then_launches() {
let fx = fixture(FakeContexts::new());
@ -2041,6 +2139,10 @@ fn ask_fixture_full(
Arc::clone(&mediator) as Arc<dyn InputMediator>,
Arc::clone(&mailbox) as Arc<dyn AgentMailbox>,
)
.with_project_store(Arc::new(FakeProjects::new(vec![
project(),
other_project(),
])))
.with_conversations(conversations)
.with_events(Arc::new(bus.clone()))
.with_structured(Arc::clone(&structured))