feat: 完成 OpenNexus 第三阶段核心功能与生产化基础 #45

Merged
Kronecker merged 151 commits from feat/phase3-completion into main 2026-09-10 01:16:31 +08:00
4 changed files with 142 additions and 1 deletions
Showing only changes of commit ec7271a47e - Show all commits
@@ -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 503SSH 22 连接仍在认证前返回空 banner;未修改远端配置,仍需要恢复就绪依赖与 SSH 可用入口。
- C-04 的 CPU 持续超限、scratch 配额、broker 限流及完整桌面证据仍未齐备,不能将本测试扩大为 C-04 全项通过。整体生产化目标继续进行。
@@ -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
View File
@@ -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") {