Files
MyAiDesk/nomifun-tauri/crates/agent/nomi-browser-engine/tests/common/mod.rs
T
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

306 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! observe 集成测试共享接线(`#[ignore]`,本机/打包 chrome)。
//!
//! 任务 5 的契约母本(launch→connect→run_attach_loop→enable_auto_attach→createTarget→arm→
//! call)已被复制过一次;任务 6 起所有 observe 集成测试**复用**本模块的 helper,勿再复制母本。
//!
//! 与契约母本(`observe_fixtures.rs::observe_inject_contract_iframe`)的关系:那个测试手动接线
//! 注入侧的 `incrementalAriaSnapshot` 单帧调用,验证**注入契约**(aria 形态快照);本模块走更高层
//! ——直接建一个 [`CdpBackend`] 并 navigate 到 fixture,让被测对象就是 `engine.observe()` 全链
//! (逐帧缝合 + 脱敏 + 代际翻新 ref 表)。
//!
//! 手动跑(本机 Windows 有系统 Chrome):
//! set NOMIFUN_CHROME_BINARY=...\chrome.exe
//! cargo nextest run -p nomi-browser-engine --run-ignored all -E 'test(observe_)'
//! 首跑写 `.snap.new``cargo insta accept`(或手动改名 .snap)接受为基线。
//! 跑完核对任务管理器无残留 chromeBuilder kill_on_drop 应自动清)。
#![allow(dead_code)] // 不同集成测试文件用到的 helper 子集不同;未用项不报警。
use std::sync::Arc;
use nomi_browser_engine::backend::CdpBackend;
use nomi_browser_engine::firewall::{EgressApprover, FirewallConfig, HostResolver};
use nomi_browser_engine::launch::{launch_chrome, LaunchConfig};
/// fixture 的 file:// URL。`file://` + 单个前导斜杠 + POSIX 路径:`CARGO_MANIFEST_DIR`
/// 在 unix 是 `/abs`(已带前导斜杠)、在 windows 是 `C:/abs`(需补一个),故仅在缺失时补斜杠。
/// 旧写法 `file:///{manifest}` 在 unix 上产生**四**斜杠(`file:////Users/...`)——chrome 容忍但
/// 回报时归一成三斜杠,导致 navigate 的 redirect 判定(请求 url != final url)误报 redirected。
pub fn fixture_url(name: &str) -> String {
let manifest = env!("CARGO_MANIFEST_DIR").replace('\\', "/");
let abs = if manifest.starts_with('/') {
manifest
} else {
format!("/{manifest}")
};
format!("file://{abs}/tests/fixtures/{name}")
}
/// 解析 chromeenv NOMIFUN_CHROME_BINARY > 打包 > 数据目录 > 下载兜底)+ 托管 launchheadless
/// + 自建 transport connect + flatten auto-attach + 取一个 page session,建一个 [`CdpBackend`]。
///
/// `profile`:本测试专属 user-data-dir 后缀,避免并发测试争用同一 profile。
/// 返回的 backend 持有 chrome 进程句柄——Drop 即清理整棵进程树(kill_on_drop);测试结束自然回收。
///
/// 不挂下载沙箱(`download_dir=None`)——下载相关集成测试用
/// [`build_backend_for_fixture_with_downloads`]。evaluate 全权 OFF(默认 default-deny)。
/// 防火墙用 `FirewallConfig::default()`IP 封禁开 + 跨域 POST 门控开 = 现行为)。
pub async fn build_backend_for_fixture(profile: &str) -> CdpBackend {
build_backend_for_fixture_inner(profile, None, false, false, FirewallConfig::default(), None, None, None)
.await
}
/// 同 [`build_backend_for_fixture`],但 **evaluate 全权模式 ON**`EngineConfig.evaluate_full_power
/// = true` 的等价 test seam)——F1-sec 验「全权 LIVE 开 → act(Evaluate) 放行」。
pub async fn build_backend_for_fixture_full_power(profile: &str) -> CdpBackend {
build_backend_for_fixture_inner(profile, None, true, false, FirewallConfig::default(), None, None, None)
.await
}
/// **SD-6 persistent-login mutex 验证 test seam**`evaluate_full_power = true` +
/// `evaluate_persistent_login` = 调用方指定。用于验证 persistent-login LIVE 值穿透到
/// `EvaluateGate.persistent_login`——`persistent_login=true` 时互斥生效(full_power+persistent
/// → Blocked);`persistent_login=false` 时全权正常放行(控制组)。
pub async fn build_backend_for_fixture_persistent_login(
profile: &str,
persistent_login: bool,
) -> CdpBackend {
build_backend_for_fixture_inner(
profile,
None,
true, // evaluate_full_power = ON (to test the mutex)
persistent_login,
FirewallConfig::default(),
None,
None,
None,
)
.await
}
/// **W4d 持久登录验证 test seam**:同 [`build_backend_for_fixture`],但额外注入
/// `storage_state`(上层从 vault `load_storage_state` 解出的登录态 JSON)——引擎在 page
/// 建好后 `restore_cookies`+`restore_local_storage`**启动注入**灌登录态。配合 W4d 集成测试验
/// 「跨引擎/会话持久登录」(会话 A capture+save vault → 会话 B 新引擎 load vault → 此 seam 注入 →
/// 登录态恢复存活)。`storage_state=None` 走零注入(现行为)。
///
/// Always runs on the default browser context (no per-pet isolation).
pub async fn build_backend_for_fixture_with_storage_state(
profile: &str,
storage_state: Option<serde_json::Value>,
) -> CdpBackend {
build_backend_for_fixture_inner(
profile,
None,
false,
false,
FirewallConfig::default(),
None,
storage_state,
None,
)
.await
}
/// **P3-G1 注入链验证 test seam**:同 [`build_backend_for_fixture`],但注入一个**自定义**
/// [`FirewallConfig`](而非 `default()`)。配合 [`CdpBackend::firewall_config_for_test`] 断言
/// 「自定义配置经 from_launched 真的注入到引擎、未被硬编码 default 吞掉」。
pub async fn build_backend_for_fixture_with_firewall(
profile: &str,
firewall: FirewallConfig,
) -> CdpBackend {
build_backend_for_fixture_inner(profile, None, false, false, firewall, None, None, None).await
}
/// **P3-D2 验证 test seam**:注入自定义 [`FirewallConfig`] **+ 一个出口审批通道**
/// [`EgressApprover`])。配合跨域 POST fixture 验「被门控请求悬挂等裁决 → 批准 continue / 拒绝
/// fail / 超时 / 无通道 fail-closed」。`egress_approver=None` 走引擎 fail-closed 默认(验泄漏窗口闭合)。
///
/// **SD-1 交互**`dns_resolver` 注入式——跨域 POST 的目标域要先过 DNS→IP SSRF 守卫才到 approver。
/// 测试须注入一个把探针域映射到**公网 IP** 的 fake resolver,否则真实 DNS 对离线伪域返 NXDOMAIN →
/// 守卫 fail-closed 在 approver **之前**就 Blockapprover 永不被咨询)。`None` = 默认 TokioResolver。
pub async fn build_backend_for_fixture_with_egress(
profile: &str,
firewall: FirewallConfig,
egress_approver: Option<Arc<dyn EgressApprover>>,
dns_resolver: Option<Arc<dyn HostResolver>>,
) -> CdpBackend {
build_backend_for_fixture_inner(profile, None, false, false, firewall, egress_approver, None, dns_resolver).await
}
/// 同 [`build_backend_for_fixture`],但挂 E4 下载沙箱(`Browser.setDownloadBehavior(allowAndName)`
/// 落 `download_dir` + 下载事件循环打 MOTW)。返回 `(backend, download_dir)`——下载测试用 download_dir
/// 校验落盘文件 + Zone.Identifier ADS。
pub async fn build_backend_for_fixture_with_downloads(
profile: &str,
) -> (CdpBackend, std::path::PathBuf) {
let download_dir = std::env::temp_dir().join(format!("nomifun-dl-{profile}-downloads"));
// 干净起点:清旧目录残留(上次跑的 GUID 文件),再建。
let _ = std::fs::remove_dir_all(&download_dir);
std::fs::create_dir_all(&download_dir).expect("create download dir");
let backend = build_backend_for_fixture_inner(
profile,
Some(download_dir.to_string_lossy().into_owned()),
false,
false,
FirewallConfig::default(),
None,
None,
None,
)
.await;
(backend, download_dir)
}
/// **单标签验证 test seamheadful**:同 [`build_backend_for_fixture`] 但 **headful**
/// `force_headless=false`)——验 `--no-startup-window` 在 headful 下被 REMOTE_DEBUGGING
/// keep-alive 拴住(chrome 不无窗口自退、launch 不报 "exited before DevTools port"+ 恰好
/// 一个受控 page。会开一个**可见** chrome 窗口(需本机有显示器;本机 Windows 有)。
pub async fn build_backend_for_fixture_headful(profile: &str) -> CdpBackend {
let chrome = nomi_browser_engine::acquire::resolve_chrome_path(
&std::env::temp_dir().join("nomifun-browser-data"),
None,
)
.await
.expect("resolve chrome (set NOMIFUN_CHROME_BINARY)");
let cfg = LaunchConfig {
chrome_path: chrome,
user_data_dir: std::env::temp_dir().join(format!("nomifun-observe-{profile}-profile")),
headful: true,
};
// force_headless=false → headful(带可见窗口)。这是 --no-startup-window keep-alive 的风险路径。
let launched = launch_chrome(&cfg, false).await.expect("launch headful chrome");
CdpBackend::from_launched(
launched,
true, // headful
true, // display_available
None,
None, // workspace_dir: headful fixture 不上传
false,
false, // evaluate_persistent_login
FirewallConfig::default(),
None,
None,
nomi_browser_engine::KnownSecretValues::default(),
None, // dns_resolver: 默认 TokioResolver
)
.await
.expect("build CdpBackend (headful)")
}
/// 同 [`build_backend_for_fixture_headful`] 但注入自定义 [`FirewallConfig`]。OOPIF 验证用:
/// **headful = 真浏览器进程模型**,site-isolation 才会把跨站 iframe 起成**独立渲染进程(OOPIF**;
/// `--headless=new` 是单渲染进程,不起 OOPIF。配合 `NOMIFUN_CHROME_BINARY` + 测试侧 `NOMI_CHROME_EXTRA_ARGS`
/// --host-resolver-rules/--site-per-process)使用。会开一个**可见** chrome 窗口(需本机有显示器)。
pub async fn build_backend_for_fixture_headful_with_firewall(
profile: &str,
firewall: FirewallConfig,
) -> CdpBackend {
let chrome = nomi_browser_engine::acquire::resolve_chrome_path(
&std::env::temp_dir().join("nomifun-browser-data"),
None,
)
.await
.expect("resolve chrome (set NOMIFUN_CHROME_BINARY)");
let cfg = LaunchConfig {
chrome_path: chrome,
user_data_dir: std::env::temp_dir().join(format!("nomifun-observe-{profile}-profile")),
headful: true,
};
let launched = launch_chrome(&cfg, false).await.expect("launch headful chrome");
CdpBackend::from_launched(launched, true, true, None, None, false, false, firewall, None, None, nomi_browser_engine::KnownSecretValues::default(), None)
.await
.expect("build CdpBackend (headful+firewall)")
}
/// **Headful + 下载沙箱 test seam**Task 6 / `TODO(verify-headful-printToPDF)`):同
/// [`build_backend_for_fixture_headful`]headful,真可见窗口)但额外挂 E4 下载沙箱
/// `download_dir`)。专供 **save_as_pdf 的 headful 校验**——验 `Page.printToPDF` 在 headful
/// Chrome 下是否仍产非空 PDFheadful 历史上对 printToPDF 有限制)。返回 `(backend, download_dir)`。
/// 会开一个**可见** chrome 窗口(需本机有显示器)。
pub async fn build_backend_for_fixture_headful_with_downloads(
profile: &str,
) -> (CdpBackend, std::path::PathBuf) {
let download_dir =
std::env::temp_dir().join(format!("nomifun-dl-{profile}-headful-downloads"));
// 干净起点:清旧目录残留,再建。
let _ = std::fs::remove_dir_all(&download_dir);
std::fs::create_dir_all(&download_dir).expect("create download dir");
let chrome = nomi_browser_engine::acquire::resolve_chrome_path(
&std::env::temp_dir().join("nomifun-browser-data"),
None,
)
.await
.expect("resolve chrome (set NOMIFUN_CHROME_BINARY)");
let cfg = LaunchConfig {
chrome_path: chrome,
user_data_dir: std::env::temp_dir().join(format!("nomifun-observe-{profile}-profile")),
headful: true,
};
let launched = launch_chrome(&cfg, false).await.expect("launch headful chrome");
let backend = CdpBackend::from_launched(
launched,
true, // headful
true, // display_available
Some(download_dir.to_string_lossy().into_owned()),
None, // workspace_dirsave_as_pdf 不依赖上传沙箱
false,
false, // evaluate_persistent_login
FirewallConfig::default(),
None,
None,
nomi_browser_engine::KnownSecretValues::default(),
None, // dns_resolver: 默认 TokioResolver
)
.await
.expect("build CdpBackend (headful+downloads)");
(backend, download_dir)
}
// 构造参数随安全配置增长(download/full_power/persistent/firewall/egress/storage/dns_resolver);
// 与 build_backend / from_launched 同源的已知取舍(见 cdp.rs 的 too_many_arguments allow + EngineRuntimeParams TODO)。
#[allow(clippy::too_many_arguments)]
async fn build_backend_for_fixture_inner(
profile: &str,
download_dir: Option<String>,
evaluate_full_power: bool,
evaluate_persistent_login: bool,
firewall: FirewallConfig,
egress_approver: Option<Arc<dyn EgressApprover>>,
storage_state: Option<serde_json::Value>,
dns_resolver: Option<Arc<dyn HostResolver>>,
) -> CdpBackend {
let chrome = nomi_browser_engine::acquire::resolve_chrome_path(
&std::env::temp_dir().join("nomifun-browser-data"),
None,
)
.await
.expect("resolve chrome (set NOMIFUN_CHROME_BINARY)");
let cfg = LaunchConfig {
chrome_path: chrome,
user_data_dir: std::env::temp_dir().join(format!("nomifun-observe-{profile}-profile")),
headful: false,
};
let launched = launch_chrome(&cfg, true).await.expect("launch chrome");
// SD-2fixture 测试默认给 temp_dir 作为 workspace(上传沙箱需要一个根;实际产品环境是 per-pet
// workspace,此处用 temp_dir 让 temp 下创建的测试文件都能通过沙箱校验)。
let workspace_dir = Some(std::env::temp_dir());
// headful=false / display=falseobserve 不依赖显示,capabilities 字段对集成断言无关紧要。
CdpBackend::from_launched(
launched,
false,
false,
download_dir,
workspace_dir,
evaluate_full_power,
evaluate_persistent_login,
firewall,
egress_approver,
storage_state,
nomi_browser_engine::KnownSecretValues::default(),
dns_resolver,
)
.await
.expect("build CdpBackend")
}