fix(backend): disponibilité session user + hand-off CLI↔TUI sans collision — #169 #170

This commit is contained in:
2026-08-06 20:00:36 +02:00
parent c50b6c5388
commit 292a81ee60
2 changed files with 114 additions and 46 deletions

View File

@ -50,8 +50,8 @@ use domain::{MemoryIndexEntry, NodeId, PtySize, SessionId, SessionKind, SkillId}
use uuid::Uuid;
use application::{
ChangeAgentProfile, ChangeAgentProfileInput, LaunchAgent, LaunchAgentInput, StructuredSessions,
TerminalSessions,
ChangeAgentProfile, ChangeAgentProfileInput, LaunchAgent, LaunchAgentInput,
StructuredRoutingMode, StructuredSessions, TerminalSessions,
};
// ---------------------------------------------------------------------------
@ -811,6 +811,14 @@ struct LaunchFixture {
/// Wires a `LaunchAgent.with_structured(...)` for a given profile + factory.
fn launch_fixture(profile: AgentProfile, factory: FakeFactory) -> LaunchFixture {
launch_fixture_with_mode(profile, factory, StructuredRoutingMode::RequireStructured)
}
fn launch_fixture_with_mode(
profile: AgentProfile,
factory: FakeFactory,
mode: StructuredRoutingMode,
) -> LaunchFixture {
let agent = scratch_agent(aid(1), "Backend", "agents/backend.md", profile.id);
let contexts = FakeContexts::with_agent(&agent, "# ctx body");
let profiles = FakeProfiles::new(vec![profile]);
@ -834,7 +842,8 @@ fn launch_fixture(profile: AgentProfile, factory: FakeFactory) -> LaunchFixture
Arc::new(FakeRecall),
None,
)
.with_structured(Arc::new(factory.clone()), Arc::clone(&structured));
.with_structured(Arc::new(factory.clone()), Arc::clone(&structured))
.with_structured_routing_mode(mode);
LaunchFixture {
launch: Arc::new(launch),
agent,
@ -1092,6 +1101,58 @@ async fn structured_relaunch_other_cell_with_conversation_id_rebinds() {
assert_eq!(desc.node_id, target);
}
/// #169/#170 — une session structurée vivante (chat/headless) ne doit pas bloquer
/// un lancement PTY/TUI utilisateur explicite du même agent. Les deux surfaces ont
/// des canaux distincts ; la garde singleton de la TUI reste portée par le registre
/// PTY, pas par `StructuredSessions`.
#[tokio::test]
async fn pty_launch_is_allowed_while_structured_session_is_live() {
let factory = FakeFactory::new(500, Some("engine-conv"));
let f = launch_fixture_with_mode(
structured_profile(pid(9)),
factory,
StructuredRoutingMode::HumanPtyFallback,
);
let chat_node = nid(1);
let mut chat = launch_input(f.agent.id);
chat.node_id = Some(chat_node);
chat.require_structured = true;
f.launch.execute(chat).await.expect("structured launch");
assert_eq!(f.factory.start_count(), 1);
assert_eq!(
f.structured
.session_id_for_agent_in_project(project_id(), &f.agent.id),
Some(sid(500))
);
let tui_node = nid(2);
let mut tui = launch_input(f.agent.id);
tui.node_id = Some(tui_node);
tui.require_structured = false;
let out = f.launch.execute(tui).await.expect("pty launch");
assert_eq!(
f.factory.start_count(),
1,
"the PTY launch must not re-enter the structured session factory"
);
assert_eq!(f.pty.spawn_count(), 1, "the user TUI gets its own PTY");
assert_eq!(out.session.id, sid(777));
assert!(out.structured.is_none(), "output describes the PTY surface");
assert_eq!(
f.sessions
.session_for_agent_in_project(project_id(), &f.agent.id),
Some(sid(777))
);
assert_eq!(
f.structured
.session_id_for_agent_in_project(project_id(), &f.agent.id),
Some(sid(500)),
"the structured conversation remains live and distinct"
);
}
/// Helper: executes a launch that is expected to be refused, returning the error.
async fn second_launch_err(f: &LaunchFixture, input: LaunchAgentInput) -> application::AppError {
f.launch