test(sandbox): 验证完整工具截止时间与进程树清理
This commit is contained in:
@@ -825,4 +825,121 @@ mod tests {
|
||||
drop(root);
|
||||
profile.remove().unwrap();
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "production 60-second wall-clock and process-tree acceptance; run explicitly"]
|
||||
fn real_sixty_second_tool_deadline_kills_tree_and_host_can_save() {
|
||||
use std::{
|
||||
os::windows::fs::OpenOptionsExt,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use windows_sys::Win32::Storage::FileSystem::*;
|
||||
let profile = Profile::create().unwrap();
|
||||
let package = tempfile::tempdir().unwrap();
|
||||
let executable = package.path().join("network-probe.exe");
|
||||
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/sandbox_network_probe.rs");
|
||||
let compile = std::process::Command::new("rustc")
|
||||
.arg("--edition=2021")
|
||||
.arg(&fixture)
|
||||
.arg("-o")
|
||||
.arg(&executable)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
compile.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&compile.stderr)
|
||||
);
|
||||
let open = |path: &std::path::Path| {
|
||||
std::fs::OpenOptions::new()
|
||||
.access_mode(READ_CONTROL | WRITE_DAC)
|
||||
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
|
||||
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
|
||||
.open(path)
|
||||
.unwrap()
|
||||
};
|
||||
let root = open(package.path());
|
||||
let entry = open(&executable);
|
||||
profile.grant_package_read_execute(&root).unwrap();
|
||||
profile.grant_package_read_execute(&entry).unwrap();
|
||||
|
||||
let folder = profile.folder().unwrap();
|
||||
let system = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap());
|
||||
let data = crate::extension_launch_data::LaunchData::new(
|
||||
&executable,
|
||||
&["wait_tree".to_owned()],
|
||||
&system,
|
||||
&folder,
|
||||
&folder.join("Temp"),
|
||||
&std::collections::BTreeMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let suspended =
|
||||
crate::extension_process::Suspended::create(&profile, &executable, data).unwrap();
|
||||
let running = unsafe { suspended.resume().unwrap() };
|
||||
let start = Instant::now();
|
||||
let deadline = running.start_tool_call().unwrap();
|
||||
while running.active_test_processes().unwrap() != 2
|
||||
&& start.elapsed() < Duration::from_secs(5)
|
||||
{
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert_eq!(
|
||||
running.active_test_processes().unwrap(),
|
||||
2,
|
||||
"container child did not start"
|
||||
);
|
||||
let vault = tempfile::tempdir().unwrap();
|
||||
let mut workspace = crate::workspace::Workspace::open(vault.path()).unwrap();
|
||||
let before = workspace
|
||||
.write("deadline.md", "", b"during tool call", "local")
|
||||
.unwrap();
|
||||
eprintln!("production deadline: two managed processes; Host save succeeded; waiting 60-second budget");
|
||||
let exit = running
|
||||
.wait(Duration::from_secs(60))
|
||||
.unwrap()
|
||||
.or_else(|| running.wait(Duration::from_secs(5)).unwrap())
|
||||
.unwrap();
|
||||
assert_ne!(
|
||||
exit, 84,
|
||||
"probe ended naturally before the watchdog killed it"
|
||||
);
|
||||
while running.active_test_processes().unwrap() != 0
|
||||
&& start.elapsed() < Duration::from_secs(70)
|
||||
{
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert_eq!(running.active_test_processes().unwrap(), 0);
|
||||
let elapsed = start.elapsed();
|
||||
assert!(
|
||||
elapsed >= Duration::from_secs(60) && elapsed < Duration::from_secs(70),
|
||||
"{elapsed:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
deadline.finish().unwrap_err().code,
|
||||
"EXTENSION_TOOL_DEADLINE_EXCEEDED"
|
||||
);
|
||||
workspace
|
||||
.write(
|
||||
"deadline.md",
|
||||
&before.hash,
|
||||
b"after process-tree cleanup",
|
||||
"local",
|
||||
)
|
||||
.unwrap();
|
||||
drop(workspace);
|
||||
let mut reopened = crate::workspace::Workspace::open(vault.path()).unwrap();
|
||||
assert_eq!(
|
||||
reopened.read("deadline.md").unwrap().content,
|
||||
"after process-tree cleanup"
|
||||
);
|
||||
eprintln!(
|
||||
"production deadline: tree empty after {elapsed:?}; Host save and reopen succeeded"
|
||||
);
|
||||
drop(reopened);
|
||||
drop(running);
|
||||
drop(entry);
|
||||
drop(root);
|
||||
profile.remove().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,10 @@ impl<'a> Suspended<'a> {
|
||||
}
|
||||
}
|
||||
impl Running<'_> {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn active_test_processes(&self) -> Result<u32> {
|
||||
self.0.job.active_processes()
|
||||
}
|
||||
/// Arm before dispatching a tool request; finish after receiving its result.
|
||||
/// Failure to arm must prevent dispatch. This does not time server lifetime.
|
||||
pub fn start_tool_call(&self) -> Result<crate::extension_deadline::ToolDeadline> {
|
||||
|
||||
+11
-1
@@ -3,8 +3,18 @@ use std::net::{SocketAddr, TcpStream, UdpSocket};
|
||||
use std::time::Duration;
|
||||
fn main() {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
if args.get(1).is_some_and(|s| s == "wait_tree") {
|
||||
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.arg("wait")
|
||||
.spawn()
|
||||
.unwrap();
|
||||
std::thread::sleep(Duration::from_secs(120));
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
std::process::exit(84);
|
||||
}
|
||||
if args.get(1).is_some_and(|s| s == "wait") {
|
||||
std::thread::sleep(Duration::from_secs(60));
|
||||
std::thread::sleep(Duration::from_secs(120));
|
||||
std::process::exit(84);
|
||||
}
|
||||
if args.get(1).is_some_and(|s| s == "launch") {
|
||||
|
||||
Reference in New Issue
Block a user