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.
This commit is contained in:
175
crates/infrastructure/tests/managed_temp.rs
Normal file
175
crates/infrastructure/tests/managed_temp.rs
Normal file
@ -0,0 +1,175 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user