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

247 lines
11 KiB
Rust
Raw Permalink 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.
//! **P3 W4clocalStorage storage_state 捕获/恢复 端到端集成**`#[ignore]`,本机/打包 chrome)。
//!
//! 验证 W4clocalStorageorigin-bound)捕获/恢复机制):
//! - **localStorage 跨 capture/restore 往返保真(origin-bound,核心验收)**:在引擎 A 页面
//! `setItem` 几个键 → `capture_local_storage` 读回(origin + 键值保真)→ 在**新**引擎 B
//! navigate 到同 origin → `restore_local_storage` 灌进去 → 页面读回 `getItem` 验值生效。
//! - **origin-bound**:捕获/恢复都绑**当前页面 origin**localStorage 同源分区,无法跨 origin 全局取/设);
//! restore 只灌 state 中 origin == 当前页面 origin 的那份(不跨 origin 误写)。
//! - **默认 context 零回归**capture/restore 走默认 context 页面,机制仍工作。
//!
//! **W4c 不验 IndexedDBbest-effort/TODO`OriginStorage.index_db` 恒 None)也不验磁盘 vault 持久化
//! W4d**。只走**内存往返**capture → StorageState → restore+ origin-bound 注入。
//!
//! **为何用真实 `https://example.com` 而非 file:// fixture**localStorage 在 `file://` origin 上
//! Chrome 行为不稳(origin 形态 `null`/`file://` 视版本而异,且 storage 可能被分区/禁用);`https://
//! example.com` 是稳定可达的真实 origin(与 W4b cookie 测试同选),localStorage 在其上可靠工作——
//! 这是「origin-bound 往返保真」最贴近真实登录态的验证场景。
//!
//! 手动跑(本机 Windows 有系统 Chrome):
//! set NOMIFUN_CHROME_BINARY=...\chrome.exe
//! cargo nextest run -p nomi-browser-engine --run-ignored all -E 'test(w4c)'
//! 跑完核对任务管理器无残留 chromeBuilder kill_on_drop + disposeOnDetach 自动清)。
//!
//! 真实结果(本机首跑 eprintln 出捕到的 localStorage——填回任务汇报)。
use nomi_browser_engine::storage_state::{OriginStorage, StorageState};
use nomi_browser_engine::BrowserEngine;
mod common;
const ORIGIN: &str = "https://example.com";
/// 在当前页面(默认 page world`setItem` 一组键值(测试 seam:模拟「页面写了 localStorage 登录态」)。
async fn seed_local_storage(
backend: &nomi_browser_engine::backend::CdpBackend,
pairs: &[(&str, &str)],
) {
// 用 JSON 安全编码键值,逐键 setItem。
let pairs_owned: Vec<[&str; 2]> = pairs.iter().map(|(k, v)| [*k, *v]).collect();
let pairs_json = serde_json::to_string(&pairs_owned).expect("json pairs");
let script = format!(
"(() => {{ const pairs = {pairs_json}; for (const [k, v] of pairs) localStorage.setItem(k, v); return localStorage.length; }})()"
);
let r = backend
.__eval_page_world_for_test(&script)
.await
.expect("seed localStorage setItem");
eprintln!("=== seeded localStorage, length now = {:?}", r.get("value"));
}
/// 读回当前页面某 localStorage 键(默认 page world `getItem`)。None = 键不存在(返回 JS null)。
async fn read_local_storage(
backend: &nomi_browser_engine::backend::CdpBackend,
key: &str,
) -> Option<String> {
let key_json = serde_json::to_string(key).expect("json key");
let script = format!("localStorage.getItem({key_json})");
let r = backend
.__eval_page_world_for_test(&script)
.await
.expect("read localStorage getItem");
r.get("value").and_then(|v| v.as_str()).map(|s| s.to_string())
}
/// **核心:localStorage 跨 capture/restore 往返保真(origin-bound**。
/// engine Anavigate example.com → seed localStorage → captureorigin + 键值保真)。
/// engine B(新引擎,独立 profile):navigate example.com → restore A 的快照 → 页面 getItem 读回值生效。
#[tokio::test]
#[ignore = "需本机/打包 chromeset NOMIFUN_CHROME_BINARY 后 --run-ignored all"]
async fn w4c_local_storage_round_trip_origin_bound() {
// ── 捕获侧:engine A ──────────────────────────────────────────────────────
let backend_a = common::build_backend_for_fixture("w4c-capA").await;
let nav = backend_a
.navigate(ORIGIN, false)
.await
.expect("navigate A to example.com");
eprintln!("=== w4c capture nav === final_url={}", nav.final_url);
// 页面写 localStorage(含特殊字符值,验往返不被吞)。
seed_local_storage(
&backend_a,
&[
("ls_auth", "jwt.eyJhbGciOiJ9.sig-abc123"),
("ls_theme", "dark"),
("ls_json", "{\"a\":1,\"b\":\"x=y&z\"}"),
],
)
.await;
// capture:读回当前 origin 的 localStorageorigin + 键值保真)。
let captured = backend_a
.capture_local_storage()
.await
.expect("capture_local_storage")
.expect("page has an origin to capture");
eprintln!(
"=== w4c captured origin={} items={:?}",
captured.origin, captured.local_storage
);
// **核心验收:捕到的 origin == 页面 origin;键值保真**。
assert_eq!(captured.origin, ORIGIN, "captured origin must match page origin (origin-bound)");
let find = |k: &str| {
captured
.local_storage
.iter()
.find(|i| i.name == k)
.map(|i| i.value.as_str())
};
assert_eq!(find("ls_auth"), Some("jwt.eyJhbGciOiJ9.sig-abc123"), "auth value captured");
assert_eq!(find("ls_theme"), Some("dark"), "theme value captured");
assert_eq!(find("ls_json"), Some("{\"a\":1,\"b\":\"x=y&z\"}"), "special chars captured intact");
// 组装一份 storage_state(只含 localStorage——本任务范围)。
let state = StorageState {
cookies: vec![],
local_storage: vec![captured.clone()],
};
// ── 恢复侧:engine B(独立 profilelocalStorage 必为空白起点)──────────────────
let backend_b = common::build_backend_for_fixture("w4c-resB").await;
backend_b
.navigate(ORIGIN, false)
.await
.expect("navigate B to example.com");
// 恢复前:B 的 localStorage 该键应不存在(新引擎 + 独立 profile = 干净起点)。
assert_eq!(
read_local_storage(&backend_b, "ls_auth").await,
None,
"fresh engine B must not have A's localStorage before restore"
);
// restore:把 A 的 localStorage 灌进 B 当前页面(origin-boundorigin 匹配 example.com)。
backend_b
.restore_local_storage(&state)
.await
.expect("restore_local_storage into B");
// 页面读回:值生效(往返保真——这是「localStorage 登录态经 restore 灌入、页面读得到」的真实信号)。
let auth = read_local_storage(&backend_b, "ls_auth").await;
let theme = read_local_storage(&backend_b, "ls_theme").await;
let json = read_local_storage(&backend_b, "ls_json").await;
eprintln!("=== w4c restored read-back === ls_auth={auth:?} ls_theme={theme:?} ls_json={json:?}");
assert_eq!(
auth.as_deref(),
Some("jwt.eyJhbGciOiJ9.sig-abc123"),
"restored localStorage value must be readable in page (round-trip fidelity)"
);
assert_eq!(theme.as_deref(), Some("dark"));
assert_eq!(json.as_deref(), Some("{\"a\":1,\"b\":\"x=y&z\"}"), "special chars survive restore");
// re-capture 在 B:应捕到刚恢复的项(capture↔restore 对称)。
let recap = backend_b
.capture_local_storage()
.await
.expect("re-capture B")
.expect("B has origin");
assert!(
recap.local_storage.iter().any(|i| i.name == "ls_auth"),
"re-capture after restore must see the restored key: {:?}",
recap.local_storage
);
}
/// **origin-boundrestore 不跨 origin 误写**。state 里只有 example.com 的 localStorage;当前页面停在
/// about:blank(无 example.com origin)→ restore 是 no-op(不把 example.com 的键写进无关页面)。
#[tokio::test]
#[ignore = "需本机/打包 chromeset NOMIFUN_CHROME_BINARY 后 --run-ignored all"]
async fn w4c_restore_is_origin_bound_no_cross_origin_write() {
let backend = common::build_backend_for_fixture("w4c-origin").await;
// 当前页面在另一个 originfixture file://),与 state 里的 example.com 不同 origin。
backend
.navigate(&common::fixture_url("act-c1.html"), false)
.await
.expect("navigate to fixture origin");
// state 只含 example.com 的 localStorage。
let state = StorageState {
cookies: vec![],
local_storage: vec![OriginStorage::new_local_storage(
ORIGIN,
[("ls_cross", "should-not-leak".to_string())]
.into_iter()
.map(|(k, v)| (k.to_string(), v)),
)],
};
// restore:当前页面 origin != example.com → no-op(绝不把 example.com 键写进 fixture 页面)。
backend
.restore_local_storage(&state)
.await
.expect("restore is no-op when origin mismatches (origin-bound)");
// fixture 页面不该出现 example.com 的键(origin-bound 防误写)。
let leaked = read_local_storage(&backend, "ls_cross").await;
eprintln!("=== w4c origin-bound no-cross-write === fixture-page ls_cross={leaked:?}");
assert_eq!(
leaked, None,
"restore must NOT write example.com's localStorage into a different-origin page (origin-bound)"
);
}
/// **默认 context 零回归**capture/restore 走默认 context 页面,机制仍工作
/// (往返保真)。
#[tokio::test]
#[ignore = "需本机/打包 chromeset NOMIFUN_CHROME_BINARY 后 --run-ignored all"]
async fn w4c_default_context_local_storage_works() {
let backend = common::build_backend_for_fixture("w4c-default").await;
backend
.navigate(ORIGIN, false)
.await
.expect("navigate default context to example.com");
seed_local_storage(&backend, &[("ls_default", "v-default-ctx")]).await;
let captured = backend
.capture_local_storage()
.await
.expect("capture default context")
.expect("default context page has origin");
eprintln!(
"=== w4c default-context captured origin={} items={:?}",
captured.origin, captured.local_storage
);
assert_eq!(captured.origin, ORIGIN);
assert!(
captured.local_storage.iter().any(|i| i.name == "ls_default" && i.value == "v-default-ctx"),
"default-context localStorage must round-trip: {:?}",
captured.local_storage
);
// restore 同一份回当前页面(覆盖即幂等)→ 读回仍在。
let state = StorageState {
cookies: vec![],
local_storage: vec![captured],
};
backend
.restore_local_storage(&state)
.await
.expect("restore default context");
assert_eq!(
read_local_storage(&backend, "ls_default").await.as_deref(),
Some("v-default-ctx"),
"default-context restore round-trip"
);
}