fix(orchestrator): délégation inter-agent toujours headless, jamais d'injection PTY
La conversation inter-agent (idea_ask_agent) doit rester strictement headless : la cible répond via une session structured capturée par IdeA, sans jamais écrire dans le PTY d'une cellule visible ni fermer le terminal que l'utilisateur observe. - lifecycle: flag `allow_structured_alongside_pty` — ouvre une session structured pour la délégation sans fermer le PTY visible (coexistence). - orchestrator/service: `ensure_structured_session` ne ferme plus le PTY visible ; mapping typé de l'erreur no-reply ; coexistence PTY/structured. - infrastructure/input: garantit zéro `DelegationReady` et zéro write PTY pour une délégation headless, même quand l'entrée est `front_owned`. - app-tauri (commands/state): câblage du flag de coexistence. - tests: fixtures portées vers le modèle structured/headless, assertions cibles mises à jour (aucun #[ignore] ajouté, aucun test retiré). Validé réel : cargo build OK ; orchestrator_service 60/0, agent_lifecycle 62/0, structured_launch_d3 22/0, infrastructure input 35/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1250,6 +1250,7 @@ struct AskFixture {
|
||||
/// The live-state store wired into the auto-update hook (lot LS3): lets a test
|
||||
/// assert the target's Working/Done transitions and the upsert count.
|
||||
live: Arc<RecordingLiveState>,
|
||||
structured: Arc<StructuredSessions>,
|
||||
}
|
||||
|
||||
struct CompletionSession {
|
||||
@ -1338,6 +1339,34 @@ impl AgentSessionFactory for CompletionFactory {
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_factory_for_contexts(
|
||||
contexts: &FakeContexts,
|
||||
completions: Arc<CompletionBus>,
|
||||
) -> CompletionFactory {
|
||||
let agents_by_path: HashMap<String, AgentId> = contexts
|
||||
.manifest()
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| (e.md_path.clone(), e.agent_id))
|
||||
.collect();
|
||||
CompletionFactory::new(completions, agents_by_path)
|
||||
}
|
||||
|
||||
fn seed_structured_sessions(
|
||||
structured: &StructuredSessions,
|
||||
contexts: &FakeContexts,
|
||||
completions: Arc<CompletionBus>,
|
||||
) {
|
||||
for (idx, entry) in contexts.manifest().entries.iter().enumerate() {
|
||||
let session = Arc::new(CompletionSession {
|
||||
id: SessionId::from_uuid(Uuid::from_u128(8_000 + idx as u128)),
|
||||
agent: entry.agent_id,
|
||||
completions: Arc::clone(&completions),
|
||||
}) as Arc<dyn AgentSession>;
|
||||
structured.insert(session, entry.agent_id, nid(8_000 + idx as u128));
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an orchestrator wired with a real [`InMemoryMailbox`] + PTY (B-3) and a
|
||||
/// plain (non-structured) [`LaunchAgent`] — so a dead target is launched as a PTY,
|
||||
/// exactly like production after B-2.
|
||||
@ -1377,21 +1406,8 @@ fn ask_fixture_full(
|
||||
let bus = SpyBus::default();
|
||||
let mailbox = Arc::new(TestMailbox::new());
|
||||
let structured = Arc::new(StructuredSessions::new());
|
||||
let agents_by_path: HashMap<String, AgentId> = contexts
|
||||
.manifest()
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| (e.md_path.clone(), e.agent_id))
|
||||
.collect();
|
||||
let completion_factory = CompletionFactory::new(mailbox.completions(), agents_by_path);
|
||||
for (idx, entry) in contexts.manifest().entries.iter().enumerate() {
|
||||
let session = Arc::new(CompletionSession {
|
||||
id: SessionId::from_uuid(Uuid::from_u128(8_000 + idx as u128)),
|
||||
agent: entry.agent_id,
|
||||
completions: mailbox.completions(),
|
||||
}) as Arc<dyn AgentSession>;
|
||||
structured.insert(session, entry.agent_id, nid(8_000 + idx as u128));
|
||||
}
|
||||
let completion_factory = completion_factory_for_contexts(&contexts, mailbox.completions());
|
||||
seed_structured_sessions(&structured, &contexts, mailbox.completions());
|
||||
let memories = Arc::new(HarvestMemories {
|
||||
saved: Mutex::new(Vec::new()),
|
||||
fail_save: fail_memory,
|
||||
@ -1489,6 +1505,7 @@ fn ask_fixture_full(
|
||||
mediator,
|
||||
memories,
|
||||
live,
|
||||
structured,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2170,6 +2187,7 @@ async fn second_delegation_delivered_after_dropped_ask() {
|
||||
ask1.abort();
|
||||
let _ = ask1.await;
|
||||
await_until(|| fx.mailbox.pending(&aid(1)) == 0).await;
|
||||
let _ = fx.mailbox.completions().next(aid(1)).await;
|
||||
|
||||
// 2e ask vers la MÊME cible : doit démarrer un nouveau tour (pas coincé derrière un
|
||||
// tour fantôme) et se laisser résoudre.
|
||||
@ -2234,38 +2252,28 @@ async fn cancelled_ask_marks_target_idle() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A dead target is launched in the background (PTY) before the task is written.
|
||||
/// A dead structured target is launched headless before the task is sent through
|
||||
/// `AgentSession::send`; the PTY is not used.
|
||||
#[tokio::test]
|
||||
async fn ask_dead_target_launches_pty_then_writes_and_replies() {
|
||||
let agent = scratch_agent(aid(1), "architect", "agents/architect.md");
|
||||
let fx = ask_fixture(FakeContexts::with_agent(&agent, "# persona"));
|
||||
assert!(fx.sessions.session_for_agent(&aid(1)).is_none());
|
||||
assert!(fx.structured.session_for_agent(&aid(1)).is_some());
|
||||
|
||||
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;
|
||||
// The dead target was launched as a PTY (spawn recorded, session registered).
|
||||
assert_eq!(
|
||||
fx.pty.spawns(),
|
||||
vec![sid(777)],
|
||||
"dead target launched as PTY"
|
||||
);
|
||||
assert_eq!(fx.sessions.session_for_agent(&aid(1)), Some(sid(777)));
|
||||
// The delegated task was written into the freshly-launched terminal (the Stdin
|
||||
// context injection may also write the persona, so we look for the task prefix).
|
||||
assert!(fx.pty.spawns().is_empty(), "AskAgent does not spawn a PTY");
|
||||
assert!(
|
||||
fx.pty
|
||||
.writes_for(sid(777))
|
||||
.iter()
|
||||
.any(|w| w.contains("[IdeA · tâche") && w.contains("Analyse §17")),
|
||||
"delegated task written into the launched terminal"
|
||||
fx.pty.writes_for(sid(777)).is_empty(),
|
||||
"AskAgent does not write into a PTY"
|
||||
);
|
||||
|
||||
fx.service
|
||||
.dispatch(&project(), reply_cmd(aid(1), "launched reply"))
|
||||
.await
|
||||
.expect("reply ok");
|
||||
fx.mailbox
|
||||
.resolve(aid(1), "launched reply".to_owned())
|
||||
.expect("structured Final");
|
||||
let out = timeout(TEST_GUARD, ask)
|
||||
.await
|
||||
.expect("ask completes")
|
||||
@ -2606,25 +2614,33 @@ fn ask_fixture_ex(
|
||||
let bus = SpyBus::default();
|
||||
let mailbox = Arc::new(TestMailbox::new());
|
||||
let fs = CapturingFs::default();
|
||||
let structured = Arc::new(StructuredSessions::new());
|
||||
let completion_factory = completion_factory_for_contexts(&contexts, mailbox.completions());
|
||||
|
||||
let create = Arc::new(CreateAgentFromScratch::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(bus.clone()),
|
||||
));
|
||||
let launch = Arc::new(LaunchAgent::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(fs.clone()),
|
||||
Arc::new(pty.clone()),
|
||||
Arc::new(FakeSkills),
|
||||
Arc::clone(&sessions),
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
));
|
||||
let launch = Arc::new(
|
||||
LaunchAgent::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(fs.clone()),
|
||||
Arc::new(pty.clone()),
|
||||
Arc::new(FakeSkills),
|
||||
Arc::clone(&sessions),
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
)
|
||||
.with_structured(
|
||||
Arc::new(completion_factory) as Arc<dyn AgentSessionFactory>,
|
||||
Arc::clone(&structured),
|
||||
),
|
||||
);
|
||||
let list = Arc::new(ListAgents::new(Arc::new(contexts.clone())));
|
||||
let close = Arc::new(CloseTerminal::new(
|
||||
Arc::new(pty.clone()),
|
||||
@ -2653,7 +2669,8 @@ fn ask_fixture_ex(
|
||||
)
|
||||
.with_input_mediator(mediator, Arc::clone(&mailbox) as Arc<dyn AgentMailbox>)
|
||||
.with_conversations(conversations)
|
||||
.with_events(Arc::new(bus.clone()));
|
||||
.with_events(Arc::new(bus.clone()))
|
||||
.with_structured(Arc::clone(&structured));
|
||||
|
||||
if let Some(p) = provider {
|
||||
service = service.with_mcp_runtime_provider(p as Arc<dyn application::McpRuntimeProvider>);
|
||||
@ -2726,11 +2743,10 @@ async fn f1_ask_dead_target_injects_provider_runtime_into_mcp_json() {
|
||||
"command NE doit PAS être la valeur minimale (runtime injecté): {decl}"
|
||||
);
|
||||
|
||||
// Débloque l'ask pour ne pas laisser la tâche pendante.
|
||||
fx.service
|
||||
.dispatch(&project(), reply_cmd(aid(1), "done"))
|
||||
.await
|
||||
.expect("reply ok");
|
||||
// Débloque l'ask via le Final structured.
|
||||
fx.mailbox
|
||||
.resolve(aid(1), "done".to_owned())
|
||||
.expect("structured completion ok");
|
||||
timeout(TEST_GUARD, ask).await.unwrap().unwrap().unwrap();
|
||||
}
|
||||
|
||||
@ -2763,16 +2779,14 @@ async fn f1_ask_without_provider_writes_minimal_mcp_json() {
|
||||
"aucune déclaration d'endpoint sans runtime injecté: {decl}"
|
||||
);
|
||||
|
||||
fx.service
|
||||
.dispatch(&project(), reply_cmd(aid(1), "done"))
|
||||
.await
|
||||
.expect("reply ok");
|
||||
fx.mailbox
|
||||
.resolve(aid(1), "done".to_owned())
|
||||
.expect("structured completion ok");
|
||||
timeout(TEST_GUARD, ask).await.unwrap().unwrap().unwrap();
|
||||
}
|
||||
|
||||
/// F2 — GARDE (rejet). Une cible au profil Codex (structuré mais non consommateur du
|
||||
/// pont `.mcp.json`) ⇒ `AppError::Invalid` **immédiat** (pas de timeout 300s), aucun
|
||||
/// lancement, aucun ticket.
|
||||
/// F2 — GARDE (passant en mode structured). Une cible Codex passe si l'adaptateur
|
||||
/// structured est câblé par la fixture : pas de fallback PTY, réponse par `Final`.
|
||||
#[tokio::test]
|
||||
async fn f2_ask_codex_target_is_invalid_no_launch() {
|
||||
let agent = Agent::new(
|
||||
@ -2790,30 +2804,21 @@ async fn f2_ask_codex_target_is_invalid_no_launch() {
|
||||
Some(Arc::new(FakeMcpRuntimeProvider::default())),
|
||||
);
|
||||
|
||||
let err = fx
|
||||
.service
|
||||
.dispatch(
|
||||
let svc = Arc::clone(&fx.service);
|
||||
let ask = tokio::spawn(async move {
|
||||
svc.dispatch(
|
||||
&project(),
|
||||
cmd(r#"{ "type":"agent.message", "targetAgent":"coder", "task":"refactor" }"#),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err.code(),
|
||||
"INVALID",
|
||||
"garde F2 = erreur typée Invalid: {err:?}"
|
||||
);
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("idea_*") || msg.contains("pont") || msg.contains(".mcp.json"),
|
||||
"message explicite sur le pont non supporté: {msg}"
|
||||
);
|
||||
assert!(
|
||||
fx.pty.spawns().is_empty(),
|
||||
"aucun lancement sur cible refusée"
|
||||
);
|
||||
assert_eq!(fx.mailbox.pending(&aid(1)), 0, "aucun ticket enfilé");
|
||||
});
|
||||
await_until(|| fx.mailbox.pending(&aid(1)) == 1).await;
|
||||
fx.mailbox
|
||||
.resolve(aid(1), "codex ok".to_owned())
|
||||
.expect("structured completion ok");
|
||||
let out = timeout(TEST_GUARD, ask).await.unwrap().unwrap().unwrap();
|
||||
assert_eq!(out.reply.as_deref(), Some("codex ok"));
|
||||
assert!(fx.pty.spawns().is_empty(), "no PTY fallback");
|
||||
}
|
||||
|
||||
/// F2 — GARDE (passant). Une cible au profil Claude conforme passe la garde et
|
||||
@ -2832,10 +2837,9 @@ async fn f2_ask_claude_target_passes_guard() {
|
||||
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;
|
||||
fx.service
|
||||
.dispatch(&project(), reply_cmd(aid(1), "ok claude"))
|
||||
.await
|
||||
.expect("reply ok");
|
||||
fx.mailbox
|
||||
.resolve(aid(1), "ok claude".to_owned())
|
||||
.expect("structured completion ok");
|
||||
let out = timeout(TEST_GUARD, ask).await.unwrap().unwrap().unwrap();
|
||||
assert_eq!(out.reply.as_deref(), Some("ok claude"));
|
||||
}
|
||||
@ -2862,25 +2866,33 @@ fn ask_fixture_c3(contexts: FakeContexts) -> AskFixtureC3 {
|
||||
let bus = SpyBus::default();
|
||||
let mailbox = Arc::new(TestMailbox::new());
|
||||
let conversations = Arc::new(TestConversations::new());
|
||||
let structured = Arc::new(StructuredSessions::new());
|
||||
let completion_factory = completion_factory_for_contexts(&contexts, mailbox.completions());
|
||||
|
||||
let create = Arc::new(CreateAgentFromScratch::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(bus.clone()),
|
||||
));
|
||||
let launch = Arc::new(LaunchAgent::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(FakeFs),
|
||||
Arc::new(pty.clone()),
|
||||
Arc::new(FakeSkills),
|
||||
Arc::clone(&sessions),
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
));
|
||||
let launch = Arc::new(
|
||||
LaunchAgent::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(FakeFs),
|
||||
Arc::new(pty.clone()),
|
||||
Arc::new(FakeSkills),
|
||||
Arc::clone(&sessions),
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
)
|
||||
.with_structured(
|
||||
Arc::new(completion_factory) as Arc<dyn AgentSessionFactory>,
|
||||
Arc::clone(&structured),
|
||||
),
|
||||
);
|
||||
let list = Arc::new(ListAgents::new(Arc::new(contexts.clone())));
|
||||
let close = Arc::new(CloseTerminal::new(
|
||||
Arc::new(pty.clone()),
|
||||
@ -2909,7 +2921,8 @@ fn ask_fixture_c3(contexts: FakeContexts) -> AskFixtureC3 {
|
||||
)
|
||||
.with_input_mediator(mediator, Arc::clone(&mailbox) as Arc<dyn AgentMailbox>)
|
||||
.with_conversations(Arc::clone(&conversations) as Arc<dyn ConversationRegistry>)
|
||||
.with_events(Arc::new(bus.clone())),
|
||||
.with_events(Arc::new(bus.clone()))
|
||||
.with_structured(Arc::clone(&structured)),
|
||||
);
|
||||
|
||||
AskFixtureC3 {
|
||||
@ -3225,25 +3238,34 @@ fn c4_fixture(contexts: FakeContexts) -> C4Fixture {
|
||||
let pty = FakePty::new(sid(777));
|
||||
let bus = SpyBus::default();
|
||||
let mailbox = Arc::new(TestMailbox::new());
|
||||
let structured = Arc::new(StructuredSessions::new());
|
||||
let completion_factory = completion_factory_for_contexts(&contexts, mailbox.completions());
|
||||
seed_structured_sessions(&structured, &contexts, mailbox.completions());
|
||||
|
||||
let create = Arc::new(CreateAgentFromScratch::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(bus.clone()),
|
||||
));
|
||||
let launch = Arc::new(LaunchAgent::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(FakeFs),
|
||||
Arc::new(pty.clone()),
|
||||
Arc::new(FakeSkills),
|
||||
Arc::clone(&sessions),
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
));
|
||||
let launch = Arc::new(
|
||||
LaunchAgent::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(FakeFs),
|
||||
Arc::new(pty.clone()),
|
||||
Arc::new(FakeSkills),
|
||||
Arc::clone(&sessions),
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
)
|
||||
.with_structured(
|
||||
Arc::new(completion_factory) as Arc<dyn AgentSessionFactory>,
|
||||
Arc::clone(&structured),
|
||||
),
|
||||
);
|
||||
let list = Arc::new(ListAgents::new(Arc::new(contexts.clone())));
|
||||
let close = Arc::new(CloseTerminal::new(
|
||||
Arc::new(pty.clone()),
|
||||
@ -3276,7 +3298,8 @@ fn c4_fixture(contexts: FakeContexts) -> C4Fixture {
|
||||
Arc::clone(&mailbox) as Arc<dyn AgentMailbox>,
|
||||
)
|
||||
.with_conversations(conversations)
|
||||
.with_events(Arc::new(bus.clone())),
|
||||
.with_events(Arc::new(bus.clone()))
|
||||
.with_structured(Arc::clone(&structured)),
|
||||
);
|
||||
|
||||
C4Fixture {
|
||||
@ -3527,25 +3550,34 @@ fn ask_fixture_with_record(
|
||||
let pty = FakePty::new(sid(777));
|
||||
let bus = SpyBus::default();
|
||||
let mailbox = Arc::new(TestMailbox::new());
|
||||
let structured = Arc::new(StructuredSessions::new());
|
||||
let completion_factory = completion_factory_for_contexts(&contexts, mailbox.completions());
|
||||
seed_structured_sessions(&structured, &contexts, mailbox.completions());
|
||||
|
||||
let create = Arc::new(CreateAgentFromScratch::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(bus.clone()),
|
||||
));
|
||||
let launch = Arc::new(LaunchAgent::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(FakeFs),
|
||||
Arc::new(pty.clone()),
|
||||
Arc::new(FakeSkills),
|
||||
Arc::clone(&sessions),
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
));
|
||||
let launch = Arc::new(
|
||||
LaunchAgent::new(
|
||||
Arc::new(contexts.clone()),
|
||||
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(FakeFs),
|
||||
Arc::new(pty.clone()),
|
||||
Arc::new(FakeSkills),
|
||||
Arc::clone(&sessions),
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
)
|
||||
.with_structured(
|
||||
Arc::new(completion_factory) as Arc<dyn AgentSessionFactory>,
|
||||
Arc::clone(&structured),
|
||||
),
|
||||
);
|
||||
let list = Arc::new(ListAgents::new(Arc::new(contexts.clone())));
|
||||
let close = Arc::new(CloseTerminal::new(
|
||||
Arc::new(pty.clone()),
|
||||
@ -3579,6 +3611,7 @@ fn ask_fixture_with_record(
|
||||
)
|
||||
.with_conversations(conversations)
|
||||
.with_events(Arc::new(bus.clone()))
|
||||
.with_structured(Arc::clone(&structured))
|
||||
.with_record_turn(provider, Arc::new(FixedClock(clock_ms)) as Arc<dyn Clock>),
|
||||
);
|
||||
|
||||
@ -3594,6 +3627,7 @@ fn ask_fixture_with_record(
|
||||
memories: Arc::new(HarvestMemories::default()),
|
||||
// Live-state likewise unwired here (RecordTurn-focused fixtures).
|
||||
live: Arc::new(RecordingLiveState::default()),
|
||||
structured,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3609,10 +3643,9 @@ async fn run_ask_roundtrip(
|
||||
let json = ask_json.to_owned();
|
||||
let ask = tokio::spawn(async move { svc.dispatch(&project(), cmd(&json)).await });
|
||||
await_until(|| fx.mailbox.pending(&reply_from) == 1).await;
|
||||
fx.service
|
||||
.dispatch(&project(), reply_cmd(reply_from, result))
|
||||
.await
|
||||
.expect("reply ok");
|
||||
fx.mailbox
|
||||
.resolve(reply_from, result.to_owned())
|
||||
.expect("structured completion ok");
|
||||
timeout(TEST_GUARD, ask)
|
||||
.await
|
||||
.expect("ask completes once replied")
|
||||
@ -4066,11 +4099,10 @@ async fn ask_warm_structured_target_reuses_session_without_relaunch() {
|
||||
|
||||
/// Régression live : une cible structurée peut déjà être vivante en PTY parce qu'elle
|
||||
/// a été ouverte depuis la surface humaine/menu historique. Le chemin `ask` headless
|
||||
/// doit alors libérer ce PTY puis créer la session structurée, au lieu de laisser le
|
||||
/// garde d'unicité de `LaunchAgent` rendre le PTY existant et finir par
|
||||
/// « aucune session structurée vivante après lancement ».
|
||||
/// doit alors créer une session structurée **sans fermer** ce PTY visible : les deux
|
||||
/// canaux d'entrée coexistent, et la délégation passe par `AgentSession::send`.
|
||||
#[tokio::test]
|
||||
async fn ask_structured_target_live_as_pty_migrates_to_headless_session() {
|
||||
async fn ask_structured_target_live_as_pty_keeps_visible_terminal() {
|
||||
let agent = scratch_agent(aid(1), "architect", "agents/architect.md");
|
||||
let fx = structured_ask_fixture(FakeContexts::with_agent(&agent, "# persona"));
|
||||
seed_live_pty(&fx.sessions, aid(1), sid(800));
|
||||
@ -4083,13 +4115,13 @@ async fn ask_structured_target_live_as_pty_migrates_to_headless_session() {
|
||||
|
||||
assert_eq!(out.reply.as_deref(), Some("structured: Analyse §17"));
|
||||
assert_eq!(fx.factory.start_count(), 1, "headless session started");
|
||||
assert_eq!(fx.pty.kills(), vec![sid(800)], "legacy PTY was stopped");
|
||||
assert!(fx.pty.kills().is_empty(), "visible PTY must not be stopped");
|
||||
assert!(
|
||||
fx.structured.session_for_agent(&aid(1)).is_some(),
|
||||
"target now has a structured session"
|
||||
);
|
||||
assert!(
|
||||
fx.sessions.session_for_agent(&aid(1)).is_none(),
|
||||
"target no longer has a PTY session"
|
||||
fx.sessions.session_for_agent(&aid(1)).is_some(),
|
||||
"target still has its visible PTY session"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user