use std::sync::Arc;
use astrid_core::dirs::AstridHome;
use astrid_core::groups::BUILTIN_ADMIN;
use astrid_core::principal::PrincipalId;
use astrid_core::profile::Quotas;
use astrid_events::kernel_api::{AdminRequestKind, AdminResponseBody, ResourceUsage};
use tempfile::TempDir;
use super::handlers;
use crate::Kernel;
async fn fixture() -> (TempDir, Arc<Kernel>) {
let dir = tempfile::tempdir().expect("tempdir");
let home = AstridHome::from_path(dir.path());
let kernel = crate::test_kernel_with_home(home).await;
(dir, kernel)
}
fn pid(name: &str) -> PrincipalId {
PrincipalId::new(name).unwrap()
}
async fn create_agent(kernel: &Arc<Kernel>, name: &str, groups: Vec<String>) {
handlers::dispatch(
kernel,
&PrincipalId::default(),
AdminRequestKind::AgentCreate {
name: name.into(),
groups,
grants: Vec::new(),
},
)
.await;
}
async fn usage_get(kernel: &Arc<Kernel>, principal: &PrincipalId) -> ResourceUsage {
let res = handlers::dispatch(
kernel,
&PrincipalId::default(),
AdminRequestKind::UsageGet {
principal: principal.clone(),
},
)
.await;
match res {
AdminResponseBody::Usage(u) => u,
other => panic!("expected Usage, got: {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn usage_get_on_nonexistent_principal_is_rejected() {
let (_dir, kernel) = fixture().await;
let res = handlers::dispatch(
&kernel,
&PrincipalId::default(),
AdminRequestKind::UsageGet {
principal: pid("typo_principal"),
},
)
.await;
match res {
AdminResponseBody::Error(msg) => assert!(
msg.contains("does not exist"),
"expected phantom-principal rejection, got: {msg}"
),
other => panic!("expected Error, got: {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn usage_get_reports_real_fuel_total() {
let (_dir, kernel) = fixture().await;
create_agent(&kernel, "alice", Vec::new()).await;
assert_eq!(
usage_get(&kernel, &pid("alice"))
.await
.cpu_fuel_consumed_total,
0,
"an uncharged principal reads 0, distinct from the old hard-coded stub"
);
kernel.fuel_ledger.charge(&pid("alice"), 1_000);
kernel.fuel_ledger.charge(&pid("alice"), 234);
assert_eq!(
usage_get(&kernel, &pid("alice"))
.await
.cpu_fuel_consumed_total,
1_234,
"usage.get must surface the live ledger total, not the stub 0"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn usage_get_exempt_matches_capability_holders() {
let (_dir, kernel) = fixture().await;
create_agent(&kernel, "boss", vec![BUILTIN_ADMIN.into()]).await;
assert!(
usage_get(&kernel, &pid("boss")).await.exempt,
"admin (holds `*`) must report exempt=true"
);
create_agent(&kernel, "worker", Vec::new()).await;
assert!(
!usage_get(&kernel, &pid("worker")).await.exempt,
"a plain agent holds no exemption cap → exempt=false"
);
handlers::dispatch(
&kernel,
&PrincipalId::default(),
AdminRequestKind::CapsGrant {
principal: pid("worker"),
capabilities: vec![astrid_core::CAP_RESOURCES_UNBOUNDED.into()],
unsafe_admin: false,
},
)
.await;
assert!(
usage_get(&kernel, &pid("worker")).await.exempt,
"system:resources:unbounded grant must report exempt=true"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn usage_get_ceilings_match_profile_quotas() {
let (_dir, kernel) = fixture().await;
create_agent(&kernel, "carol", Vec::new()).await;
let quotas = Quotas {
max_cpu_fuel_per_sec: 7_000_000,
max_memory_bytes: 32 * 1024 * 1024,
..Quotas::default()
};
handlers::dispatch(
&kernel,
&PrincipalId::default(),
AdminRequestKind::QuotaSet {
principal: pid("carol"),
quotas: quotas.clone(),
},
)
.await;
let usage = usage_get(&kernel, &pid("carol")).await;
assert_eq!(usage.principal, pid("carol"));
assert_eq!(usage.cpu_fuel_per_sec_limit, quotas.max_cpu_fuel_per_sec);
assert_eq!(
usage.memory_bytes_limit_per_instance,
quotas.max_memory_bytes
);
assert_eq!(usage.memory_bytes_current_total, None);
}