feat(agents): role orchestrateur du contexte global expose et configurable par agent — isOrchestrator distinct des permissions MCP, DTO update_project_orchestrator (terminologie wire legacy conservee), refus idea_context_update explicite avec writer effectif + orientation idea_context_propose, surface AgentsPanel + adaptateurs frontend — #276 (QA verte: cargo fmt/check, tests cibles application/app-tauri/infrastructure/backend, npm typecheck, 69/69 tests frontend agents/adapters)
This commit is contained in:
@ -59,6 +59,7 @@ use application::{
|
||||
ReadAgentContextInput, ReleaseManagedTemp, ResolveManagedTemp, StructuredRoutingMode,
|
||||
StructuredSessions, TerminalSessions, UpdateAgentContactable, UpdateAgentContactableInput,
|
||||
UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort, UpdateAgentEffortInput,
|
||||
UpdateProjectOrchestrator, UpdateProjectOrchestratorInput,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -1069,6 +1070,63 @@ async fn list_agents_default_orchestrator_is_oldest_agent_when_none_designated()
|
||||
assert_eq!(entries[1].is_orchestrator, false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_project_orchestrator_persists_single_writer_and_publishes_change() {
|
||||
let a1 = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
let a2 = scratch_agent(aid(2), "Frontend", "agents/frontend.md", pid(9));
|
||||
let contexts = FakeContexts::with_agent(&a1, "ctx1");
|
||||
contexts
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.manifest
|
||||
.entries
|
||||
.push(ManifestEntry::from_agent(&a2));
|
||||
let events = SpyBus::default();
|
||||
let update =
|
||||
UpdateProjectOrchestrator::new(Arc::new(contexts.clone()), Arc::new(events.clone()));
|
||||
|
||||
let out = update
|
||||
.execute(UpdateProjectOrchestratorInput {
|
||||
project: project(),
|
||||
agent_id: a2.id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.orchestrator, a2.id);
|
||||
assert_eq!(contexts.manifest().orchestrator, Some(a2.id));
|
||||
assert_eq!(contexts.manifest().effective_orchestrator(), Some(a2.id));
|
||||
assert_eq!(
|
||||
events.events(),
|
||||
vec![DomainEvent::OrchestratorChanged {
|
||||
project_id: project().id,
|
||||
orchestrator: Some(a2.id),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_project_orchestrator_rejects_unknown_agent_without_mutation_or_event() {
|
||||
let a1 = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
let contexts = FakeContexts::with_agent(&a1, "ctx1");
|
||||
let events = SpyBus::default();
|
||||
let update =
|
||||
UpdateProjectOrchestrator::new(Arc::new(contexts.clone()), Arc::new(events.clone()));
|
||||
|
||||
let err = update
|
||||
.execute(UpdateProjectOrchestratorInput {
|
||||
project: project(),
|
||||
agent_id: aid(404),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.code(), "NOT_FOUND");
|
||||
assert_eq!(contexts.manifest().orchestrator, None);
|
||||
assert!(events.events().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_then_update_context_roundtrips() {
|
||||
let a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
@ -1125,6 +1183,30 @@ async fn update_agent_effort_sets_preset_selection_and_persists_manifest() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_mutation_preserves_explicit_project_orchestrator() {
|
||||
let a1 = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
let a2 = scratch_agent(aid(2), "Frontend", "agents/frontend.md", pid(9));
|
||||
let contexts = FakeContexts::with_agent(&a1, "ctx1");
|
||||
{
|
||||
let mut inner = contexts.0.lock().unwrap();
|
||||
inner.manifest.entries.push(ManifestEntry::from_agent(&a2));
|
||||
inner.manifest.designate(a2.id).unwrap();
|
||||
}
|
||||
let update = UpdateAgentEffort::new(Arc::new(contexts.clone()));
|
||||
|
||||
update
|
||||
.execute(UpdateAgentEffortInput {
|
||||
project: project(),
|
||||
agent_id: a1.id,
|
||||
effort: Some(EffortSelection::Preset("high".to_owned())),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contexts.manifest().orchestrator, Some(a2.id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_agent_effort_none_clears_existing_override() {
|
||||
let a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9))
|
||||
@ -1236,6 +1318,35 @@ async fn delete_removes_entry_then_unknown_is_not_found() {
|
||||
assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_writer_clears_explicit_designation_and_falls_back_to_oldest_remaining() {
|
||||
let a1 = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
let a2 = scratch_agent(aid(2), "Frontend", "agents/frontend.md", pid(9));
|
||||
let contexts = FakeContexts::with_agent(&a1, "ctx1");
|
||||
{
|
||||
let mut inner = contexts.0.lock().unwrap();
|
||||
inner.manifest.entries.push(ManifestEntry::from_agent(&a2));
|
||||
inner.manifest.designate(a2.id).unwrap();
|
||||
}
|
||||
let events = SpyBus::default();
|
||||
let delete = DeleteAgent::new(Arc::new(contexts.clone()), Arc::new(events.clone()));
|
||||
|
||||
delete
|
||||
.execute(DeleteAgentInput {
|
||||
project: project(),
|
||||
agent_id: a2.id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contexts.manifest().orchestrator, None);
|
||||
assert_eq!(contexts.manifest().effective_orchestrator(), Some(a1.id));
|
||||
assert!(events.events().contains(&DomainEvent::OrchestratorChanged {
|
||||
project_id: project().id,
|
||||
orchestrator: None,
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_agent_cleans_structured_then_pty_runtime_before_manifest_removal() {
|
||||
let a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
|
||||
|
||||
Reference in New Issue
Block a user