Files
IdeA/crates/infrastructure/tests/managed_temp.rs
Blomios fdca55ecd4 feat(infra): tmp partagé inter-projets géré par IdeA (handles opaques, leases, GC, projection sandbox) — #189 (QA verte)
- Domaine: port ManagedTempStore + modèle (handle opaque, kind Directory/File, état Active/Released, allocation/lease, expiry).
- Application: use cases Allocate/Resolve/Release/GcManagedTemp, horodatés via Clock.
- Infrastructure: impl FS FsManagedTempStore sous app_data_dir/managed-tmp (root global inter-projets, métadonnées par handle, GC des leases expirés/libérés).
- Exposition MCP contrôlée (ManagedTempToolProvider, catalogue) + câblage sandbox: les agents résolvent un handle au lieu d'inventer un path libre.
- Tests: application/infrastructure managed_temp.
2026-08-13 13:34:33 +02:00

176 lines
5.0 KiB
Rust

use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use domain::ports::ManagedTempStore;
use domain::{
ManagedTempAllocation, ManagedTempError, ManagedTempHandle, ManagedTempKind, ManagedTempState,
ProjectId,
};
use infrastructure::FsManagedTempStore;
use uuid::Uuid;
fn temp_app_data(tag: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!(
"idea-managed-temp-{tag}-{}-{unique}",
std::process::id()
))
}
fn handle(value: &str) -> ManagedTempHandle {
ManagedTempHandle::new(value).unwrap()
}
fn allocation(
handle: ManagedTempHandle,
kind: ManagedTempKind,
purpose: &str,
created_at_ms: u64,
expires_at_ms: Option<u64>,
) -> ManagedTempAllocation {
ManagedTempAllocation {
handle,
kind,
owner_project_id: Some(ProjectId::from_uuid(Uuid::from_u128(42))),
purpose: purpose.to_owned(),
created_at_ms,
expires_at_ms,
}
}
fn assert_under(path: &Path, root: &Path) {
assert!(
path.starts_with(root),
"{} should be under {}",
path.display(),
root.display()
);
}
#[tokio::test]
async fn allocate_resolve_release_and_gc_directory() {
let app_data = temp_app_data("dir");
let store = FsManagedTempStore::new(&app_data);
let temp_root = std::env::temp_dir();
assert_eq!(store.root(), app_data.join("managed-tmp"));
assert_ne!(store.root(), temp_root.as_path());
let lease = store
.allocate(allocation(
handle("alloc_dir_1"),
ManagedTempKind::Directory,
"scratch",
1_000,
Some(2_000),
))
.await
.unwrap();
let path = PathBuf::from(lease.path.as_str());
assert_eq!(lease.state, ManagedTempState::Active);
assert_eq!(lease.last_access_at_ms, 1_000);
assert_under(&path, store.root());
assert!(tokio::fs::metadata(&path).await.unwrap().is_dir());
assert!(
tokio::fs::metadata(store.root().join("alloc_dir_1").join("metadata.json"))
.await
.is_ok()
);
let resolved = store.resolve(&handle("alloc_dir_1"), 1_500).await.unwrap();
assert_eq!(resolved.state, ManagedTempState::Active);
assert_eq!(resolved.last_access_at_ms, 1_500);
let released = store.release(&handle("alloc_dir_1"), 1_600).await.unwrap();
assert_eq!(released.state, ManagedTempState::Released);
assert_eq!(released.released_at_ms, Some(1_600));
assert!(matches!(
store.resolve(&handle("alloc_dir_1"), 1_700).await,
Err(ManagedTempError::NotFound)
));
let report = store.gc(1_700).await.unwrap();
assert_eq!(report.deleted, 1);
assert!(tokio::fs::metadata(store.root().join("alloc_dir_1"))
.await
.is_err());
let _ = tokio::fs::remove_dir_all(app_data).await;
}
#[tokio::test]
async fn expired_file_is_not_resolved_and_is_gc_eligible() {
let app_data = temp_app_data("file");
let store = FsManagedTempStore::new(&app_data);
let lease = store
.allocate(allocation(
handle("alloc_file_1"),
ManagedTempKind::File,
"../../must-not-affect-path",
1_000,
Some(1_100),
))
.await
.unwrap();
let path = PathBuf::from(lease.path.as_str());
assert_under(&path, store.root());
assert_eq!(path.file_name().unwrap(), "data");
assert!(tokio::fs::metadata(&path).await.unwrap().is_file());
assert!(matches!(
store.resolve(&handle("alloc_file_1"), 1_200).await,
Err(ManagedTempError::NotFound)
));
assert_eq!(store.gc(1_200).await.unwrap().deleted, 1);
assert!(tokio::fs::metadata(path).await.is_err());
let _ = tokio::fs::remove_dir_all(app_data).await;
}
#[tokio::test]
async fn invalid_path_like_handles_are_rejected_before_filesystem_resolution() {
assert!(ManagedTempHandle::new("../escape").is_err());
assert!(ManagedTempHandle::new("a/b").is_err());
assert!(ManagedTempHandle::new("..\\escape").is_err());
assert!(ManagedTempHandle::new("").is_err());
}
#[tokio::test]
async fn handle_collision_does_not_reuse_existing_allocation() {
let app_data = temp_app_data("collision");
let store = FsManagedTempStore::new(&app_data);
store
.allocate(allocation(
handle("same_handle"),
ManagedTempKind::Directory,
"first",
1_000,
None,
))
.await
.unwrap();
assert!(matches!(
store
.allocate(allocation(
handle("same_handle"),
ManagedTempKind::Directory,
"second",
1_100,
None,
))
.await,
Err(ManagedTempError::Store(_))
));
let resolved = store.resolve(&handle("same_handle"), 1_200).await.unwrap();
assert_eq!(resolved.purpose, "first");
let _ = tokio::fs::remove_dir_all(app_data).await;
}