From 2d228cd42455d23e99e16e769aede1afe9ad1330 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Wed, 9 Sep 2026 05:41:51 +0800 Subject: [PATCH] =?UTF-8?q?test(extensions):=20=E9=AA=8C=E8=AF=81=E5=AE=B9?= =?UTF-8?q?=E5=99=A8=20CPU=20=E5=A4=B1=E8=B4=A5=E4=B8=8E=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2?= 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 | 48 +++++++++- frontend/src-tauri/src/extension_instance.rs | 89 +++++++++++++++++++ .../tests/fixtures/sandbox_network_probe.rs | 20 ++++- 4 files changed, 162 insertions(+), 5 deletions(-) diff --git a/docs/development/OpenNexus生产化实施进度-2026-09-08.md b/docs/development/OpenNexus生产化实施进度-2026-09-08.md index dd4241e..3b00a4f 100644 --- a/docs/development/OpenNexus生产化实施进度-2026-09-08.md +++ b/docs/development/OpenNexus生产化实施进度-2026-09-08.md @@ -554,3 +554,13 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写 - 默认扩展回归首次发现旧 CPU 控制标志测试仍只期望 ENABLE|HARD_CAP,已更新为同时核验新 NOTIFY 标志;未放宽资源限额值。scratch 硬配额、CPU 连续时限严格证明、全部限额清理矩阵、Host 实际产品路由及其他生产验收继续未完成,第三方执行能力仍禁用。 - 最终默认扩展回归 68 通过、7 ignored(长时验收与辅助进程入口),日志 `.build/extension-cpu-regression.log`;显式 CPU 压力/空闲对照测试通过,本次从恢复附近的计时点到检测为 15.7348341 秒,整个测试 15.90 秒,日志 `.build/extension-cpu-enforcement.log`。此前简化压力测试为 12.1788269 秒;这些差异进一步表明不能宣称精确 10 秒触发。记录中的“Job empty after”还包含后续 Host 保存/重开工作,不能作为纯终止 API 延迟的独立测量。 - desktop 全目标 Clippy -D warnings 通过,日志 `.build/extension-cpu-clippy.log`。本轮改动集中于原生 Job 与进程资源状态传播,未重新声称上一轮的 Rust 全目标、前端、Python 和远端验收均对当前版本完成。 + + +## 增量:AppContainer/MCP 的 CPU 故障传播与替换验收 + +- 扩展原生探针新增 mcp_cpu 工具模式,完成 MCP 初始化、工具目录和调用后启动同包 cpu_burn 后代进程,由后代的 8 个忙线程产生 CPU 压力。该负载处于真实零能力 AppContainer 和原生 Job 中,不是通过模拟资源状态触发错误。 +- 显式原生协议测试核验 tools/call 得到 EXTENSION_RESOURCE_CPU_EXCEEDED,Running 资源状态一致,失败会话拒绝目录刷新,关闭后 Job 的 ActiveProcesses=0;Host 写入并重开读取笔记成功。本次工具调用到错误约 12.7969873 秒,整项测试 30.86 秒,通过日志 `.build/extension-cpu-appcontainer.log`。 +- 显式后台注册表测试核验调用回执和终态均保留 CPU 错误、终态 Failed、工具目录清空、旧 Endpoint 拒绝请求、Job 清空、包入口与根目录 ACL 恢复原基线。注册项回收后以同包启动新实例,其实例 UUID 改变,实际 MCP 工具调用成功,随后停止回收。本次调用到错误约 9.7654748 秒,整项测试 12.66 秒,日志 `.build/extension-cpu-registry.log`。 +- 这些证据补齐了 CPU 故障通过原生会话和后台注册表传播的路径,不能将 9.77/12.80 秒的结果改称严格连续 10 秒的计时证明;窗口语义差异仍保留。实际产品启用、资源/网络/文件攻击全矩阵和完整 C-04 验收仍未完成。 +- 默认扩展回归 68 通过、9 ignored,新增两项显式长时测试已单独执行通过,其余 ignored 仍有其他长时验收及辅助进程入口;desktop 全目标 Clippy -D warnings 通过。日志 `.build/extension-cpu-integration-regression.log`、`.build/extension-cpu-integration-clippy.log`。 +- 同轮只读复查测试 Sync:HTTP /health=200,/ready=503,响应错误 DEPENDENCY_UNAVAILABLE;SSH 22 可以建立 TCP,但在认证前读到空 banner,连接被对端关闭。未尝试写入部署,也未将 /health 成功当作服务就绪。证据 `.build/sync-cpu-integration-readiness.json`;该远端障碍不阻止继续本地生产化工作。 diff --git a/frontend/src-tauri/src/extension_container.rs b/frontend/src-tauri/src/extension_container.rs index d45ab02..3024e02 100644 --- a/frontend/src-tauri/src/extension_container.rs +++ b/frontend/src-tauri/src/extension_container.rs @@ -681,15 +681,20 @@ mod tests { } #[test] fn real_container_cannot_reach_ipv4_or_ipv6_loopback_listeners() { - real_native_protocol_probes(false); + real_native_protocol_probes(false, false); } #[cfg(feature = "desktop")] #[test] #[ignore = "real MCP tools/call 60-second deadline acceptance; run explicitly"] fn real_mcp_tool_deadline_terminates_hung_server_and_host_can_save() { - real_native_protocol_probes(true); + real_native_protocol_probes(true, false); } - fn real_native_protocol_probes(_mcp_deadline: bool) { + #[test] + #[ignore = "real AppContainer MCP descendant CPU pressure; run explicitly"] + fn real_mcp_cpu_pressure_reports_resource_error_and_reaps_tree() { + real_native_protocol_probes(false, true); + } + fn real_native_protocol_probes(_mcp_deadline: bool, cpu: bool) { use std::{ net::{TcpListener, UdpSocket}, os::windows::fs::OpenOptionsExt, @@ -966,6 +971,9 @@ mod tests { if _mcp_deadline { mcp_modes.push("mcp_deadline"); } + if cpu { + mcp_modes.push("mcp_cpu"); + } for mode in mcp_modes { use std::sync::{ atomic::{AtomicBool, Ordering}, @@ -1084,6 +1092,40 @@ mod tests { ); assert!(session.take_tools_changed()); } + "mcp_cpu" => { + assert_eq!(result.unwrap_err().code, "EXTENSION_RESOURCE_CPU_EXCEEDED"); + let elapsed = tool_started.elapsed(); + assert!(elapsed < std::time::Duration::from_secs(20)); + eprintln!("AppContainer MCP descendant CPU error: {elapsed:?}"); + assert_eq!( + session.refresh_tools(&cancel).err().unwrap().code, + "EXTENSION_MCP_SESSION_FAILED" + ); + assert_eq!( + running.check_authorization().unwrap_err().code, + "EXTENSION_RESOURCE_CPU_EXCEEDED" + ); + let vault = tempfile::tempdir().unwrap(); + let mut workspace = + crate::workspace::Workspace::open(vault.path()).unwrap(); + workspace + .write( + "cpu-recovery.md", + "", + b"Host saved after descendant CPU termination", + "local", + ) + .unwrap(); + drop(workspace); + assert_eq!( + crate::workspace::Workspace::open(vault.path()) + .unwrap() + .read("cpu-recovery.md") + .unwrap() + .content, + "Host saved after descendant CPU termination" + ); + } "mcp_deadline" => { assert_eq!( result.unwrap_err().code, diff --git a/frontend/src-tauri/src/extension_instance.rs b/frontend/src-tauri/src/extension_instance.rs index f75a5b1..2aacdde 100644 --- a/frontend/src-tauri/src/extension_instance.rs +++ b/frontend/src-tauri/src/extension_instance.rs @@ -511,6 +511,14 @@ mod tests { } #[test] fn native_worker_routes_reviews_cancels_calls_and_reaps_generations() { + native_worker_lifecycle(false); + } + #[test] + #[ignore = "real background MCP descendant CPU exhaustion and restart; run explicitly"] + fn native_cpu_failure_is_reported_reaped_and_replacement_can_start() { + native_worker_lifecycle(true); + } + fn native_worker_lifecycle(cpu: bool) { let temp = tempfile::tempdir().unwrap(); let package = temp.path().join("package"); std::fs::create_dir(&package).unwrap(); @@ -724,6 +732,87 @@ mod tests { .unwrap(), 0 ); + if cpu { + let exhausted = unsafe { registry.start(make("mcp_cpu")) }.unwrap(); + wait_for(|| exhausted.snapshot().status != Status::Starting); + assert_eq!(exhausted.snapshot().status, Status::Ready); + let old_identity = + serde_json::to_value(exhausted.snapshot().identity.unwrap()).unwrap(); + let review = exhausted + .review("echo".into(), json!({})) + .unwrap() + .wait(Duration::from_secs(5)) + .unwrap(); + let started = Instant::now(); + assert_eq!( + exhausted + .invoke_confirmed(review.review_id) + .unwrap() + .wait(Duration::from_secs(20)) + .unwrap_err() + .code, + "EXTENSION_RESOURCE_CPU_EXCEEDED" + ); + eprintln!("background CPU call error after {:?}", started.elapsed()); + wait_for(|| { + registry.reap(); + registry.entries.is_empty() + }); + assert_eq!(exhausted.snapshot().status, Status::Failed); + assert_eq!( + exhausted.snapshot().error.as_deref(), + Some("EXTENSION_RESOURCE_CPU_EXCEEDED") + ); + assert_eq!(exhausted.snapshot().tool_count, 0); + assert!(exhausted.review("echo".into(), json!({})).is_err()); + assert_eq!( + exhausted + .control + .job + .lock() + .unwrap() + .as_ref() + .unwrap() + .active_processes() + .unwrap(), + 0 + ); + assert_eq!( + crate::extension_container::test_acl_entries(&acl_file), + file_acl + ); + assert_eq!( + crate::extension_container::test_acl_entries(&acl_root), + root_acl + ); + let replacement = unsafe { registry.start(make("mcp")) }.unwrap(); + wait_for(|| replacement.snapshot().status != Status::Starting); + assert_eq!(replacement.snapshot().status, Status::Ready); + assert_ne!( + serde_json::to_value(replacement.snapshot().identity.unwrap()).unwrap() + ["instance_id"], + old_identity["instance_id"] + ); + let review = replacement + .review("echo".into(), json!({})) + .unwrap() + .wait(Duration::from_secs(5)) + .unwrap(); + assert_eq!( + replacement + .invoke_confirmed(review.review_id) + .unwrap() + .wait(Duration::from_secs(5)) + .unwrap()["content"][0]["text"], + "native MCP success" + ); + replacement.stop(); + wait_for(|| { + registry.reap(); + registry.entries.is_empty() + }); + assert_eq!(replacement.snapshot().status, Status::Stopped); + } let called = Arc::new(AtomicBool::new(false)); let marker = Arc::clone(&called); let mut denied = make("mcp"); diff --git a/frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs b/frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs index 523aa28..24ac507 100644 --- a/frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs +++ b/frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs @@ -3,6 +3,22 @@ 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 == "cpu_burn") { + std::thread::scope(|scope| { + for _ in 0..8 { + scope.spawn(|| { + let until = std::time::Instant::now() + Duration::from_secs(120); + let mut value = 1u64; + while std::time::Instant::now() < until { + for _ in 0..10000 { + value = std::hint::black_box(value.wrapping_mul(6364136223846793005).wrapping_add(1)); + } + } + }); + } + }); + return; + } if args.get(1).is_some_and(|s| s.starts_with("mcp")) { use std::io::{BufRead, Write}; fn read(reader: &mut impl BufRead) -> String { let mut line = String::new(); reader.read_line(&mut line).unwrap(); line } @@ -25,8 +41,8 @@ fn main() { } let mut request = read(&mut input); assert!(request.contains("tools/call")); - if args[1] == "mcp_cancel" || args[1] == "mcp_deadline" { - let _child = std::process::Command::new(std::env::current_exe().unwrap()).arg("wait").spawn().unwrap(); + if args[1] == "mcp_cancel" || args[1] == "mcp_deadline" || args[1] == "mcp_cpu" { + let _child = std::process::Command::new(std::env::current_exe().unwrap()).arg(if args[1] == "mcp_cpu" { "cpu_burn" } else { "wait" }).spawn().unwrap(); std::thread::sleep(Duration::from_secs(120)); return; }