From ec7271a47ebc2b6d16258359bd3803e5df6c3616 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Tue, 8 Sep 2026 23:09:21 +0800 Subject: [PATCH] =?UTF-8?q?test(sandbox):=20=E9=AA=8C=E8=AF=81=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E5=B7=A5=E5=85=B7=E6=88=AA=E6=AD=A2=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E4=B8=8E=E8=BF=9B=E7=A8=8B=E6=A0=91=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OpenNexus生产化实施进度-2026-09-08.md | 10 ++ frontend/src-tauri/src/extension_container.rs | 117 ++++++++++++++++++ frontend/src-tauri/src/extension_process.rs | 4 + .../tests/fixtures/sandbox_network_probe.rs | 12 +- 4 files changed, 142 insertions(+), 1 deletion(-) diff --git a/docs/development/OpenNexus生产化实施进度-2026-09-08.md b/docs/development/OpenNexus生产化实施进度-2026-09-08.md index a73bbeb..6530c5d 100644 --- a/docs/development/OpenNexus生产化实施进度-2026-09-08.md +++ b/docs/development/OpenNexus生产化实施进度-2026-09-08.md @@ -379,3 +379,13 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写 - 真实 AppContainer 等待探针验证:正常完成守卫后进程仍活着;使用测试内部 100 ms 期限时,在没有 check/finish 轮询驱动的情况下进程退出,随后 finish 明确返回超时;显式取消和未完成释放均在 5 秒内观察到进程退出。生产公开 API 不接受延长期限参数。本轮没有执行完整 60 秒墙钟超限、计时线程故障注入或高并发调用压力测试,不能据此独立通过 C-04。 - 48 项扩展回归通过、3 项 Job 辅助入口 ignored(由父测试执行),全目标 Clippy -D warnings 通过。日志 `.build/extension-deadline-tests.log`、`.build/extension-deadline-clippy.log`。 - 守卫仍需接到实际 MCP/扩展调用派发与结束路径;正式许可/包句柄绑定、broker、scratch 配额及完整验收尚未完成,整体生产化目标继续进行。 + + +## 验收增量:完整 60 秒工具预算、进程树清理与 Host 保存 + +- 新增长时显式验收测试 real_sixty_second_tool_deadline_kills_tree_and_host_can_save。测试探针在真实 AppContainer 内创建一个同受 Job 管理的子进程,两个进程自然等待均为 120 秒,避免与 60 秒预算自然退出混淆;先核对活动进程数为 2,再等待生产 start_tool_call 固定 60 秒预算。 +- 本次实际执行通过:计时开始 60.0048499 秒后 Job 活动进程数为 0,探针没有以自然结束码退出,finish 返回 EXTENSION_TOOL_DEADLINE_EXCEEDED。等待期间不通过 deadline.check/finish 驱动超时,终止由独立守卫完成。 +- 同一 Host 测试进程在工具运行期间使用正式 Workspace.write 保存笔记,清理后以 expected_hash 再次写入,关闭后重开验证内容一致。此证据覆盖 Host 库保存路径,尚不等于打包桌面 UI 端到端保存验收。 +- 长时测试以 --ignored --exact 显式执行,1 项通过;常规 AppContainer 5 项回归通过,长时条目在常规运行中保留 ignored 标记避免每次都额外等待一分钟。全目标 Clippy -D warnings 通过。日志 `.build/extension-deadline-sixty-seconds.log`、`.build/extension-deadline-container-regression.log`、`.build/extension-deadline-acceptance-clippy.log`。这补足上一条记录中未执行完整 60 秒和容器子树验证的缺口,不覆盖计时线程故障注入或高并发压力。 +- 远端只读复查:HTTP /health 200、/ready 503,SSH 22 连接仍在认证前返回空 banner;未修改远端配置,仍需要恢复就绪依赖与 SSH 可用入口。 +- C-04 的 CPU 持续超限、scratch 配额、broker 限流及完整桌面证据仍未齐备,不能将本测试扩大为 C-04 全项通过。整体生产化目标继续进行。 diff --git a/frontend/src-tauri/src/extension_container.rs b/frontend/src-tauri/src/extension_container.rs index b737f75..89e05f3 100644 --- a/frontend/src-tauri/src/extension_container.rs +++ b/frontend/src-tauri/src/extension_container.rs @@ -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(); + } } diff --git a/frontend/src-tauri/src/extension_process.rs b/frontend/src-tauri/src/extension_process.rs index 59c34a6..bbb65db 100644 --- a/frontend/src-tauri/src/extension_process.rs +++ b/frontend/src-tauri/src/extension_process.rs @@ -175,6 +175,10 @@ impl<'a> Suspended<'a> { } } impl Running<'_> { + #[cfg(test)] + pub(crate) fn active_test_processes(&self) -> Result { + 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 { diff --git a/frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs b/frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs index 9b0a67b..480015e 100644 --- a/frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs +++ b/frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs @@ -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") {