fix(runtime): isolate agent state by project (#101)

This commit is contained in:
2026-07-25 22:14:02 +02:00
parent 6a87c4635f
commit 6e98fd89f7
52 changed files with 1894 additions and 805 deletions

View File

@ -555,7 +555,7 @@ impl AgentSessionFactory for FakeFactory {
// ---------------------------------------------------------------------------
use application::ProviderSessionProvider;
use domain::{ConversationId, ProviderSessionStore};
use domain::{ConversationId, ConversationParty, ProviderSessionStore};
/// In-memory [`ProviderSessionStore`] for the P8b launch tests: a
/// `(conversation, provider_id) → resumable_id` map, observable after the launch.
@ -659,9 +659,13 @@ fn nid(n: u128) -> NodeId {
NodeId::from_uuid(Uuid::from_u128(n))
}
fn project_id() -> ProjectId {
ProjectId::from_uuid(Uuid::from_u128(1000))
}
fn project() -> Project {
Project::new(
ProjectId::from_uuid(Uuid::from_u128(1000)),
project_id(),
"demo",
ProjectPath::new(ROOT).unwrap(),
RemoteRef::local(),
@ -734,7 +738,7 @@ fn seed_live_pty_session(
size,
);
session.status = domain::SessionStatus::Running;
sessions.insert(PtyHandle { session_id }, session);
sessions.insert_in_project(project_id(), PtyHandle { session_id }, session);
}
// ---------------------------------------------------------------------------
@ -815,12 +819,19 @@ async fn structured_launch_starts_session_registers_no_pty_spawn() {
// La session est enregistrée dans le registre structuré, retrouvable par agent.
let registered = f
.structured
.session_for_agent(&f.agent.id)
.session_for_agent_in_project(project_id(), &f.agent.id)
.expect("structured session registered");
assert_eq!(registered.id(), sid(500), "session id is the factory's");
assert_eq!(f.structured.node_for_agent(&f.agent.id), Some(nid(3)));
assert_eq!(
f.structured
.node_for_agent_in_project(project_id(), &f.agent.id),
Some(nid(3))
);
// Rien côté registre PTY.
assert!(f.sessions.session_for_agent(&f.agent.id).is_none());
assert!(f
.sessions
.session_for_agent_in_project(project_id(), &f.agent.id)
.is_none());
// AgentLaunched publié avec l'id de session structurée.
assert_eq!(
@ -845,11 +856,15 @@ async fn structured_launch_starts_session_registers_no_pty_spawn() {
SessionKind::Agent { agent_id } if agent_id == f.agent.id
));
// P8a (ARCHITECTURE §19.7) : la CELLULE porte l'**id de paire IdeA**, pas l'id
// moteur. Cellule neuve, lancement direct (aucun requester) ⇒ `pair(User, agent)`
// dérivé via `ConversationId::for_pair` = l'UUID de l'agent (`aid(1)`).
// moteur. Cellule neuve, lancement direct (aucun requester) ⇒ `pair(project, User, agent)`.
let expected_pair = ConversationId::for_project_pair(
project_id(),
ConversationParty::User,
ConversationParty::agent(f.agent.id),
);
assert_eq!(
out.assigned_conversation_id.as_deref(),
Some("00000000-0000-0000-0000-000000000001"),
Some(expected_pair.to_string().as_str()),
"cell carries the IdeA pair id (pivot logique), not the engine resumable"
);
// L'id de session MOTEUR (resumable provider) part dans le cache séparé.
@ -882,8 +897,15 @@ async fn non_structured_profile_takes_pty_path_unchanged() {
);
// Session côté registre PTY, rien côté structuré.
assert_eq!(f.sessions.session_for_agent(&f.agent.id), Some(sid(777)));
assert!(f.structured.session_for_agent(&f.agent.id).is_none());
assert_eq!(
f.sessions
.session_for_agent_in_project(project_id(), &f.agent.id),
Some(sid(777))
);
assert!(f
.structured
.session_for_agent_in_project(project_id(), &f.agent.id)
.is_none());
// output.structured = None ; session PTY classique.
assert!(
@ -937,7 +959,8 @@ async fn structured_launch_new_in_other_cell_refuses_when_live_elsewhere() {
"still a single live structured session"
);
assert_eq!(
f.structured.node_for_agent(&f.agent.id),
f.structured
.node_for_agent_in_project(project_id(), &f.agent.id),
Some(host),
"session stays pinned on its host node A"
);
@ -964,7 +987,11 @@ async fn structured_relaunch_same_node_rebinds_no_second_start() {
assert_eq!(f.factory.start_count(), 1, "no second factory.start");
assert_eq!(f.pty.spawn_count(), 0, "still no pty spawn");
assert_eq!(f.structured.len(), 1, "single live structured session");
assert_eq!(f.structured.node_for_agent(&f.agent.id), Some(host));
assert_eq!(
f.structured
.node_for_agent_in_project(project_id(), &f.agent.id),
Some(host)
);
let desc = out.structured.expect("descriptor on rebind");
assert_eq!(desc.session_id, sid(500), "same live session id");
assert_eq!(desc.node_id, host);
@ -1002,7 +1029,11 @@ async fn structured_relaunch_other_cell_with_conversation_id_rebinds() {
1,
"still a single live structured session"
);
assert_eq!(f.structured.node_for_agent(&f.agent.id), Some(target));
assert_eq!(
f.structured
.node_for_agent_in_project(project_id(), &f.agent.id),
Some(target)
);
let desc = out.structured.expect("descriptor on rebind");
assert_eq!(desc.session_id, sid(500), "same live session id");
assert_eq!(desc.node_id, target);
@ -1130,7 +1161,8 @@ async fn swap_structured_live_session_shuts_down_then_relaunches() {
.start(&profile, &ctx, &cwd, &SessionPlan::None, None, &[], None)
.await
.expect("seed structured session");
f.structured.insert(session, agent.id, host);
f.structured
.insert_in_project(project_id(), session, agent.id, host);
}
// La factory a maintenant été appelée 1 fois (le seed) ; reset logique : on
// comptera les start APRÈS, donc on mémorise la base.
@ -1182,8 +1214,16 @@ async fn swap_structured_live_session_shuts_down_then_relaunches() {
relaunched.node_id, host,
"relaunch reopens in the same cell"
);
assert_eq!(f.structured.session_id_for_agent(&agent.id), Some(sid(601)));
assert_eq!(f.structured.node_for_agent(&agent.id), Some(host));
assert_eq!(
f.structured
.session_id_for_agent_in_project(project_id(), &agent.id),
Some(sid(601))
);
assert_eq!(
f.structured
.node_for_agent_in_project(project_id(), &agent.id),
Some(host)
);
assert_eq!(
f.structured.len(),
1,
@ -1230,8 +1270,15 @@ async fn swap_pty_live_session_keeps_a1_kill_behaviour() {
);
assert_eq!(relaunched.id, sid(777));
// The relaunched session lives in the PTY registry, not the structured one.
assert_eq!(f.sessions.session_for_agent(&agent.id), Some(sid(777)));
assert!(f.structured.session_for_agent(&agent.id).is_none());
assert_eq!(
f.sessions
.session_for_agent_in_project(project_id(), &agent.id),
Some(sid(777))
);
assert!(f
.structured
.session_for_agent_in_project(project_id(), &agent.id)
.is_none());
assert_eq!(f.contexts.profile_of(&agent.id), Some(pid(2)));
}