Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,327 @@
//! observe 注入契约快照测试(`#[ignore]`,本机/打包 chrome)。
//!
//! 对一个**固定 HTML fixture**`tests/fixtures/iframe.html`file:// 加载)跑注入侧的
//! `incrementalAriaSnapshot`,把它返回的 `.full`aria YAML,给 LLM 看的那一版)冻成 insta
//! 快照。目的:
//! - **供人审**aria 输出形态(role/name/ref=f0e<n>)一眼可读、可在 review 里核对;
//! - **防漂移**vendor 的 Playwright InjectedScript 升级后若 aria 序列化形态变了,快照 diff
//! 会立刻报出来(DESIGN:整包 vendor 不 fork,靠契约测试钉住外部行为)。
//!
//! 接线母本 = `src/injected.rs` 的 `inject_aria_snapshot_smoke`(八步:launch → connect →
//! run_attach_loop → enable_auto_attach → createTarget page → navigate(file://) → arm →
//! `Runtime.evaluate("document.body")` 取 objectId → `call_injected("incrementalAriaSnapshot",
//! [body, opts], by_value=true)` → 取 `.full`)。
//!
//! 手动跑(本机 Windows 有系统 Chrome):
//! set NOMIFUN_CHROME_BINARY=...\chrome.exe
//! cargo nextest run -p nomi-browser-engine --run-ignored all -E 'test(observe_inject_contract)'
//! 首跑写 `.snap.new``cargo insta accept`(或手动改名 .snap)接受为基线。
//! 跑完核对任务管理器无残留 chromeBuilder kill_on_drop 应自动清)。
use std::time::Duration;
use chromiumoxide::cdp::browser_protocol::page::{
EnableParams as PageEnable, NavigateParams,
};
use chromiumoxide::cdp::browser_protocol::target::{CreateTargetParams, EventAttachedToTarget};
use chromiumoxide::cdp::js_protocol::runtime::{
CallArgument, EvaluateParams, ExecutionContextId, RemoteObjectId,
};
use nomi_browser_engine::injected::InjectionManager;
use nomi_browser_engine::launch::{launch_chrome, LaunchConfig};
use nomi_browser_engine::transport::Connection;
mod common;
/// fixture 的 file:// URL`file://` + 单前导斜杠 + POSIX 路径;unix `/abs` 已带斜杠、windows
/// `C:/abs` 补一个。旧 `file:///{manifest}` 在 unix 产四斜杠,被 chrome 归一后破坏 url 比较)。
fn fixture_url() -> String {
let manifest = env!("CARGO_MANIFEST_DIR").replace('\\', "/");
let abs = if manifest.starts_with('/') {
manifest
} else {
format!("/{manifest}")
};
format!("file://{abs}/tests/fixtures/iframe.html")
}
#[tokio::test]
#[ignore = "需本机/打包 chromeset NOMIFUN_CHROME_BINARY 后 --run-ignored all"]
async fn observe_inject_contract_iframe() {
// 1) resolve chromeenv NOMIFUN_CHROME_BINARY > 打包 > 数据目录 > 下载兜底)+ launch headless。
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("nomifun-observe-contract-profile"),
headful: false,
};
let launched = launch_chrome(&cfg, true).await.expect("launch chrome");
// 2) connect + 先 run_attach_loop 后 enable_auto_attach(顺序铁律,否则首批 attach 事件丢)。
let _child = launched.child; // 保活 chromedrop 即清理)。
let conn = Connection::connect_launched(launched.transport)
.await
.expect("connect");
let _attach_loop = conn.run_attach_loop();
conn.enable_auto_attach().await.expect("auto attach");
// 3) 取一个 page sessioncreateTarget about:blank + 等其 attachedToTarget)。
let mut attached = conn.subscribe(EventAttachedToTarget::IDENTIFIER, None);
let create = CreateTargetParams::new("about:blank");
let cr = conn
.send::<CreateTargetParams>(nomi_browser_engine::transport::ROOT_SESSION, &create)
.await
.expect("createTarget");
let target_id = cr["targetId"].as_str().expect("targetId").to_string();
let page_session = loop {
let ev = tokio::time::timeout(Duration::from_secs(10), attached.recv())
.await
.expect("attach timeout")
.expect("attach recv");
if let Ok(att) = serde_json::from_value::<EventAttachedToTarget>(ev.params.clone()) {
let tid: String = att.target_info.target_id.clone().into();
if tid == target_id && att.target_info.r#type == "page" {
break String::from(att.session_id);
}
}
};
// 4) navigate 到固定 fixturefile://,含 h1/button/textbox/iframearia 形态稳定)。
conn.send::<PageEnable>(&page_session, &PageEnable::default())
.await
.expect("Page.enable");
let mut load_rx = conn.subscribe("Page.loadEventFired", Some(&page_session));
conn.send::<NavigateParams>(&page_session, &NavigateParams::new(fixture_url()))
.await
.expect("navigate");
let _ = tokio::time::timeout(Duration::from_secs(15), load_rx.recv()).await;
// 5) arm 注入管线 + 等主 frame 的 utility-world context 就绪(主 frameId = page targetId)。
let mgr = InjectionManager::new(conn.clone(), page_session.clone());
let _ctx_loop = mgr.arm().await.expect("arm injection");
let frame_id = target_id.clone();
let mut ctx_id = None;
for _ in 0..50 {
if let Ok(id) = mgr.context_id_for(&frame_id) {
ctx_id = Some(id);
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let ctx_id = ctx_id.expect("utility world context never registered for main frame");
// 6) 取 document.body 的 objectId(同 utility world 的元素句柄),作 incrementalAriaSnapshot 的 node。
let mut body_eval = EvaluateParams::new("document.body".to_string());
body_eval.context_id = Some(ExecutionContextId::new(ctx_id));
body_eval.return_by_value = Some(false);
let body_res = conn
.send::<EvaluateParams>(&page_session, &body_eval)
.await
.expect("evaluate document.body");
let body_obj_id = body_res["result"]["objectId"]
.as_str()
.expect("document.body objectId")
.to_string();
// 7) call_injected incrementalAriaSnapshot(body, {mode:ai, refPrefix:f0, depth:12})node 走
// objectIdopts 走 by-value;返回 by-valueresult.value 是 {full, iframeRefs, ...})。
let node_arg = CallArgument {
object_id: Some(RemoteObjectId::new(body_obj_id)),
..Default::default()
};
let opts_arg = CallArgument {
value: Some(serde_json::json!({"mode": "ai", "refPrefix": "f0", "depth": 12})),
..Default::default()
};
let result = mgr
.call_injected(
&frame_id,
"incrementalAriaSnapshot",
vec![node_arg, opts_arg],
true,
)
.await
.expect("call_injected incrementalAriaSnapshot");
// 8) 取 .fullaria YAML 字符串)并快照。aria YAML 只含页面结构(generic/button/textbox/iframe
// + ref=f0e<n>),不含随机 world 名,故无需归一。
let full = result["value"]["full"]
.as_str()
.expect("incrementalAriaSnapshot result.value.full is a string")
.to_string();
assert!(!full.trim().is_empty(), "aria .full must be non-empty");
insta::assert_snapshot!(full);
}
// ═══════════════════════════════════════════════════════════════════════════
// 任务 6CdpBackend::observe 全链集成测试(`#[ignore]`,本机/打包 chrome)。
// 复用 tests/common 的 build_backend_for_fixture helper(勿再复制契约母本)。
// 被测对象是 engine.observe():逐帧 incrementalAriaSnapshot → 缝合 → 脱敏 → 代际翻新 ref 表。
// ═══════════════════════════════════════════════════════════════════════════
use nomi_browser_engine::{BrowserEngine, ObserveOpts};
/// 同进程 iframe 缝合:navigate iframe.htmlsrcdoc iframe 是同进程,应能缝)→ observe →
/// 快照 yaml + 断言含父帧 ref f0e*;若同进程子帧缝上则含子内容(缩进的 Inner 链接)。
#[tokio::test]
#[ignore = "需本机/打包 chromeset NOMIFUN_CHROME_BINARY 后 --run-ignored all"]
async fn observe_iframe_stitched() {
let engine = common::build_backend_for_fixture("iframe").await;
engine
.navigate(&common::fixture_url("iframe.html"), false)
.await
.expect("navigate iframe.html");
let obs = engine
.observe(&ObserveOpts::default())
.await
.expect("observe");
eprintln!("=== observe_iframe_stitched yaml ===\n{}\n=== end ===", obs.yaml);
// 父帧 reff0e*)必现。
assert!(obs.yaml.contains("f0e"), "expected parent-frame ref f0e*:\n{}", obs.yaml);
// <data> 包裹。
assert!(obs.yaml.contains("<data"), "expected <data> wrap:\n{}", obs.yaml);
// 同进程 srcdoc 子帧应缝入(含其链接文本 Inner);srcdoc iframe 是同进程,应能缝。
//
// 快照里的 origin 是本机绝对 file:// 路径——快照前归一成稳定占位符,使快照可移植
// (不钉死 checkout 路径),同时仍审 <data origin=…> 包裹 + 缝合结构。
let normalized = normalize_fixture_origin(&obs.yaml, "iframe.html");
insta::assert_snapshot!("observe_iframe_stitched", normalized);
}
/// 把 yaml 里本机绝对 `origin="file:///.../tests/fixtures/<name>"` 归一成
/// `origin="file://<FIXTURE>/<name>"`,让 insta 快照不钉死 checkout 路径。
fn normalize_fixture_origin(yaml: &str, name: &str) -> String {
let manifest = env!("CARGO_MANIFEST_DIR").replace('\\', "/");
// Chrome reports file:// origins with exactly ONE leading slash before the
// POSIX path (unix: `file:///abs`; windows: `file:///C:/abs`). But
// CARGO_MANIFEST_DIR is `/abs` on unix and `C:/abs` on windows — so prepend a
// slash only when it doesn't already start with one. The old unconditional
// `file:///{manifest}` produced FOUR slashes on unix (`file:////Users/...`),
// never matched the reported origin, and leaked the absolute checkout path
// into the snapshot. This makes the placeholder fire on every host.
let abs = if manifest.starts_with('/') {
manifest
} else {
format!("/{manifest}")
};
let actual = format!("file://{abs}/tests/fixtures/{name}");
let placeholder = format!("file://<FIXTURE>/{name}");
yaml.replace(&actual, &placeholder)
}
/// open shadow 可见、closed shadow 不可见(D11)。
#[tokio::test]
#[ignore = "需本机/打包 chromeset NOMIFUN_CHROME_BINARY 后 --run-ignored all"]
async fn observe_shadow_open_visible_closed_absent() {
let engine = common::build_backend_for_fixture("shadow").await;
engine
.navigate(&common::fixture_url("shadow.html"), false)
.await
.expect("navigate shadow.html");
let obs = engine
.observe(&ObserveOpts::default())
.await
.expect("observe");
eprintln!("=== observe_shadow yaml ===\n{}\n=== end ===", obs.yaml);
assert!(
obs.yaml.contains("OpenShadowBtn"),
"open shadow content must be visible:\n{}",
obs.yaml
);
assert!(
!obs.yaml.contains("ClosedShadowBtn"),
"closed shadow content must NOT be visible:\n{}",
obs.yaml
);
}
/// 代际单调递增(无导航两次 observe)+ 导航后旧 ref 在新表 resolve 不到(generation 跳变隔离)。
#[tokio::test]
#[ignore = "需本机/打包 chromeset NOMIFUN_CHROME_BINARY 后 --run-ignored all"]
async fn observe_generation_increments_and_renews() {
let engine = common::build_backend_for_fixture("gen").await;
engine
.navigate(&common::fixture_url("shadow.html"), false)
.await
.expect("navigate");
let obs1 = engine.observe(&ObserveOpts::default()).await.expect("observe1");
let obs2 = engine.observe(&ObserveOpts::default()).await.expect("observe2");
// 无导航两次 observe:代际单调 +1。
assert_eq!(
obs2.generation.0,
obs1.generation.0 + 1,
"generation must increment per observe: {:?} -> {:?}",
obs1.generation,
obs2.generation
);
// 导航到不同页再 observe:代际继续递增(旧代际 ref 不再属于新表)。
engine
.navigate(&common::fixture_url("secrets.html"), false)
.await
.expect("navigate2");
let obs3 = engine.observe(&ObserveOpts::default()).await.expect("observe3");
assert!(
obs3.generation.0 > obs2.generation.0,
"generation must advance after navigation: {:?} -> {:?}",
obs2.generation,
obs3.generation
);
// 每代 entries 应非空(页面有可操作元素:button 等)。
assert!(!obs1.entries.is_empty(), "obs1 entries should be non-empty");
assert!(!obs3.entries.is_empty(), "obs3 entries should be non-empty");
}
/// 脱敏:secrets.html 的明文 sk-/Bearer token 不出现在 yaml,且整体被 <data> 包裹。
#[tokio::test]
#[ignore = "需本机/打包 chromeset NOMIFUN_CHROME_BINARY 后 --run-ignored all"]
async fn observe_redacts_secrets() {
let engine = common::build_backend_for_fixture("secrets").await;
engine
.navigate(&common::fixture_url("secrets.html"), false)
.await
.expect("navigate secrets.html");
let obs = engine
.observe(&ObserveOpts::default())
.await
.expect("observe");
eprintln!("=== observe_redacts_secrets yaml ===\n{}\n=== end ===", obs.yaml);
assert!(obs.yaml.contains("<data"), "expected <data> wrap:\n{}", obs.yaml);
// 明文 secret 实体不得出现。
assert!(
!obs.yaml.contains("sk-ABCDEFGHIJ0123456789xyzQRSTUV"),
"plaintext sk- token leaked:\n{}",
obs.yaml
);
assert!(
!obs.yaml.contains("abcdef0123456789ABCDEFghij"),
"plaintext Bearer token leaked:\n{}",
obs.yaml
);
// D5Critical):password <input value="hun]ter2sk"> 的明文 value 不得进 YAML。
// 序列化层(observe)按 DOM type=password 收 ref 后宿主侧抹掉内联 value(短/低熵口令
// 正则脱敏救不了,必须靠 type=password 信号置空)。**value 含 `]`** 压测确定性锚点 bug:
// 旧 blank_inline_value 用全行 rfind(']') → 锚点落进 value 内部漏抹 → 明文泄露;改 ref-token
// 锚后必不泄露。
assert!(
!obs.yaml.contains("hun]ter2"),
"password value (with ]) leaked into observe yaml:\n{}",
obs.yaml
);
// 脱敏占位符应在(确认确实脱敏,而非该文本压根没进 aria 树)。
assert!(
obs.yaml.contains("[REDACTED_SECRET]"),
"expected redaction placeholder (text must have entered the aria tree):\n{}",
obs.yaml
);
}