test(extensions): 验证容器 CPU 失败与实例替换

This commit is contained in:
2026-09-09 05:41:51 +08:00
parent b33740d8aa
commit 2d228cd424
4 changed files with 162 additions and 5 deletions
@@ -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_EXCEEDEDRunning 资源状态一致,失败会话拒绝目录刷新,关闭后 Job 的 ActiveProcesses=0Host 写入并重开读取笔记成功。本次工具调用到错误约 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`
- 同轮只读复查测试 SyncHTTP /health=200/ready=503,响应错误 DEPENDENCY_UNAVAILABLESSH 22 可以建立 TCP,但在认证前读到空 banner,连接被对端关闭。未尝试写入部署,也未将 /health 成功当作服务就绪。证据 `.build/sync-cpu-integration-readiness.json`;该远端障碍不阻止继续本地生产化工作。
+45 -3
View File
@@ -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,
@@ -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");
+18 -2
View File
@@ -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;
}