diff --git a/docs/development/OpenNexus生产化实施进度-2026-09-08.md b/docs/development/OpenNexus生产化实施进度-2026-09-08.md index d0ca998..5198a41 100644 --- a/docs/development/OpenNexus生产化实施进度-2026-09-08.md +++ b/docs/development/OpenNexus生产化实施进度-2026-09-08.md @@ -4,6 +4,12 @@ **状态:实施中,未 done,未通过生产发布门禁。** 本文记录工程进度,不将单元测试或开发机运行替代[生产化计划](../architecture/第三阶段生产化工程规划与验收目标.md)中 A–E 的完整验收。 +## 当前状态摘要 + +截至 `7b69537`,Host Sync capability 已开启,扩展 capability 仍关闭。笔记与 Task CRUD 已接入当前 Rust Vault;同步具备上传/拉取、冲突解决、非空初始合并、100 MiB 续传、主题和编辑器偏好、持久退避。Workspace 当前 schema 9。凭据备份/恢复及 WTS 撤销已实现,但实机锁屏验收仍缺。 + +最近完整测试:后端 900 项(Task 增量后)、前端 517 项、Rust desktop 52 项;后两者在重试持久化增量后复跑。以下各节为历史增量,出现的旧 capability、旧 schema 和旧测试数量描述对应当时状态,以本摘要及后续增量为准。OS 沙箱、Rust 扩展管理、剩余逻辑数据类别、完整故障/性能/发布验收仍未完成。 + ## 持续实施增量 - Rust 同步新增 schema 4 inbox/boundary、稳定远端 file_id、remote 来源的移动/删除日志和冲突保留。真实 HTTP 双客户端测试覆盖历史拉取、零回流、本机提交回放保护和同改冲突;文件提交前后的 inbox 重启测试通过。冲突解决和用户界面正在继续实施,仍不开放完整 Sync capability。 @@ -116,3 +122,10 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写 - 取消/切换绑定不增加失败次数,旧绑定回调不能修改新绑定;保存的错误仅允许有长度限制的机器码,不保存服务端任意响应正文。Retry-After 和指数退避保持 1–3600 秒限制。界面对停止自动重试状态不再显示误导性的倒计时。 - 连续关闭重开 20 次验证失败次数与时间保留,另验证认证停止、状态清除、取消、错误正文过滤和新旧绑定隔离。当前是绑定级轮次状态;每个上传作业的独立尝试历史、强制刷新后的 401 重试和完整服务故障矩阵仍需完成。 - 全量前端 97 文件 / 517 项、Rust desktop 全目标 52 项通过(另 1 个父测试驱动并强杀的辅助入口);TypeScript 两项目检查和 Clippy `-D warnings` 通过。日志 `.build/retry-frontend-tests.log`、`.build/retry-rust-tests.log`。Core 打包正在更新,不视为签名发布包。 + + +## 增量:最新 Core 打包与 20 次冷启动 + +- 使用锁定 packaging 环境重新生成 PyInstaller onedir,清单包含 1523 个文件。构建日志 `.build/core-build-production.log`,产物 `.build/sidecar/dist/opennexus-core` 与 `.build/sidecar/manifest.json`。 +- 新增显式打包测试 `cargo test --locked --features desktop --test core_bundle packaged_core_twenty -- --ignored`,调用实际可执行文件并在每次启动前校验完整文件清单。20 个独立临时数据目录、20 个不同会话代际、带鉴权和代际头的 HTTP 健康请求全部成功,逐次退出后端口关闭。测试耗时 95.64 秒,日志 `.build/core-packaged-cold-starts.log`。初版测试遗漏代际请求头导致拒绝,修正测试后通过。 +- 该测试按要求先构建后显式运行,普通源码全目标测试会跳过此入口。Clippy `-D warnings` 通过。此证据来自当前 Windows 开发机,不能替代干净 VM、MSVC 签名安装包、两台真实设备或完整 A-01 验收。 diff --git a/frontend/src-tauri/tests/core_bundle.rs b/frontend/src-tauri/tests/core_bundle.rs index 1e8240b..1cafade 100644 --- a/frontend/src-tauri/tests/core_bundle.rs +++ b/frontend/src-tauri/tests/core_bundle.rs @@ -19,3 +19,55 @@ fn bundle_rejects_modified_missing_and_extra_files() { std::fs::remove_file(file).unwrap(); assert!(verify_bundle(dir.path(), manifest).is_err()); } + +#[test] +#[ignore = "requires scripts/build-core.py; run explicitly after building the isolated Core"] +fn packaged_core_twenty_cold_starts_use_isolated_data_and_rotate_sessions() { + use notesagent_host::core::CoreSupervisor; + use std::{ + collections::HashSet, + io::{Read, Write}, + path::Path, + time::Duration, + }; + let output = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.build/sidecar"); + let bundle = output.join("dist/opennexus-core").canonicalize().unwrap(); + let manifest = std::fs::read_to_string(output.join("manifest.json")).unwrap(); + let executable = bundle.join(if cfg!(windows) { + "opennexus-core.exe" + } else { + "opennexus-core" + }); + let temp = tempfile::tempdir().unwrap(); + let mut generations = HashSet::new(); + for attempt in 0..20 { + let mut core = CoreSupervisor::new( + executable.clone(), + vec![], + bundle.clone(), + temp.path().join(format!("run-{attempt}")), + ) + .with_bundle_manifest(manifest.clone()); + let request = core.request_session("/health").unwrap(); + assert!(generations.insert(request.generation.clone())); + let endpoint = request + .url + .trim_start_matches("http://") + .trim_end_matches("/health"); + let mut stream = std::net::TcpStream::connect(endpoint).unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(20))) + .unwrap(); + write!(stream, "GET /health HTTP/1.1\r\nHost: {endpoint}\r\nAuthorization: {}\r\nX-Core-Generation: {}\r\nConnection: close\r\n\r\n", request.authorization.as_str(), request.generation).unwrap(); + let mut response = String::new(); + stream.read_to_string(&mut response).unwrap(); + assert!( + response.starts_with("HTTP/1.1 200"), + "packaged health failed on attempt {attempt}" + ); + assert!(!response.contains(request.authorization.as_str())); + drop(stream); + drop(core); + assert!(std::net::TcpStream::connect(endpoint).is_err()); + } +}