fix(runtime): isolation d'usage multi-projet #107
Implémentation du garde ModelServerProjectUseGuard pour séparer les retours des agents entre projets concurrents. - Garde RAII par LocalModelServerId partagé entre projets - Refus inter-projets concurrent via model_server_in_use - Partage intra-projet conservé avec refcount - Libération automatique à la fermeture/erreur de session Tests de validation: rejet projet distinct, compteur refs, libération retrait
This commit is contained in:
@ -8,7 +8,7 @@ use async_trait::async_trait;
|
||||
|
||||
use application::{
|
||||
DeleteModelServer, DeleteModelServerInput, EnsureLocalModelServer, EnsureLocalModelServerInput,
|
||||
ModelServerReadinessPolicy,
|
||||
ModelServerReadinessPolicy, TerminalSessions,
|
||||
};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::model_server::{
|
||||
@ -23,12 +23,19 @@ use domain::ports::{
|
||||
ProcessStatus, ProfileStore, RemotePath, SpawnSpec, StoreError,
|
||||
};
|
||||
use domain::profile::{AgentProfile, ContextInjection, OpenCodeConfig, StructuredAdapter};
|
||||
use domain::{LocalModelServerId, ProfileId, ProjectPath};
|
||||
use domain::{
|
||||
AgentId, LocalModelServerId, NodeId, ProfileId, ProjectId, ProjectPath, PtySize, SessionId,
|
||||
SessionKind, SessionStatus, TerminalSession,
|
||||
};
|
||||
|
||||
fn sid(n: u128) -> LocalModelServerId {
|
||||
LocalModelServerId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
fn pid(n: u128) -> ProjectId {
|
||||
ProjectId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
fn config(
|
||||
id: LocalModelServerId,
|
||||
port: u16,
|
||||
@ -611,6 +618,83 @@ async fn concurrent_ensure_same_server_shares_one_start_attempt() {
|
||||
assert_eq!(starting_events, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_model_server_use_rejects_distinct_project_until_guard_is_released() {
|
||||
let usecase = ensure(
|
||||
Arc::new(FakeRegistry::default()),
|
||||
Arc::new(FakeProbe::new(Vec::new())),
|
||||
Arc::new(FakeProcess::default()),
|
||||
Arc::new(FakeFs::default()),
|
||||
Arc::new(FakeEvents::default()),
|
||||
);
|
||||
|
||||
let guard = usecase.acquire_project_use(sid(30), pid(1)).unwrap();
|
||||
let err = usecase.acquire_project_use(sid(30), pid(2)).unwrap_err();
|
||||
|
||||
assert_eq!(err.code(), "MODEL_SERVER");
|
||||
assert!(err.to_string().contains("model_server_in_use"));
|
||||
|
||||
drop(guard);
|
||||
assert!(usecase.acquire_project_use(sid(30), pid(2)).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_model_server_use_allows_same_project_with_refcount() {
|
||||
let usecase = ensure(
|
||||
Arc::new(FakeRegistry::default()),
|
||||
Arc::new(FakeProbe::new(Vec::new())),
|
||||
Arc::new(FakeProcess::default()),
|
||||
Arc::new(FakeFs::default()),
|
||||
Arc::new(FakeEvents::default()),
|
||||
);
|
||||
|
||||
let first = usecase.acquire_project_use(sid(31), pid(1)).unwrap();
|
||||
let second = usecase.acquire_project_use(sid(31), pid(1)).unwrap();
|
||||
|
||||
assert!(usecase.acquire_project_use(sid(31), pid(2)).is_err());
|
||||
drop(first);
|
||||
assert!(usecase.acquire_project_use(sid(31), pid(2)).is_err());
|
||||
drop(second);
|
||||
assert!(usecase.acquire_project_use(sid(31), pid(2)).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_session_removal_releases_local_model_server_use_guard() {
|
||||
let usecase = ensure(
|
||||
Arc::new(FakeRegistry::default()),
|
||||
Arc::new(FakeProbe::new(Vec::new())),
|
||||
Arc::new(FakeProcess::default()),
|
||||
Arc::new(FakeFs::default()),
|
||||
Arc::new(FakeEvents::default()),
|
||||
);
|
||||
let project = pid(1);
|
||||
let server_id = sid(32);
|
||||
let session_id = SessionId::from_uuid(uuid::Uuid::from_u128(33));
|
||||
let guard = usecase.acquire_project_use(server_id, project).unwrap();
|
||||
let sessions = TerminalSessions::new();
|
||||
let mut session = TerminalSession::starting(
|
||||
session_id,
|
||||
NodeId::from_uuid(uuid::Uuid::from_u128(34)),
|
||||
ProjectPath::new("/tmp/project-a").unwrap(),
|
||||
SessionKind::Agent {
|
||||
agent_id: AgentId::from_uuid(uuid::Uuid::from_u128(35)),
|
||||
},
|
||||
PtySize { rows: 24, cols: 80 },
|
||||
);
|
||||
session.status = SessionStatus::Running;
|
||||
|
||||
sessions.insert_in_project_with_model_server_guard(
|
||||
project,
|
||||
domain::ports::PtyHandle { session_id },
|
||||
session,
|
||||
Some(guard),
|
||||
);
|
||||
assert!(usecase.acquire_project_use(server_id, pid(2)).is_err());
|
||||
|
||||
sessions.remove(&session_id);
|
||||
assert!(usecase.acquire_project_use(server_id, pid(2)).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_model_path_is_path_not_accessible() {
|
||||
let registry = Arc::new(FakeRegistry::default());
|
||||
|
||||
Reference in New Issue
Block a user