use super::*;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::time::Duration;
use trusty_memory::project_root::{write_project_pin, ProjectPin};
struct EnvGuard {
key: &'static str,
prev: Option<OsString>,
}
impl EnvGuard {
fn set<V: AsRef<OsStr>>(key: &'static str, value: V) -> Self {
let prev = std::env::var_os(key);
unsafe { std::env::set_var(key, value) };
Self { key, prev }
}
fn clear(key: &'static str) -> Self {
let prev = std::env::var_os(key);
unsafe { std::env::remove_var(key) };
Self { key, prev }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match self.prev.take() {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
}
const PIN_MAP_HANG_GUARD: Duration = Duration::from_secs(60);
#[serial_test::serial]
#[test]
fn spawn_startup_tasks_populates_pin_map() {
trusty_common::memory_core::retrieval::seed_shared_embedder_with_mock();
let tmp = tempfile::tempdir().expect("tempdir");
let search_root = tmp.path().join("Projects");
let project_dir = search_root.join("my-project");
fs::create_dir_all(&project_dir).expect("create project dir");
write_project_pin(&project_dir, &ProjectPin::new("my-palace".to_string())).expect("write pin");
let _home = EnvGuard::set("HOME", tmp.path());
let _enforcement = EnvGuard::set("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
let _data_dir = EnvGuard::clear(trusty_common::DATA_DIR_OVERRIDE_ENV);
let _update = EnvGuard::set(trusty_common::update::NO_UPDATE_CHECK_ENV, "1");
let state_root = tmp.path().join("data");
fs::create_dir_all(&state_root).expect("create data dir");
let (map_tx, map_rx) = std::sync::mpsc::channel();
let (found_tx, found_rx) = std::sync::mpsc::channel();
let runtime_thread = std::thread::Builder::new()
.name("pin-map-runtime".to_string())
.spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build test runtime");
let found = rt.block_on(async move {
let state = AppState::new(state_root);
let _ = map_tx.send(std::sync::Arc::clone(&state.pin_project_map));
spawn_startup_tasks(&state);
loop {
if let Some(entry) = state.pin_project_map.get("my-palace") {
return entry.clone();
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
});
rt.shutdown_timeout(Duration::ZERO);
let _ = found_tx.send(found);
})
.expect("spawn the test runtime thread");
let found = match found_rx.recv_timeout(PIN_MAP_HANG_GUARD) {
Ok(found) => found,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => std::panic::resume_unwind(
runtime_thread
.join()
.expect_err("the runtime thread dropped its sender without panicking"),
),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
let keys: Vec<String> = map_rx
.try_recv()
.map(|map| map.iter().map(|e| e.key().clone()).collect())
.unwrap_or_default();
panic!(
"#5937 hang guard: pin_project_map had no 'my-palace' {PIN_MAP_HANG_GUARD:?} \
after spawn_startup_tasks. Keys present: {keys:?}. HOME is now {:?}; \
the scan must read {:?}.",
std::env::var_os("HOME"),
tmp.path(),
);
}
};
let actual = fs::canonicalize(found).expect("canonicalize actual");
let expected = fs::canonicalize(&project_dir).expect("canonicalize expected");
assert_eq!(
actual, expected,
"pin_project_map entry must point to the project directory"
);
}
#[serial_test::serial]
#[test]
fn hydration_never_exceeds_the_startup_open_limit() {
let tmp = tempfile::tempdir().expect("tempdir");
let _home = EnvGuard::set("HOME", tmp.path());
let _enforcement = EnvGuard::set("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
let _update = EnvGuard::set(trusty_common::update::NO_UPDATE_CHECK_ENV, "1");
let state_root = tmp.path().join("data");
fs::create_dir_all(&state_root).expect("create data dir");
const PALACES: usize = 8;
const LIMIT: usize = 2;
for i in 0..PALACES {
let id = format!("palace-{i}");
let palace = trusty_common::memory_core::palace::Palace {
id: trusty_common::memory_core::palace::PalaceId::new(id.clone()),
name: id.clone(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state_root.join(&id),
};
trusty_common::memory_core::store::PalaceStore::save_palace(&palace)
.expect("seed palace metadata");
}
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()
.expect("build test runtime");
let (loaded, peak, limit) = rt.block_on(async {
let mut state = AppState::new(state_root);
state.startup_gate = trusty_memory::startup_budget::StartupOpenGate::with_limit(LIMIT);
let gate = state.startup_gate.clone();
let loaded = state
.load_palaces_from_disk()
.await
.expect("hydration must not fail");
(loaded, gate.peak_concurrent(), gate.limit())
});
rt.shutdown_timeout(Duration::ZERO);
assert_eq!(limit, LIMIT, "the pinned limit must survive into the gate");
assert_eq!(
loaded, PALACES,
"every seeded palace must still hydrate — the bound throttles, it does not skip"
);
assert!(
peak <= LIMIT,
"#7106: hydration held {peak} palaces open at once against a limit of {LIMIT}"
);
assert!(
peak > 0,
"every open must take a permit, or the ceiling governs nothing"
);
}