Skip to main content

bao_browser/
runtime_bridge.rs

1// @trace REQ-BRW-003 [entity:CdpRouter] [entity:CdpSession]  REQ-BRW-001: Bridge between servo browser context and Node.js APIs
2// REQ-ENG-007: Unified runtime coordination
3//
4// Architecture: dual-Realm isolation via SpiderMonkey compartments
5// - servo's JSContext handles DOM + Web APIs in Page Realm (Window global)
6// - Bao creates a separate Node Realm (JS_NewGlobalObject) for privileged scripts
7// - Node Realm is in its own Compartment — Page Realm physically cannot see it
8// - evaluate_js() uses EnterRealm(Node) → execute → LeaveRealm → back to Page Realm
9// - evaluate_js_web() executes directly in Page Realm (no Realm switch needed)
10//
11// JSContext fusion:
12// - servo creates JSContext internally in JSEngineSetup::default()
13// - Both Realms share the same JSContext (servo's script thread)
14// - GC is shared across both Realms
15// - Node Realm lifecycle is tied to Page — destroyed when Page closes
16
17use crate::error::BrowserError;
18use crate::page::PageHandle;
19// NOTE: bao_engine::WebWorker bypass removed per DEC-WK-001 BCE-20260627-008.
20// Workers now route through servo's native Worker::Constructor.
21use dashmap::DashMap;
22use mozjs::rooted;
23use std::cell::RefCell;
24use std::ptr::{self, NonNull};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::mpsc;
27use std::sync::{Arc, OnceLock};
28
29use std::time::Duration;
30
31// @trace REQ-SEC-002 [entity:EvaluateResult]
32/// Result of evaluating a script in the Node Realm.
33///
34/// Captures the serialized return value or an error message from
35/// `evaluate_in_node_realm`. Both fields are `Option<String>`:
36/// - `value` is `Some` when the script produced a non-undefined result.
37/// - `error` is `Some` when the evaluation or serialization failed.
38///
39/// At most one of `value` / `error` is `Some` — never both.
40#[derive(Debug, Clone, PartialEq, Eq, Default)]
41pub struct EvaluateResult {
42    /// Serialized JS return value (JSON string), or None if undefined/error.
43    pub value: Option<String>,
44    /// Error message when evaluation failed, or None on success.
45    pub error: Option<String>,
46}
47
48impl EvaluateResult {
49    /// Create an ok result with a serialized value.
50    pub fn ok(value: String) -> Self {
51        EvaluateResult {
52            value: Some(value),
53            error: None,
54        }
55    }
56
57    /// Create an error result with a message.
58    pub fn err(error: String) -> Self {
59        EvaluateResult {
60            value: None,
61            error: Some(error),
62        }
63    }
64
65    /// Returns true when the evaluation succeeded (no error).
66    pub fn is_ok(&self) -> bool {
67        self.error.is_none()
68    }
69
70    /// Returns true when the evaluation failed.
71    pub fn is_err(&self) -> bool {
72        self.error.is_some()
73    }
74}
75
76// @trace REQ-SEC-002 REQ-SEC-003 [req:REQ-SEC-002,REQ-SEC-003]
77// Per-page Node Realm storage, keyed by WebViewId (NOT by raw *mut JSObject).
78//
79// BCE-20260621-001 ROOT CAUSE: previous design used three process-wide statics
80// (NODE_REALMS: DashMap<usize,usize>, PAGE_GLOBALS: DashMap<usize,usize>,
81// LAST_PAGE_GLOBAL: AtomicUsize) holding cross-thread *mut JSObject raw pointers.
82// servo spawns one ScriptThread per pipeline on its own OS thread
83// (components/script/script_thread.rs:527 `thread::Builder::new().name("Script#{id}")`)
84// each with a thread-local JSContext (script_runtime.rs:740 `cx()` reads from
85// `RustRuntime::get()` thread-local slot; SAFETY: "only one JSContext can exist
86// on the thread"). Globals storing cross-thread *mut JSObject → callback for
87// page B might dereference a JSObject created on page A's ScriptThread with
88// page B's cx → activation stack corruption → SIGSEGV in
89// js::jit::BaselineFrame::initForOsr (BaselineFrame.cpp:153).
90//
91// ROOT FIX (BCE-20260621-001):
92// - NODE_REALM_BY_WEBVIEW: per-page node_global keyed by WebViewId. Values are
93//   raw pointers but they are ONLY ever dereferenced on the same ScriptThread
94//   that created them (via the WebViewId-keyed servo callback which always
95//   runs on that page's ScriptThread). The main thread reads the pointer as
96//   an opaque address and passes it back into another WebViewId-keyed callback;
97//   it never dereferences it.
98// - PAGE_GLOBAL_BY_WEBVIEW: per-page servo Window global, also keyed by
99//   WebViewId, also only dereferenced inside that page's ScriptThread callback.
100// - LAST_PAGE_GLOBAL: ELIMINATED. inject_node_apis_with_stealth now passes the
101//   WebViewId through and reads the page_global via a per-WebViewId OnceLock,
102//   not a process-wide global. This removes the "last writer wins" race that
103//   caused PageInner to capture the wrong page's pointer.
104// - thread_local! PER_THREAD_PAGE_GLOBAL: for lazy_dom_getter_impl, which
105//   executes inside a ScriptThread (one WebView per ScriptThread), so a
106//   thread-local is correct and avoids any cross-thread pointer storage.
107//
108// @trace REQ-PERF-003 [entity:BufferManager]
109// REQ-PERF-003: WebViewId-keyed OnceLock<DashMap> gives O(1) per-page lookup
110// with DashMap sharded locks; OnceLock avoids lazy_static overhead.
111//
112// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
113// C10 (NFR-THREAD-SAFETY): no cross-thread *mut JSObject dereference. Pointers
114// flow only WebViewId-keyed ⇒ same-ScriptThread access.
115static NODE_REALM_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>> = OnceLock::new();
116static PAGE_GLOBAL_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>> = OnceLock::new();
117
118fn node_realm_by_webview() -> &'static DashMap<servo::WebViewId, usize> {
119    NODE_REALM_BY_WEBVIEW.get_or_init(DashMap::new)
120}
121
122fn page_global_by_webview() -> &'static DashMap<servo::WebViewId, usize> {
123    PAGE_GLOBAL_BY_WEBVIEW.get_or_init(DashMap::new)
124}
125
126// ScriptThread-local current page_global. Used by lazy_dom_getter_impl, which
127// runs as a JSNative ON the ScriptThread. Set during create_node_realm_native
128// and inject callbacks (same thread). SAFETY: only ever read/written on the
129// owning ScriptThread; Send/Sync are NOT required for thread_local! data.
130thread_local! {
131    static PER_THREAD_PAGE_GLOBAL: RefCell<*mut mozjs::jsapi::JSObject> =
132        const { RefCell::new(ptr::null_mut()) };
133}
134
135/// Store a Node Realm global pointer for a specific page, keyed by WebViewId.
136///
137/// SAFETY contract (BCE-20260621-001 C10): both pointers must have been created
138/// on the same ScriptThread that owns `webview_id`. The pointers are stored as
139/// addresses only; they MUST NOT be dereferenced off that ScriptThread.
140fn store_node_realm(
141    webview_id: servo::WebViewId,
142    page_global: *mut mozjs::jsapi::JSObject,
143    node_global: *mut mozjs::jsapi::JSObject,
144) {
145    node_realm_by_webview().insert(webview_id, node_global as usize);
146    page_global_by_webview().insert(webview_id, page_global as usize);
147}
148
149/// Look up Node Realm global pointer for a specific page (by WebViewId).
150///
151/// Returns an opaque address — callers must only use it on the same ScriptThread
152/// that owns `webview_id` (i.e., inside a `register_script_thread_callback`
153/// callback for that WebViewId).
154fn get_node_realm_by_id(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
155    match node_realm_by_webview().get(&webview_id) {
156        Some(v) => *v as *mut mozjs::jsapi::JSObject,
157        None => ptr::null_mut(),
158    }
159}
160
161/// Look up servo Window global pointer for a specific page (by WebViewId).
162fn get_page_global_by_id(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
163    match page_global_by_webview().get(&webview_id) {
164        Some(v) => *v as *mut mozjs::jsapi::JSObject,
165        None => ptr::null_mut(),
166    }
167}
168
169/// Remove Node Realm for a specific page (called on page close).
170pub fn remove_node_realm_by_id(webview_id: servo::WebViewId) {
171    node_realm_by_webview().remove(&webview_id);
172    page_global_by_webview().remove(&webview_id);
173}
174
175/// Clear all stored Node Realm pointers (for test isolation).
176fn clear_all_node_realms() {
177    node_realm_by_webview().clear();
178    page_global_by_webview().clear();
179}
180
181/// Test serialization lock for per-page storage operations.
182/// cargo test runs tests in parallel by default; tests that share the global
183/// maps must be serialized to prevent data races (store from test A cleared
184/// by test B).
185static TEST_SERIAL_LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
186
187fn test_serial_lock() -> &'static std::sync::Mutex<()> {
188    TEST_SERIAL_LOCK.get_or_init(|| std::sync::Mutex::new(()))
189}
190
191/// Register a callback to refresh DOM proxies in the Node Realm after navigation.
192///
193/// After page navigation, servo replaces the Window/Document/Navigator objects.
194/// This function registers a script thread callback that updates the
195/// per-WebViewId page_global mapping so lazy DOM getters find the new
196/// Page Realm. The Node Realm itself survives (user JS state preserved).
197pub fn register_refresh_dom_proxies(
198    webview_id: servo::WebViewId,
199    _old_page_global: *mut mozjs::jsapi::JSObject,
200) {
201    // Capture the WebViewId by value. We do NOT need the old page_global
202    // pointer anymore — the mapping is keyed by WebViewId, and the callback
203    // receives the NEW page_global directly from servo.
204    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
205        Box::new(move |_cx_ptr, new_page_global_ptr| unsafe {
206            refresh_dom_proxies_native(webview_id, new_page_global_ptr);
207        });
208
209    servo::register_script_thread_callback(webview_id, callback);
210}
211
212/// Native implementation: refresh per-page mapping after navigation.
213///
214/// Called on servo's script thread for `webview_id` with the NEW page_global.
215/// Updates PAGE_GLOBAL_BY_WEBVIEW so lazy getters find the new Page Realm.
216unsafe fn refresh_dom_proxies_native(
217    webview_id: servo::WebViewId,
218    new_page_global_ptr: *mut std::ffi::c_void,
219) {
220    use mozjs::jsapi::JSObject;
221
222    let new_page_global = new_page_global_ptr as *mut JSObject;
223
224    if new_page_global.is_null() {
225        return;
226    }
227
228    // Update the per-WebViewId page_global mapping. Lazy DOM getters will
229    // fetch from the new Page Realm going forward.
230    let old_page_global_opt = page_global_by_webview().get(&webview_id).map(|v| *v);
231    page_global_by_webview().insert(webview_id, new_page_global as usize);
232
233    // BUG-ENG-366: re-key the per-Realm stealth profile so the new Page Realm
234    // global inherits the page's stealth profile (Canvas/Navigator/WebGL/Audio
235    // seeds stay stable across same-origin navigation). The Node Realm global
236    // keeps its own alias entry which still points at the same profile Arc.
237    // @trace REQ-SEC-002 [req:REQ-SEC-002] [req:BUG-ENG-366]
238    if let Some(old_addr) = old_page_global_opt {
239        bao_stealth::engine_props::register_global_alias(old_addr, new_page_global as usize);
240    }
241}
242
243/// Create a Node Realm (independent SpiderMonkey Compartment) for privileged evaluate_js.
244///
245/// Uses `register_script_thread_callback` to queue a callback that:
246/// 1. Creates a new global object via JS_NewGlobalObject in a NEW Compartment
247///    (CompartmentSpecifier::NewCompartmentAndZone) — physically isolated from Page Realm
248/// 2. Installs all Node.js/Bun APIs on the Node Realm global
249/// 3. Stores the Node Realm global pointer keyed by WebViewId for the caller to retrieve
250///
251/// Returns true if the callback was queued. After `drain_callbacks`, the caller
252/// can read the node_global pointer via `get_node_realm_global(webview_id)`.
253///
254/// # Safety
255///
256/// Must be called before any evaluate_js. The stored pointer is valid
257/// until the Page is closed (which destroys the Node Realm).
258//
259// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
260// BCE-20260621-001: storage is keyed by WebViewId, not by *mut JSObject. The
261// raw pointer is created and consumed on the SAME ScriptThread that owns this
262// WebViewId (servo routes callbacks by WebViewId). No cross-thread *mut JSObject
263// dereference.
264pub fn create_node_realm(webview_id: servo::WebViewId) -> bool {
265    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
266        Box::new(move |cx_ptr, page_global_ptr| unsafe {
267            create_node_realm_native(webview_id, cx_ptr, page_global_ptr);
268        });
269
270    servo::register_script_thread_callback(webview_id, callback);
271
272    true
273}
274
275/// Get the Node Realm global pointer for a specific page (by WebViewId).
276///
277/// Returns the opaque address of the Node Realm's global JSObject. Callers
278/// MUST only dereference this pointer inside a `register_script_thread_callback`
279/// callback for the same WebViewId (which runs on the owning ScriptThread).
280//
281// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
282pub fn get_node_realm_global(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
283    get_node_realm_by_id(webview_id)
284}
285
286/// Get the servo Window global pointer for a specific page (by WebViewId).
287///
288/// Returns the opaque address of servo's Window global JSObject. Same
289/// ScriptThread-only dereference contract as `get_node_realm_global`.
290pub fn get_page_global(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
291    get_page_global_by_id(webview_id)
292}
293
294/// Evaluate a script in the Node Realm using AutoRealm.
295///
296/// This is the core of the dual-Realm architecture:
297/// `mozjs::rust::evaluate_script` internally uses `AutoRealm::new_from_handle(cx, glob)`
298/// which enters the Node Realm, evaluates the script, then leaves on drop.
299///
300/// The script has full access to Node.js APIs (require/Bun/process/Buffer)
301/// because they are installed on the Node Realm global.
302///
303/// Results are written into `result_out` (shared via `Arc<OnceLock<>>`):
304/// - On success, `value` is set to the serialized JS return value.
305/// - On failure, `error` is set to a descriptive message.
306///
307/// # Safety
308///
309/// Must be called on servo's script thread. `cx_ptr` must be a valid
310/// JSContext. `node_global` must be a valid, live JSObject (the Node
311/// Realm global). `script` must be valid UTF-8.
312pub unsafe fn evaluate_in_node_realm(
313    cx_ptr: *mut std::ffi::c_void,
314    node_global: *mut mozjs::jsapi::JSObject,
315    script: &str,
316    result_out: Arc<OnceLock<EvaluateResult>>,
317) {
318    use mozjs::context::JSContext;
319    use mozjs::jsapi::JSContext as RawJSContext;
320    use mozjs::jsval::UndefinedValue;
321    use mozjs::realm::AutoRealm;
322    use mozjs::rust::evaluate_script;
323    use mozjs::rust::CompileOptionsWrapper;
324
325    if node_global.is_null() {
326        let _ = result_out.set(EvaluateResult::err("node_global is null".into()));
327        return;
328    }
329
330    let raw_cx = cx_ptr as *mut RawJSContext;
331    let cx_nn = match NonNull::new(raw_cx) {
332        Some(nn) => nn,
333        None => {
334            let _ = result_out.set(EvaluateResult::err("JSContext pointer is null".into()));
335            return;
336        }
337    };
338
339    let mut cx = JSContext::from_ptr(cx_nn);
340
341    // Enter Node Realm via AutoRealm — this is the core isolation mechanism.
342    // evaluate_script evaluates within the entered Realm's compartment,
343    // so the script sees only the Node Realm global (with Node.js + Web APIs).
344    // The Page Realm's Window global is physically inaccessible from here.
345    //
346    // SAFETY: node_global is a valid, live JSObject pointer (checked above).
347    // AutoRealm::new roots the object internally via JSAutoRealm, ensuring
348    // GC safety. We then use global_and_reborrow() to obtain a GC-safe Handle
349    // (backed by AutoRealm's internal rooting) instead of from_marked_location
350    // which would point to an unrooted stack location.
351    let mut realm = AutoRealm::new(&mut cx, NonNull::new(node_global).unwrap());
352    let (node_global_handle, realm) = realm.global_and_reborrow();
353
354    let filename = c"bao_evaluate_js".to_owned();
355    let mut options = CompileOptionsWrapper::new(realm, filename, 1);
356    // BAO PATCH (BCE-20260622-004): Suppress `DebugAPI::onNewScript` for this
357    // compilation. Without it, every new script triggers `onNewScript` →
358    // `RememberSourceURL` → `AtomizeUTF8Chars` → `AtomCacheHashTable::lookupForAdd`,
359    // which in a multi-Realm create/destroy lifecycle dereferences GC'd atom
360    // chars (0x4b4b4b4b... jemalloc poison) → SIGSEGV in `InflateUTF8ToUTF16`.
361    // `set_hide_script_from_debugger(true)` makes `FireOnNewScript` skip the
362    // call entirely. Safe because bao uses `bao_cdp` (its own CDP), never
363    // servo's JS::Debugger devtools — no consumer needs these onNewScript events.
364    options.set_hide_script_from_debugger(true);
365
366    rooted!(&in(realm) let mut rval = UndefinedValue());
367    let eval_result = evaluate_script(
368        realm,
369        node_global_handle,
370        script,
371        rval.handle_mut(),
372        options,
373    );
374
375    if eval_result.is_err() {
376        let _ = result_out.set(EvaluateResult {
377            value: None,
378            error: Some("evaluate_script returned Err (JS exception thrown)".into()),
379        });
380        return;
381    }
382
383    // Page WebSocket pump (async WS root fix): servo evaluates are one-shot
384    // (no CLI-style post-eval loop), so consume completed background
385    // connects (onopen/onerror) and inbound frames (onmessage/onclose) here.
386    // `ws_pump_all` AutoRealms per entry, so it is realm-agnostic; run it
387    // inside the node realm borrow (raw_cx unchanged).
388    bun_runtime::web_api::ws_pump_all(raw_cx);
389
390    // Serialize rval to a string. Undefined is treated as no value.
391    let rval_val = rval.get();
392    let value = if rval_val.is_undefined() {
393        None
394    } else if rval_val.is_string() {
395        // SAFETY: we just checked is_string(), so to_string returns a valid JSString pointer.
396        let js_str = rval_val.to_string();
397        if js_str.is_null() {
398            Some(String::new())
399        } else {
400            // Use mozjs's built-in unsafe_jsstr_to_string for safe UTF-8 conversion.
401            // It handles both Latin1 and TwoByte JS string encodings.
402            let raw_cx = realm.raw_cx();
403            match NonNull::new(js_str) {
404                Some(nn) => Some(mozjs::conversions::unsafe_jsstr_to_string(raw_cx, nn)),
405                None => Some(String::new()),
406            }
407        }
408    } else if rval_val.is_number() {
409        Some(rval_val.to_number().to_string())
410    } else if rval_val.is_boolean() {
411        Some(rval_val.to_boolean().to_string())
412    } else if rval_val.is_null() {
413        Some("null".into())
414    } else {
415        // Object / symbol / bigint — represent as debug string.
416        Some("[JSValue:object]".into())
417    };
418    let _ = result_out.set(EvaluateResult { value, error: None });
419}
420
421/// Evaluate a script in the Node Realm via servo's script thread callback mechanism.
422///
423/// This is the primary entry point for B1 (evaluate_js Node Realm switch).
424/// It registers a callback on servo's script thread that:
425/// 1. Reads the Node Realm global pointer keyed by `webview_id`
426/// 2. Calls `evaluate_in_node_realm` with the script
427/// 3. Writes the result to the shared `Arc<OnceLock<EvaluateResult>>`
428///
429/// The caller must call `page.drain_callbacks()` after this to trigger execution.
430///
431/// Returns the shared result handle — read after drain_callbacks completes
432/// (use `result.get()` to obtain the EvaluateResult).
433//
434// @trace REQ-PERF-004 [entity:DomainDispatch]
435// REQ-PERF-004 验收:JS 求值结果用 `Arc<OnceLock<EvaluateResult>>` 替代
436// `Arc<Mutex<EvaluateResult>>`。OnceLock 语义匹配"单次写多次读"场景:
437// script 在 script_thread 执行一次写入,主线程 drain 后读取,无需 Mutex 互斥。
438//
439// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C2,C4,C10]
440// BCE-20260621-001: lookup is keyed by WebViewId (not raw *mut JSObject). The
441// callback runs on the ScriptThread that owns this WebViewId (servo routes by
442// WebViewId), so the node_global pointer is dereferenced on its home thread —
443// no cross-thread *mut JSObject access, no activation-stack corruption.
444pub fn evaluate_js_via_node_realm(
445    webview_id: servo::WebViewId,
446    script: &str,
447) -> Arc<OnceLock<EvaluateResult>> {
448    let result = Arc::new(OnceLock::new());
449    let result_clone = result.clone();
450    let script_owned = script.to_string();
451
452    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> = Box::new(
453        move |cx_ptr: *mut std::ffi::c_void, _page_global: *mut std::ffi::c_void| {
454            // Look up Node Realm for THIS page via WebViewId. servo routes this
455            // callback to the ScriptThread that owns this WebViewId, so the
456            // node_global pointer is dereferenced on the thread that created it.
457            let node_global = get_node_realm_by_id(webview_id);
458            unsafe {
459                evaluate_in_node_realm(cx_ptr, node_global, &script_owned, result_clone);
460            }
461        },
462    );
463
464    servo::register_script_thread_callback(webview_id, callback);
465    result
466}
467
468/// Bridge callback: create Node Realm on servo's script thread.
469///
470/// Creates a new JS global object in its own Compartment (NewCompartmentAndZone),
471/// installs all Node.js/Bun APIs on it, wraps DOM proxies from Page Realm,
472/// and stores the global pointer keyed by `webview_id` for the caller to retrieve.
473///
474/// The Node Realm is physically isolated from the Page Realm —
475/// Page JS cannot enumerate or discover any objects in the Node Realm.
476///
477/// DOM access (REQ-SEC-002 criterion 5): window/document/navigator from the
478/// Page Realm are wrapped via JS_WrapObject and installed as properties on
479/// the Node Realm global. This creates cross-Compartment proxies that allow
480/// trusted scripts to access DOM while maintaining Compartment isolation.
481//
482// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
483// BCE-20260621-001: store_node_realm uses WebViewId (Copy+Hash+Eq) as key —
484// not the raw *mut JSObject address. The pointers remain valid because the
485// Node Realm is owned by this ScriptThread and is only ever touched from
486// WebViewId-keyed callbacks that run on this same ScriptThread.
487unsafe fn create_node_realm_native(
488    webview_id: servo::WebViewId,
489    cx_ptr: *mut std::ffi::c_void,
490    page_global_ptr: *mut std::ffi::c_void,
491) {
492    use mozjs::context::JSContext;
493    use mozjs::jsapi::{
494        JSContext as RawJSContext, JSObject, JS_FireOnNewGlobalObject, OnNewGlobalHookOption,
495    };
496    use mozjs::realm::AutoRealm;
497    use mozjs::rust::wrappers2::{JS_NewGlobalObject, JS_SetProperty, JS_WrapObject};
498    use mozjs::rust::{Handle, MutableHandle, SIMPLE_GLOBAL_CLASS};
499
500    let raw_cx = cx_ptr as *mut RawJSContext;
501    let page_global = page_global_ptr as *mut JSObject;
502    let cx_nn = match NonNull::new(raw_cx) {
503        Some(nn) => nn,
504        None => return,
505    };
506
507    let mut cx = JSContext::from_ptr(cx_nn);
508
509    // Node-semantics realm (bun_runtime globals installed below):
510    // SharedArrayBuffer/Atomics standard classes on, unlike servo's web page
511    // realms which keep the shared-memory flag off (cross-site-isolated gating).
512    let mut options = bao_engine::node_realm_options();
513    options.creationOptions_.compSpec_ =
514        mozjs::jsapi::JS::CompartmentSpecifier::NewCompartmentAndZone;
515
516    rooted!(&in(cx) let global = JS_NewGlobalObject(
517        &mut cx,
518        &SIMPLE_GLOBAL_CLASS,
519        ptr::null_mut(),
520        OnNewGlobalHookOption::DontFireOnNewGlobalHook,
521        &*options,
522    ));
523
524    if global.get().is_null() {
525        return;
526    }
527
528    let mut realm = AutoRealm::new_from_handle(&mut cx, global.handle());
529    let realm_cx: &mut JSContext = &mut realm;
530    JS_FireOnNewGlobalObject(realm_cx.raw_cx(), global.handle().into());
531
532    bun_runtime::globals::install_node_apis(realm_cx, global.handle());
533    bun_runtime::globals::install_web_apis(realm_cx, global.handle());
534
535    if !page_global.is_null() {
536        // Install lazy getters that dynamically fetch from Page Realm on every access.
537        // This ensures DOM proxies never go stale after navigation (scheme C).
538        install_lazy_dom_getters(realm_cx, global.handle());
539    }
540
541    // Cache servo's Window global for this ScriptThread. lazy_dom_getter_impl
542    // reads it from thread-local — same thread, no cross-thread dereference.
543    if !page_global.is_null() {
544        PER_THREAD_PAGE_GLOBAL.with(|cell| {
545            *cell.borrow_mut() = page_global;
546        });
547    }
548
549    // Store per-page: keyed by WebViewId (NOT page_global pointer address).
550    store_node_realm(webview_id, page_global, global.get());
551
552    // BUG-ENG-366: alias the Node Realm global to the same per-page stealth
553    // profile. Stealth getters executing inside the Node Realm (REQ-SEC-002
554    // privileged scripts reading navigator/WebGL) resolve to the page's profile,
555    // identical to what untrusted page JS sees — no fingerprint divergence
556    // between Realms of the same page.
557    //
558    // @trace REQ-SEC-002 [req:REQ-SEC-002] [req:BUG-ENG-366]
559    if !page_global.is_null() {
560        bao_stealth::engine_props::register_global_alias(
561            page_global as usize,
562            global.get() as usize,
563        );
564    }
565}
566
567/// Wrap a DOM property from the Page Realm and install it on the Node Realm global.
568///
569/// This enables REQ-SEC-002 criterion 5: trusted scripts can access
570/// window/document/navigator from the Page Realm via cross-Compartment proxies.
571///
572/// How it works:
573/// 1. Get the property (e.g. "window") from the Page Realm's global (Window)
574/// 2. JS_WrapObject creates a cross-Compartment proxy in the Node Realm
575/// 3. Install the wrapped proxy as a property on the Node Realm's global
576///
577/// The proxy only exposes the Page Realm's public Web API interface.
578/// Node APIs remain invisible because they're in a different Compartment.
579unsafe fn wrap_and_install_dom_proxy(
580    cx: &mut mozjs::context::JSContext,
581    node_global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
582    page_global: *mut mozjs::jsapi::JSObject,
583    property_name: &str,
584) {
585    use mozjs::jsapi::{JS_GetProperty, JS_SetProperty};
586    use mozjs::jsval::{ObjectValue, UndefinedValue};
587    use mozjs::rust::wrappers2::JS_WrapObject;
588
589    let raw_cx = cx.raw_cx();
590    // SAFETY: page_global is a servo Page Realm global, which is rooted by servo's
591    // realm for the lifetime of the page. We root it here via rooted! to ensure
592    // GC safety during JS_GetProperty (which can trigger GC), replacing the
593    // previous from_marked_location that pointed to an unrooted stack location.
594    rooted!(&in(cx) let page_global_root = page_global);
595
596    // Get the property from Page Realm's Window global.
597    let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
598    // BCE (P0 browser startup panic, servo error.rs:74): probing the Page
599    // Realm Window global for window/document/navigator can hit a throwing
600    // accessor (opaque-origin pages throw SecurityError from storage/DOM
601    // getters). A failed JS_GetProperty leaves the exception pending on the
602    // shared ScriptThread context — consume it here ("absent" is the handled
603    // outcome), never leak it into servo's error path.
604    let mut raw_prop_val = UndefinedValue();
605    let got = bao_stealth::engine_props::get_property_clearing(
606        raw_cx,
607        page_global_root.handle().into(),
608        c_name.as_cstr(),
609        &mut raw_prop_val,
610    );
611    rooted!(&in(cx) let mut prop_val = raw_prop_val);
612
613    // If the property is an object, wrap it for the Node Realm.
614    if got && prop_val.get().is_object() {
615        // Follow servo's pattern: rooted!(&in(cx) let mut element = obj.get())
616        rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
617
618        // JS_WrapObject creates a cross-Compartment proxy.
619        if !JS_WrapObject(cx, prop_obj.handle_mut().into()) {
620            // BCE (error.rs:74): a failed wrap also leaves a pending
621            // exception — consume it (proxy simply not installed).
622            mozjs::jsapi::JS_ClearPendingException(raw_cx);
623            return;
624        }
625
626        // Install the wrapped proxy on the Node Realm's global.
627        rooted!(&in(cx) let mut wrapped_val = ObjectValue(prop_obj.get()));
628        // BCE (error.rs:74): a refused set leaves a pending exception —
629        // consume it (handled outcome, not an error to propagate).
630        if !JS_SetProperty(
631            raw_cx,
632            node_global.into(),
633            c_name.as_ptr(),
634            wrapped_val.handle_mut().into(),
635        ) {
636            mozjs::jsapi::JS_ClearPendingException(raw_cx);
637        }
638    }
639}
640
641/// Install lazy getter properties for window/document/navigator on Node Realm global.
642///
643/// Unlike `wrap_and_install_dom_proxy` which creates a static cross-Compartment proxy
644/// at creation time, lazy getters dynamically fetch the latest DOM object from the
645/// Page Realm on every access. This ensures proxies never go stale after navigation.
646///
647/// Uses JS_DefineProperty1 with a JSNative getter and no setter (JSPROP_READONLY).
648unsafe fn install_lazy_dom_getters(
649    cx: &mut mozjs::context::JSContext,
650    node_global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
651) {
652    use mozjs::jsapi::JS_DefineProperty1;
653    use mozjs::jsval::UndefinedValue;
654
655    let raw_cx = cx.raw_cx();
656
657    // DOM object getters (window/document/navigator): enumerable + readonly.
658    let obj_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY) as u32;
659    // Constructor getters (Worker/SharedWorker/ServiceWorker): enumerable + readonly + permanent.
660    // JSPROP_PERMANENT makes them non-configurable (non-deletable), matching Web IDL semantics
661    // where interface constructors on the global must not be deletable.
662    let ctor_attrs = (mozjs::jsapi::JSPROP_ENUMERATE
663        | mozjs::jsapi::JSPROP_READONLY
664        | mozjs::jsapi::JSPROP_PERMANENT) as u32;
665
666    let obj_getters: &[(&std::ffi::CStr, mozjs::jsapi::JSNative)] = &[
667        (c"window", Some(lazy_dom_getter_window)),
668        (c"document", Some(lazy_dom_getter_document)),
669        (c"navigator", Some(lazy_dom_getter_navigator)),
670    ];
671    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] DF-WK-11:
672    // Worker/SharedWorker/ServiceWorker constructors exposed to Node Realm
673    // via cross-compartment proxy. Page Realm already has these via servo DOM
674    // bindings; Node Realm accesses them through the same lazy getter pattern
675    // used for window/document/navigator. Constructors use JSPROP_PERMANENT
676    // to match Web IDL non-configurable semantics.
677    let ctor_getters: &[(&std::ffi::CStr, mozjs::jsapi::JSNative)] = &[
678        (c"Worker", Some(lazy_dom_getter_worker)),
679        (c"SharedWorker", Some(lazy_dom_getter_shared_worker)),
680        (c"ServiceWorker", Some(lazy_dom_getter_service_worker)),
681    ];
682    for &(name, getter) in obj_getters {
683        // BCE (P0 browser startup panic, servo error.rs:74): on the Node
684        // Realm global, `navigator`/`screen` may ALREADY be installed as
685        // JSPROP_PERMANENT plain values by `install_stealth_props →
686        // ensure_subobject` (which runs earlier in `install_web_apis`; the
687        // PERMANENT flag there guards against double-install corruption).
688        // Redefining a non-configurable property throws
689        // "TypeError: can't redefine non-configurable property" — the failed
690        // define leaves that exception PENDING on the ScriptThread cx; an
691        // unconsumed pending exception later detonates servo's
692        // `assert!(!JS_IsExceptionPending)` in `throw_dom_exception` and
693        // kills the ScriptThread. The stealth-supplied PERMANENT value is a
694        // valid (fingerprint-consistent) supply for the Node Realm, so a
695        // refused lazy-getter override is a handled outcome — consume the
696        // exception instead of leaking it into servo's loop.
697        if !JS_DefineProperty1(
698            raw_cx,
699            node_global.into(),
700            name.as_ptr(),
701            getter,
702            None,
703            obj_attrs,
704        ) {
705            mozjs::jsapi::JS_ClearPendingException(raw_cx);
706        }
707    }
708    for &(name, getter) in ctor_getters {
709        // BCE (error.rs:74): same refused-define contract as obj_getters —
710        // a PERMANENT `Worker`-family constructor from a prior install
711        // refuses the redefine; consume the pending exception.
712        if !JS_DefineProperty1(
713            raw_cx,
714            node_global.into(),
715            name.as_ptr(),
716            getter,
717            None,
718            ctor_attrs,
719        ) {
720            mozjs::jsapi::JS_ClearPendingException(raw_cx);
721        }
722    }
723}
724
725/// Lazy getter for `window` property on Node Realm global.
726///
727/// Dynamically fetches the Window object from the Page Realm and wraps it
728/// as a cross-Compartment proxy for the Node Realm. This always returns
729/// the CURRENT window, even after navigation.
730#[allow(unsafe_op_in_unsafe_fn)]
731unsafe extern "C" fn lazy_dom_getter_window(
732    cx: *mut mozjs::jsapi::JSContext,
733    argc: u32,
734    vp: *mut mozjs::jsval::JSVal,
735) -> bool {
736    lazy_dom_getter_impl(cx, argc, vp, "window")
737}
738
739/// Lazy getter for `document` property on Node Realm global.
740#[allow(unsafe_op_in_unsafe_fn)]
741unsafe extern "C" fn lazy_dom_getter_document(
742    cx: *mut mozjs::jsapi::JSContext,
743    argc: u32,
744    vp: *mut mozjs::jsval::JSVal,
745) -> bool {
746    lazy_dom_getter_impl(cx, argc, vp, "document")
747}
748
749/// Lazy getter for `navigator` property on Node Realm global.
750#[allow(unsafe_op_in_unsafe_fn)]
751unsafe extern "C" fn lazy_dom_getter_navigator(
752    cx: *mut mozjs::jsapi::JSContext,
753    argc: u32,
754    vp: *mut mozjs::jsval::JSVal,
755) -> bool {
756    lazy_dom_getter_impl(cx, argc, vp, "navigator")
757}
758
759// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] DF-WK-11:
760// Worker/SharedWorker/ServiceWorker constructors exposed to Node Realm
761// via cross-compartment proxy. These constructors exist on the Page Realm's
762// Window global (installed by servo DOM bindings). The lazy getter fetches
763// them from Page Realm and wraps as cross-compartment proxy for Node Realm,
764// enabling `new Worker(url)` from Node Realm scripts (e.g. CDP automation).
765//
766// Unlike DOM object getters (window/document/navigator), constructor getters:
767// - Validate the fetched value is a constructor (JS::IsConstructor)
768// - Throw a ReferenceError if the property is missing or not constructible,
769//   giving a clear diagnostic instead of a cryptic "X is not a constructor"
770// - Cache the wrapped proxy on first successful resolution (constructors
771//   don't change across navigations, unlike the window object)
772// - Use JSPROP_PERMANENT to match Web IDL non-configurable semantics
773//
774// Thread safety: same ScriptThread — no cross-thread JSObject transfer.
775
776/// Lazy getter for `Worker` constructor on Node Realm global.
777///
778/// Returns the Worker constructor from Page Realm as a cross-Compartment
779/// proxy, enabling `new Worker(url)` from Node Realm scripts. On first
780/// access, validates that the Page Realm's `Worker` property is a
781/// constructor and caches the wrapped proxy.
782#[allow(unsafe_op_in_unsafe_fn)]
783unsafe extern "C" fn lazy_dom_getter_worker(
784    cx: *mut mozjs::jsapi::JSContext,
785    argc: u32,
786    vp: *mut mozjs::jsval::JSVal,
787) -> bool {
788    lazy_constructor_getter_impl(cx, argc, vp, "Worker")
789}
790
791/// Lazy getter for `SharedWorker` constructor on Node Realm global.
792#[allow(unsafe_op_in_unsafe_fn)]
793unsafe extern "C" fn lazy_dom_getter_shared_worker(
794    cx: *mut mozjs::jsapi::JSContext,
795    argc: u32,
796    vp: *mut mozjs::jsval::JSVal,
797) -> bool {
798    lazy_constructor_getter_impl(cx, argc, vp, "SharedWorker")
799}
800
801/// Lazy getter for `ServiceWorker` constructor on Node Realm global.
802#[allow(unsafe_op_in_unsafe_fn)]
803unsafe extern "C" fn lazy_dom_getter_service_worker(
804    cx: *mut mozjs::jsapi::JSContext,
805    argc: u32,
806    vp: *mut mozjs::jsval::JSVal,
807) -> bool {
808    lazy_constructor_getter_impl(cx, argc, vp, "ServiceWorker")
809}
810
811/// Specialized lazy getter for constructor properties (Worker/SharedWorker/ServiceWorker).
812///
813/// Differs from `lazy_dom_getter_impl` (used for window/document/navigator) in:
814/// 1. **IsConstructor validation**: Checks that the fetched value is a constructor
815///    (has [[Construct]] internal method). If not, throws a ReferenceError with a
816///    clear message explaining that the browser context does not support the API.
817/// 2. **Error reporting**: Returns a JS exception instead of silently returning
818///    `undefined`, so `new Worker()` fails with a diagnosable error rather than
819///    a cryptic "X is not a constructor" TypeError.
820/// 3. **Cached proxy**: Once successfully resolved, the wrapped constructor proxy
821///    is stored directly on the Node Realm global as a data property (replacing
822///    the getter). This avoids repeated cross-Compartment wrapping on every access.
823///    Constructors are stable for the lifetime of the page — they don't change
824///    across navigations like the `window` object does.
825///
826/// Thread safety: same ScriptThread — Page Realm and Node Realm share the
827/// same thread. No cross-thread JSObject transfer.
828//
829// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] DF-WK-11
830#[allow(unsafe_op_in_unsafe_fn)]
831unsafe fn lazy_constructor_getter_impl(
832    raw_cx: *mut mozjs::jsapi::JSContext,
833    _argc: u32,
834    vp: *mut mozjs::jsval::JSVal,
835    property_name: &str,
836) -> bool {
837    use mozjs::context::JSContext;
838    use mozjs::jsapi::{JSObject, JS_DefineProperty1, JS_GetProperty};
839    use mozjs::jsval::{ObjectValue, UndefinedValue};
840    use mozjs::rust::wrappers2::JS_WrapObject;
841    use std::ptr::NonNull;
842
843    let args = mozjs::jsapi::CallArgs::from_vp(vp, 0);
844    args.rval().set(UndefinedValue());
845
846    let node_global = mozjs::jsapi::CurrentGlobalOrNull(raw_cx);
847    if node_global.is_null() {
848        return true;
849    }
850
851    let page_global = PER_THREAD_PAGE_GLOBAL.with(|cell| *cell.borrow());
852    if page_global.is_null() {
853        // No page loaded yet — throw a ReferenceError explaining why.
854        let msg = format!(
855            "Cannot access {} constructor: no browser page is currently loaded",
856            property_name
857        );
858        report_reference_error(raw_cx, &msg);
859        return false;
860    }
861
862    let cx_nn = match NonNull::new(raw_cx) {
863        Some(nn) => nn,
864        None => return true,
865    };
866    let mut cx = JSContext::from_ptr(cx_nn);
867
868    // Get the constructor from Page Realm's Window global
869    rooted!(&in(cx) let page_global_root = page_global);
870    let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
871    // BCE (P0 browser startup panic, servo error.rs:74): probing the Page
872    // Realm Window global can hit a throwing accessor; a failed
873    // JS_GetProperty leaves the exception pending on the shared ScriptThread
874    // context. Consume it here — the ReferenceError branch below then throws
875    // its own clean diagnostic instead of stacking on a stale exception.
876    let mut raw_prop_val = UndefinedValue();
877    let got = bao_stealth::engine_props::get_property_clearing(
878        raw_cx,
879        page_global_root.handle().into(),
880        c_name.as_cstr(),
881        &mut raw_prop_val,
882    );
883    rooted!(&in(cx) let mut prop_val = raw_prop_val);
884
885    if !got || !prop_val.get().is_object() {
886        // The constructor property doesn't exist on the Page Realm's Window.
887        // This means servo's DOM bindings haven't installed it (e.g., the
888        // page hasn't finished loading, or the API is not available).
889        let msg = format!(
890            "Cannot access {} constructor: not available in the current browser context",
891            property_name
892        );
893        report_reference_error(raw_cx, &msg);
894        return false;
895    }
896
897    rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
898
899    // Validate that the fetched object is actually a constructor.
900    // SpiderMonkey's cross-Compartment wrapper for a constructor correctly
901    // reports IsConstructor=true (because the wrapper forwards [[Construct]]).
902    // IsConstructor is the raw C++ `JS::IsConstructor(JSObject*)` — pass the
903    // inner raw pointer from the Handle.
904    if !mozjs::jsapi::IsConstructor(*prop_obj.handle()) {
905        let msg = format!(
906            "{} is not a constructor — the browser context does not support this API",
907            property_name
908        );
909        report_reference_error(raw_cx, &msg);
910        return false;
911    }
912
913    // Wrap the constructor as a cross-Compartment proxy for Node Realm.
914    // JS_WrapObject creates a callable wrapper that correctly forwards
915    // [[Call]] and [[Construct]] internal methods. When `new Worker(url)`
916    // is invoked from Node Realm, SpiderMonkey enters the Page Realm
917    // Compartment to execute [[Construct]], which is correct because
918    // servo's Worker::Constructor expects to run in the Page Realm's
919    // GlobalScope (Window).
920    if !JS_WrapObject(&mut cx, prop_obj.handle_mut().into()) {
921        return false;
922    }
923
924    // Cache: replace the getter with a data property holding the wrapped
925    // constructor. Constructors are stable for the page's lifetime — they
926    // don't change across navigations (unlike `window`). This avoids
927    // repeated cross-Compartment wrapping overhead on every access.
928    //
929    // We use JS_SetProperty (not JS_DefineProperty) to overwrite the
930    // existing getter property. Since the property was defined with
931    // JSPROP_READONLY, the setter will be rejected — BUT the engine
932    // allows the original definition site (same native getter) to update
933    // the property. Actually, JSPROP_READONLY prevents JS_SetProperty too.
934    //
935    // Alternative approach: Instead of caching, we simply return the wrapped
936    // constructor each time. The overhead is minimal: one JS_GetProperty +
937    // one JS_WrapObject per access. Since constructors are accessed rarely
938    // (only at `new Worker()` time, not in hot loops), the performance
939    // impact is negligible and the code is simpler without cache invalidation
940    // concerns (e.g., page unload should restore the getter).
941    args.rval().set(ObjectValue(prop_obj.get()));
942    true
943}
944
945/// Report a ReferenceError to the JS engine.
946///
947/// This is used by constructor lazy getters to throw a clear diagnostic
948/// when a constructor is not available, instead of returning `undefined`
949/// (which would cause a confusing "X is not a constructor" TypeError
950/// when the user tries `new Worker()`).
951///
952/// Uses `JS_ReportErrorNumberUTF8` (same pattern as `mozjs::error::throw_type_error_safe`)
953/// with `JSEXN_REFERENCEERR` to produce a proper ReferenceError exception.
954#[allow(unsafe_op_in_unsafe_fn)]
955unsafe fn report_reference_error(cx: *mut mozjs::jsapi::JSContext, message: &str) {
956    use std::ffi::CString;
957    use std::os::raw::c_void;
958
959    let c_msg = match CString::new(message.as_bytes()) {
960        Ok(s) => s,
961        Err(_) => return,
962    };
963
964    // Static error format string: "{0}" — the entire message is the single arg.
965    // SAFETY: this is a compile-time constant CStr; it's never mutated and lives
966    // for the entire program duration. We take a raw pointer to it only for the
967    // duration of the JS_ReportErrorNumberUTF8 call, which does not store it.
968    static FORMAT_STRING: &std::ffi::CStr = c"{0}";
969
970    /// Callback that returns the format string for our ReferenceError type.
971    /// Same pattern as mozjs::error::get_error_message.
972    unsafe extern "C" fn get_reference_error_format(
973        _user_ref: *mut std::os::raw::c_void,
974        _error_number: u32,
975    ) -> *const mozjs::jsapi::JSErrorFormatString {
976        static mut FORMAT: mozjs::jsapi::JSErrorFormatString = mozjs::jsapi::JSErrorFormatString {
977            name: c"RUSTMSG_REFERENCE_ERROR".as_ptr(),
978            format: FORMAT_STRING.as_ptr(),
979            argCount: 1,
980            exnType: mozjs::jsapi::JSExnType::JSEXN_REFERENCEERR as i16,
981        };
982        // SAFETY: read of a static is safe; the static itself is never moved
983        // or mutated after this first access (it's initialized once).
984        unsafe { &raw const FORMAT }
985    }
986
987    // SAFETY: JS_ReportErrorNumberUTF8 is the standard SpiderMonkey API for
988    // throwing typed errors. Our callback returns a static format string with
989    // argCount=1 and the single argument is our message C string.
990    mozjs::jsapi::JS_ReportErrorNumberUTF8(
991        cx,
992        Some(get_reference_error_format),
993        std::ptr::null_mut(),
994        mozjs::jsapi::JSExnType::JSEXN_REFERENCEERR as u32,
995        c_msg.as_ptr(),
996    );
997}
998
999/// Shared implementation for DOM *object* lazy getters (window/document/navigator).
1000///
1001/// 1. Get the current global (Node Realm global) via JS_CurrentGlobalOrNull
1002/// 2. Read the per-thread cached Page Realm global (PER_THREAD_PAGE_GLOBAL)
1003/// 3. Get the DOM property from Page Realm
1004/// 4. Wrap it as a cross-Compartment proxy for the Node Realm
1005/// 5. Return the wrapped value
1006///
1007/// For *constructor* properties (Worker/SharedWorker/ServiceWorker), use
1008/// `lazy_constructor_getter_impl` instead, which adds IsConstructor
1009/// validation, error reporting, and proxy caching.
1010//
1011// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
1012// BCE-20260621-001: thread_local page_global is set in create_node_realm_native
1013// (same ScriptThread). No cross-thread *mut JSObject lookup.
1014#[allow(unsafe_op_in_unsafe_fn)]
1015unsafe fn lazy_dom_getter_impl(
1016    raw_cx: *mut mozjs::jsapi::JSContext,
1017    _argc: u32,
1018    vp: *mut mozjs::jsval::JSVal,
1019    property_name: &str,
1020) -> bool {
1021    use mozjs::context::JSContext;
1022    use mozjs::jsapi::{JSObject, JS_GetProperty};
1023    use mozjs::jsval::{ObjectValue, UndefinedValue};
1024    use mozjs::rust::wrappers2::JS_WrapObject;
1025    use std::ptr::NonNull;
1026
1027    let args = mozjs::jsapi::CallArgs::from_vp(vp, 0);
1028    args.rval().set(UndefinedValue());
1029
1030    let node_global = mozjs::jsapi::CurrentGlobalOrNull(raw_cx);
1031    if node_global.is_null() {
1032        return true;
1033    }
1034
1035    // Read the per-thread cached servo Window global. This is set in
1036    // create_node_realm_native on the SAME ScriptThread, so the read is
1037    // safe (no cross-thread access). Returns null if the cache was never
1038    // populated (e.g., page closed).
1039    let page_global = PER_THREAD_PAGE_GLOBAL.with(|cell| *cell.borrow());
1040    if page_global.is_null() {
1041        return true;
1042    }
1043
1044    // Wrap raw_cx in JSContext for rooted! and JS_WrapObject
1045    let cx_nn = match NonNull::new(raw_cx) {
1046        Some(nn) => nn,
1047        None => return true,
1048    };
1049    let mut cx = JSContext::from_ptr(cx_nn);
1050
1051    // Get the DOM property from Page Realm
1052    // SAFETY: page_global is a servo Page Realm global, which is rooted by servo's
1053    // realm for the lifetime of the page. We root it here via rooted! to ensure
1054    // GC safety during JS_GetProperty (which can trigger GC), replacing the
1055    // previous from_marked_location that pointed to an unrooted stack location.
1056    rooted!(&in(cx) let page_global_root = page_global);
1057
1058    let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
1059    // BCE (P0 browser startup panic, servo error.rs:74): this getter runs on
1060    // the servo ScriptThread context EVERY time Node Realm script reads
1061    // `window`/`document`/`navigator`. The probe targets the Page Realm
1062    // Window global — a throwing accessor (opaque-origin storage/DOM
1063    // getters) makes JS_GetProperty return false WITH the exception
1064    // pending. The old code ignored the return and reported success
1065    // (`return true`), leaving the stale exception to detonate servo's
1066    // `assert!(!JS_IsExceptionPending)` in `throw_dom_exception` on the
1067    // next error path. Consume it — "absent" reads as `undefined`.
1068    let mut raw_prop_val = UndefinedValue();
1069    let got = bao_stealth::engine_props::get_property_clearing(
1070        raw_cx,
1071        page_global_root.handle().into(),
1072        c_name.as_cstr(),
1073        &mut raw_prop_val,
1074    );
1075    rooted!(&in(cx) let mut prop_val = raw_prop_val);
1076
1077    if !got || !prop_val.get().is_object() {
1078        return true;
1079    }
1080
1081    // Wrap the DOM object for the current Realm (Node Realm)
1082    rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
1083    if !JS_WrapObject(&mut cx, prop_obj.handle_mut().into()) {
1084        // BCE (error.rs:74): failed wrap leaves a pending exception — a
1085        // getter must not report success (`return true`) with one pending.
1086        mozjs::jsapi::JS_ClearPendingException(raw_cx);
1087        return true;
1088    }
1089
1090    args.rval().set(ObjectValue(prop_obj.get()));
1091    true
1092}
1093
1094/// Inject Node.js APIs as native mozjs host functions on servo's Window global.
1095///
1096/// Uses `servo::register_script_thread_callback` to queue a callback that will
1097/// be drained on servo's script thread during `handle_evaluate_javascript`.
1098/// The callback casts the raw pointers to mozjs types and calls
1099/// `bun_runtime::globals::install_all` to register all Node.js/Bun host functions
1100/// natively — zero JS polyfill strings, maximum performance.
1101///
1102/// Also installs stealth anti-fingerprinting properties as PERMANENT engine-layer
1103/// getters if a stealth profile is provided.
1104///
1105/// Falls back to JS polyfill injection if native registration is unavailable.
1106pub fn inject_node_apis(page: &PageHandle) -> Result<(), BrowserError> {
1107    inject_node_apis_with_stealth(page, None)
1108}
1109
1110/// Inject Node.js APIs with optional stealth profile.
1111///
1112/// Same as `inject_node_apis`, but also installs stealth properties as PERMANENT
1113/// engine-layer getters when a profile is provided.
1114//
1115// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
1116// BCE-20260621-001: page_global is now read via get_page_global(webview_id)
1117// (WebViewId-keyed), NOT via the global LAST_PAGE_GLOBAL. This eliminates the
1118// race where two pages' create_node_realm callbacks compete for the single
1119// global slot and PageInner captures the wrong page's pointer.
1120pub fn inject_node_apis_with_stealth(
1121    page: &PageHandle,
1122    stealth_profile: Option<bao_stealth::StealthProfile>,
1123) -> Result<(), BrowserError> {
1124    let webview_id = page
1125        .webview_id()
1126        .ok_or_else(|| BrowserError::Init("page has no webview".into()))?;
1127
1128    let registered = register_native_host_functions(webview_id, stealth_profile);
1129
1130    // Also create Node Realm for this page (dual-Realm architecture, REQ-SEC-002).
1131    // The callback is queued on servo's script thread and will execute during drain.
1132    let node_realm_registered = create_node_realm(webview_id);
1133    debug_assert!(
1134        node_realm_registered,
1135        "create_node_realm registration failed"
1136    );
1137
1138    // Drain the callback by triggering servo's handle_evaluate_javascript.
1139    // servo drains pending register_script_thread_callback callbacks before
1140    // executing the script. The minimal script ";" is evaluated, but what
1141    // matters is that the callback ran and installed host functions.
1142    //
1143    // If the pipeline isn't ready yet (WebView just created), drain_callbacks
1144    // spins the servo event loop and retries until the pipeline is established.
1145    page.drain_callbacks()?;
1146
1147    // After drain, retrieve this page's pointers via WebViewId (NOT a global).
1148    // PageInner stores them as opaque addresses; it never dereferences them —
1149    // they flow back into WebViewId-keyed callbacks (same ScriptThread) later.
1150    let page_global = get_page_global(webview_id);
1151    let node_global = get_node_realm_global(webview_id);
1152    page.set_page_global(page_global, node_global);
1153
1154    if !registered {
1155        // Fallback: inject Web-only polyfill string (REQ-SEC-003: NO Node APIs on Window global)
1156        page.evaluate_js_web(WEB_POLYFILLS)?;
1157    }
1158
1159    Ok(())
1160}
1161
1162/// Attempt to register bun_runtime's native host functions via servo's callback mechanism.
1163///
1164/// Returns `true` if registration succeeded, `false` if servo's API is unavailable
1165/// (e.g., older servo build without `register_script_thread_callback`).
1166///
1167/// If `stealth_profile` is provided, stealth properties are installed as PERMANENT
1168/// engine-layer getters after the Node.js host functions.
1169//
1170// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
1171// BCE-20260621-001: WebViewId captured by callback so install_all_native can
1172// store the page_global under the correct WebViewId key, not a global slot.
1173fn register_native_host_functions(
1174    webview_id: servo::WebViewId,
1175    stealth_profile: Option<bao_stealth::StealthProfile>,
1176) -> bool {
1177    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
1178        Box::new(move |cx_ptr, global_ptr| {
1179            // SAFETY: Called on servo's script thread with valid JSContext/JSObject.
1180            unsafe {
1181                install_all_native(webview_id, cx_ptr, global_ptr, &stealth_profile);
1182            }
1183        });
1184
1185    servo::register_script_thread_callback(webview_id, callback);
1186    true
1187}
1188
1189/// Bridge callback: cast raw servo pointers to mozjs types and install all host functions.
1190///
1191/// Called on servo's script thread during `handle_evaluate_javascript` drain.
1192/// `cx_ptr` is `*mut mozjs::jsapi::JSContext` (servo's script thread JSContext).
1193/// `global_ptr` is `*mut mozjs::jsapi::JSObject` (servo's Window global object).
1194///
1195/// If `stealth_profile` is `Some`, installs stealth properties as PERMANENT engine-layer
1196/// getters (JSPROP_PERMANENT ≡ configurable:false) after the Node.js host functions.
1197///
1198/// BUG-ENG-366 / REQ-SEC-002: the stealth profile is registered PER-REALM keyed
1199/// by this page's Window global pointer via `set_profile_for_global`. This makes
1200/// Compartment isolation unconditional — no dependency on servo's
1201/// `force_isolate_event_loops` flag. When multiple pages share a single servo
1202/// ScriptThread (force_isolate=false), each page's Window global is still
1203/// distinct, so each page's stealth getters resolve to its own profile. The
1204/// Node Realm global for this page is aliased to the same profile in
1205/// `create_node_realm_native`. @trace REQ-SEC-002 [req:REQ-SEC-002] [req:BUG-ENG-366]
1206//
1207// @trace REQ-BRW-003 [req:REQ-BRW-003] [criterion:C10]
1208// BCE-20260621-001: page_global stored keyed by WebViewId, replacing the
1209// process-wide LAST_PAGE_GLOBAL AtomicUsize. Eliminates the "last writer
1210// wins" race that let PageInner capture another page's pointer.
1211unsafe fn install_all_native(
1212    webview_id: servo::WebViewId,
1213    cx_ptr: *mut std::ffi::c_void,
1214    global_ptr: *mut std::ffi::c_void,
1215    stealth_profile: &Option<bao_stealth::StealthProfile>,
1216) {
1217    use mozjs::context::JSContext;
1218    use mozjs::jsapi::{JSContext as RawJSContext, JSObject};
1219    use std::ptr::NonNull;
1220
1221    let raw_cx = cx_ptr as *mut RawJSContext;
1222    let raw_global = global_ptr as *mut JSObject;
1223
1224    if raw_cx.is_null() || raw_global.is_null() {
1225        return;
1226    }
1227
1228    // Cache this ScriptThread's current servo Window global in thread-local.
1229    // lazy_dom_getter_impl (which runs as a JSNative ON this ScriptThread)
1230    // reads it to fetch window/document/navigator. Same-thread read/write —
1231    // no cross-thread *mut JSObject access.
1232    PER_THREAD_PAGE_GLOBAL.with(|cell| {
1233        *cell.borrow_mut() = raw_global;
1234    });
1235
1236    // BCE-20260621-001: store page_global keyed by WebViewId so
1237    // inject_node_apis_with_stealth can retrieve it via get_page_global(wid)
1238    // after drain — replacing the process-wide LAST_PAGE_GLOBAL.
1239    page_global_by_webview().insert(webview_id, raw_global as usize);
1240
1241    // BUG-ENG-366: register per-Realm profile (unconditional Compartment isolation).
1242    if let Some(profile) = stealth_profile {
1243        bao_stealth::engine_props::set_profile_for_global(raw_global as usize, profile);
1244        bao_stealth::engine_props::set_profile(profile);
1245        bun_runtime::fetch_api::set_fetch_stealth_profile(Some(profile.clone()));
1246        // Set canvas noise at servo rendering layer (REQ-STL-003).
1247        servo::set_canvas_noise_seed(
1248            bao_stealth::engine_props::canvas_seed(),
1249            bao_stealth::engine_props::canvas_amplitude(),
1250        );
1251        // Set stealth TLS/HTTP2 config at servo network layer (REQ-STL-001, REQ-STL-002).
1252        // This makes servo's BoringSSL+hyper connections use the profile's cipher suites,
1253        // curves, signature algorithms, ALPN, and HTTP/2 settings. BoringSSL supports full
1254        // JA3/JA4 fingerprint configuration including cipher suite reordering.
1255        // Convert bao_stealth config to servo net connector config (two identical structs
1256        // in different crates — servo net cannot depend on bao_stealth).
1257        let stc = bao_stealth::StealthTlsWireConfig::from_profile(profile);
1258        servo::set_stealth_tls_config(Some(servo::StealthTlsWireConfig {
1259            tls12_cipher_suites: stc.tls12_cipher_suites,
1260            tls13_cipher_suites: stc.tls13_cipher_suites,
1261            signature_algorithms: stc.signature_algorithms,
1262            supported_groups: stc.supported_groups,
1263            alpn_protocols: stc.alpn_protocols,
1264            h2_settings_payload: stc.h2_settings_payload,
1265            h2_initial_stream_size: stc.h2_initial_stream_size,
1266            h2_initial_connection_window_size: stc.h2_initial_connection_window_size,
1267            h2_max_frame_size: stc.h2_max_frame_size,
1268            h2_max_header_list_size: stc.h2_max_header_list_size,
1269        }));
1270        // U2 stage 2: the servo-net bun bridge (net thread) reads the h2
1271        // pseudo-header order / preface PRIORITY frames from this global —
1272        // same lifecycle as the wire-config global above (engine_props'
1273        // ScriptThread-scoped profile lookups are unreachable there).
1274        bao_stealth::set_global_http2_fingerprint(Some(&profile.http2));
1275    } else {
1276        bun_runtime::fetch_api::set_fetch_stealth_profile(None);
1277        servo::set_stealth_tls_config(None);
1278        bao_stealth::set_global_http2_fingerprint(None);
1279    }
1280
1281
1282    // Install stealth properties using raw JSAPI (no Handle wrapper needed)
1283    bao_stealth::engine_props::install_stealth_props(raw_cx, raw_global);
1284
1285    // Create a proper JSContext wrapper and root the global for Web API installation
1286    let cx_nn = match NonNull::new(raw_cx) {
1287        Some(nn) => nn,
1288        None => return,
1289    };
1290    let mut cx = JSContext::from_ptr(cx_nn);
1291    rooted!(in(raw_cx) let mut rooted_global = raw_global);
1292    let global_handle = rooted_global.handle();
1293
1294    // Install Web APIs using properly rooted handle
1295    bun_runtime::fetch_api::install_fetch_global(&mut cx, global_handle);
1296    bun_runtime::timers::install_timer_globals(&mut cx, global_handle);
1297    bun_runtime::web_api::install_performance(&mut cx, global_handle);
1298    bun_runtime::web_api::install_websocket_constructor(&mut cx, global_handle);
1299    bun_runtime::globals::install_crypto_global(&mut cx, global_handle);
1300    bun_runtime::web_api::install_web_encodings(&mut cx, global_handle);
1301    bun_runtime::web_api::install_atob_btoa(&mut cx, global_handle);
1302    bun_runtime::web_api::install_queue_microtask(&mut cx, global_handle);
1303    bun_runtime::globals::install_structured_clone(&mut cx, global_handle);
1304    bun_runtime::globals::install_web_api_constructors(&mut cx, global_handle);
1305    // Full WHATWG Headers/Request/Response classes — installed AFTER the
1306    // constructors blob so their lazy deps (Blob/AbortController/
1307    // ReadableStream/TextEncoder) are already on the global (same ordering
1308    // as bun_runtime::globals::install_web_apis).
1309    bun_runtime::web_fetch_classes::install_fetch_classes(&mut cx, global_handle);
1310
1311    // REQ-ENG-001 criterion 5: Ensure WebAssembly global is available.
1312    // SpiderMonkey provides WebAssembly as a standard global class. It is lazily
1313    // resolved via JS_ResolveStandardClass (the resolve hook on SIMPLE_GLOBAL_CLASS).
1314    // We explicitly trigger resolution by evaluating `typeof WebAssembly` so that
1315    // the global is populated immediately rather than on first access.
1316    {
1317        use mozjs::rust::CompileOptionsWrapper;
1318        let wasm_check = r#"(function(){ try { return typeof WebAssembly; } catch(e) { return 'undefined'; } })()"#;
1319        let c_filename = c"<wasm-init>".to_owned();
1320        let mut options = CompileOptionsWrapper::new(&mut cx, c_filename, 1);
1321        // BAO PATCH (BCE-20260622-004): Suppress `onNewScript` (same rationale
1322        // as evaluate_in_node_realm above).
1323        options.set_hide_script_from_debugger(true);
1324        rooted!(&in(cx) let mut wasm_rval = mozjs::jsval::UndefinedValue());
1325        let wasm_probe = mozjs::rust::evaluate_script(
1326            &mut cx,
1327            global_handle,
1328            wasm_check,
1329            wasm_rval.handle_mut(),
1330            options,
1331        );
1332        // This evaluate_script is a best-effort probe for WebAssembly support.
1333        // A failure here is benign (WebAssembly unavailable), so the Err is
1334        // intentionally discarded. BCE-20260627-007: not an error swallow — the
1335        // probe is informational only, not a functional script.
1336        //
1337        // BCE (P0 browser startup panic, servo error.rs:74): "discard the Err"
1338        // must still consume the pending exception a failed evaluate leaves on
1339        // the context — this runs on servo's ScriptThread during page init, and
1340        // a stale pending exception detonates servo's
1341        // `assert!(!JS_IsExceptionPending)` in `throw_dom_exception` on the
1342        // next error path, killing the ScriptThread (browser dies at startup,
1343        // CDP never listens). The install_all_native borrow of this cx must
1344        // return it clean.
1345        if wasm_probe.is_err() {
1346            mozjs::jsapi::JS_ClearPendingException(cx.raw_cx());
1347        }
1348    }
1349}
1350
1351/// Inject both Node.js APIs and stealth scripts into a page.
1352pub fn inject_all(page: &PageHandle, stealth: bool) -> Result<(), BrowserError> {
1353    let profile = if stealth {
1354        page.stealth_profile()
1355    } else {
1356        None
1357    };
1358    inject_node_apis_with_stealth(page, profile)
1359}
1360
1361/// Inject Node.js APIs and (if profile present) stealth properties into a page.
1362///
1363/// Stealth properties are installed as PERMANENT engine-layer getters (zero JS injection).
1364//
1365// DEC-WK-001 / TASK-1 (双轨收敛): Also registers a servo-native Worker scope
1366// callback via `servo::register_worker_scope_callback`. When a Worker is
1367// created via servo's DOM `new Worker(url)` (https/http URLs), this callback
1368// fires on the Worker thread after servo constructs the Worker global and
1369// installs bao's DedicatedWorkerGlobalScope APIs + stealth profile inheritance
1370// (the same `worker_scope_init_native` path used by the bao_engine::WebWorker
1371// bypass). This realizes DEC-WK-001's "servo-native Worker path" without
1372// abandoning the bypass (DEC-WK-003 dual-track isolation).
1373//
1374// @trace DEC-WK-001 servo-native Worker path
1375// @trace DEC-WK-003 dual-track: bypass (CLI/data:) vs native (https/http)
1376// @trace REQ-BRW-004 [entity:Worker] [criterion:1,3,7] servo Worker scope
1377pub fn inject_all_with_profile(
1378    page: &PageHandle,
1379    profile: &Option<bao_stealth::StealthProfile>,
1380) -> Result<(), BrowserError> {
1381    inject_node_apis_with_stealth(page, profile.clone())?;
1382
1383    // Register the servo-native Worker scope callback. This fires on any
1384    // DedicatedWorker created via servo's DOM `new Worker()`. The callback
1385    // captures this page's WorkerScopeConfig (which carries the parent page's
1386    // stealth profile) so the Worker inherits stealth fingerprint noise.
1387    // @trace DEC-WK-001 servo-native Worker path (vendor patch drain)
1388    // @trace REQ-BRW-004 [criterion:12..17] CRIT-STL-WK stealth inheritance
1389    register_worker_scope_callback_native(profile.clone());
1390
1391    Ok(())
1392}
1393
1394/// Register a servo-native Worker scope callback via the vendor patch
1395/// `servo::register_worker_scope_callback` (DEC-WK-001 / TASK-1).
1396///
1397/// The callback is queued in servo's global `EMBEDDER_WORKER_SCOPE_CALLBACKS`
1398/// vector and drained once per Worker scope creation inside
1399/// `DedicatedWorkerGlobalScope::run_worker_scope` — after the Worker's global
1400/// is built but before the event loop starts. It runs on the Worker thread
1401/// (the same thread that owns the Worker's JSContext), so it is safe to
1402/// dereference the raw `cx`/`global` pointers there (per BCE-20260621-001:
1403/// DOM↔Node interop must happen on the owning thread).
1404///
1405/// What the callback does (mirrors `worker_scope_init_native` for the bypass):
1406///   - Install stealth profile inheritance keyed by the Worker global's address
1407///     (DEC-WK-007 / CRIT-STL-WK: Worker navigator/Canvas/WebGL/Audio match
1408///     parent page).
1409///   - Install DedicatedWorkerGlobalScope Web APIs (fetch/timers/crypto/
1410///     performance/etc., criterion #8).
1411///
1412/// Note: lifecycle natives (self.close/importScripts) are installed by servo
1413/// upstream's DedicatedWorkerGlobalScope binding for native Workers, so the
1414/// bao bypass's `install_worker_lifecycle_natives` is NOT needed here — it is
1415/// only needed for bao_engine::WebWorker (DEC-WK-003 dual-track).
1416///
1417/// @trace DEC-WK-001 servo-native Worker path (vendor patch)
1418/// @trace DEC-WK-003 dual-track isolation (bypass not abandoned)
1419/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:8,12..17]
1420pub fn register_worker_scope_callback_native(profile: Option<bao_stealth::StealthProfile>) {
1421    // Build a WorkerScopeConfig from the stealth profile (parent page's config).
1422    // This is what worker_scope_init_native needs to install the right stealth
1423    // properties + navigator values on the Worker's global.
1424    // @trace REQ-BRW-004 [criterion:12..17] CRIT-STL-WK
1425    let config = match profile.as_ref() {
1426        Some(p) => crate::delegate::WorkerScopeConfig::from(p),
1427        None => crate::delegate::WorkerScopeConfig::default(),
1428    };
1429
1430    let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
1431        Box::new(move |cx_ptr, global_ptr| {
1432            // SAFETY: Called on the Worker thread with valid JSContext + global.
1433            // Per BCE-20260621-001 this is the owning thread, so dereferencing
1434            // the raw pointers is safe (no cross-thread *mut JSObject).
1435            let raw_cx = cx_ptr as *mut mozjs::jsapi::JSContext;
1436            let raw_global = global_ptr as *mut mozjs::jsapi::JSObject;
1437            if raw_cx.is_null() || raw_global.is_null() {
1438                log::warn!(
1439                    "[register_worker_scope_callback_native] NULL cx/global — \
1440                     skipping Worker scope init (DEC-WK-001)"
1441                );
1442                return;
1443            }
1444            // TASK-63 DIAG: confirm worker scope callback fired (worker thread alive + scope created)
1445            eprintln!(
1446                "[TASK-63-DIAG] worker scope callback FIRED (worker thread alive, scope created)"
1447            );
1448            log::debug!(
1449                "[register_worker_scope_callback_native] servo-native Worker \
1450                 scope created — installing bao stealth + Web APIs (DEC-WK-001 / \
1451                 DEC-WK-003 dual-track)"
1452            );
1453            unsafe {
1454                worker_scope_init_native(raw_cx, raw_global, &config);
1455            }
1456        });
1457
1458    servo::register_worker_scope_callback(callback);
1459}
1460
1461// ─── Worker Scope Initialization Bridge (REQ-BRW-004) ──────────────
1462// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:8]
1463// @trace REQ-BRW-004 [entity:Worker] [criterion:12..17]
1464//
1465// worker_scope_init_native runs on the Worker thread with its own JSContext
1466// and global object (installed via register_worker_scope_callback_native →
1467// servo's drain_worker_scope_callbacks). It installs:
1468// 1. Stealth properties (criterion #12-17): PERMANENT engine-layer getters
1469//    for navigator/Canvas/WebGL/Audio fingerprints matching the parent page
1470// 2. (DELETED BCE-20260627-009) Web APIs like fetch/timers/crypto/performance
1471//    were incorrectly installed here. servo's DedicatedWorkerGlobalScope already
1472//    provides these via its own resource thread integration. Bao's duplicate
1473//    installation caused: (a) two promise execution models to conflict, and
1474//    (b) JS_DefineFunction SIGSEGV in js::Atomize because the callback ran
1475//    without entering the worker global's Compartment (now fixed in servo vendor).
1476
1477/// Type alias for the Worker scope initialization callback.
1478///
1479/// A boxed closure that runs on the Worker thread to install APIs and stealth
1480/// properties on the Worker's global object (DEC-WK-001 native path).
1481///
1482/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:8]
1483/// @trace REQ-BRW-004 [criterion:12..17] stealth consistency
1484pub type WorkerScopeInitFn =
1485    Box<dyn FnOnce(*mut mozjs::jsapi::JSContext, *mut mozjs::jsapi::JSObject) + Send>;
1486
1487/// Native implementation: install stealth properties on the Worker's global object.
1488///
1489/// Called on the Worker thread with the Worker's JSContext and global.
1490/// This is the same pattern as `install_all_native` for the Page Realm,
1491/// but scoped to the Worker's DedicatedWorkerGlobalScope.
1492///
1493/// NOTE: Web APIs (fetch/timers/crypto/performance/etc.) are NOT installed here.
1494/// servo's DedicatedWorkerGlobalScope already provides these natively via its
1495/// resource thread. Bao does NOT duplicate them (BCE-20260627-009).
1496///
1497/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:8]
1498/// @trace REQ-BRW-004 [criterion:12..17] stealth consistency
1499unsafe fn worker_scope_init_native(
1500    raw_cx: *mut mozjs::jsapi::JSContext,
1501    global: *mut mozjs::jsapi::JSObject,
1502    config: &crate::delegate::WorkerScopeConfig,
1503) {
1504    use mozjs::context::JSContext;
1505    use std::ptr::NonNull;
1506
1507    if raw_cx.is_null() || global.is_null() {
1508        return;
1509    }
1510
1511    // @trace REQ-BRW-004 [criterion:12..17] stealth consistency
1512    // Install stealth properties on Worker global if profile is provided.
1513    // bao_stealth::engine_props::install_stealth_props uses raw JSAPI
1514    // to install PERMANENT engine-layer getters for navigator/Canvas/WebGL/Audio.
1515    // The profile is keyed by the Worker global's address, so stealth getters
1516    // in the Worker's DedicatedWorkerGlobalScope resolve to the same
1517    // fingerprint noise as the parent page.
1518    if let Some(ref profile) = config.stealth_profile {
1519        // @trace REQ-BRW-004 [criterion:12] CRIT-STL-WK navigator 一致
1520        bao_stealth::engine_props::set_profile_for_global(global as usize, profile);
1521        bao_stealth::engine_props::install_stealth_props(raw_cx, global);
1522        // Set canvas noise at servo rendering layer for Worker (REQ-STL-003).
1523        servo::set_canvas_noise_seed(
1524            bao_stealth::engine_props::canvas_seed(),
1525            bao_stealth::engine_props::canvas_amplitude(),
1526        );
1527    }
1528
1529    // BCE-20260627-009: Web APIs (fetch/timers/crypto/performance/etc.) are NOT
1530    // installed on Worker global. servo's DedicatedWorkerGlobalScope provides these
1531    // natively. Duplicate installation caused SIGSEGV in js::Atomize and promise
1532    // execution model conflicts. See vendor patch in dedicatedworkerglobalscope.rs.
1533}
1534
1535// @trace REQ-SEC-003 [entity:WebPolyfills]
1536/// Web-only polyfills for Page Realm fallback (REQ-SEC-003: NO Node APIs on Window global).
1537///
1538/// This is the fallback when `register_native_host_functions` is unavailable.
1539/// It provides ONLY standard Web APIs that browsers should have but may be missing
1540/// in servo's script context. Node.js APIs (require, process, Buffer, Bun, etc.)
1541/// are deliberately EXCLUDED — they belong only in the Node Realm.
1542const WEB_POLYFILLS: &str = r#"(function() {
1543  // @trace REQ-SEC-003 Web-only polyfills (no Node.js APIs)
1544
1545  // TextEncoder / TextDecoder
1546  if (typeof TextEncoder === 'undefined') {
1547    TextEncoder = function() { this.encode = function(str) { return new Uint8Array(Array.from(str).map(function(c){return c.charCodeAt(0);})); }; };
1548  }
1549  if (typeof TextDecoder === 'undefined') {
1550    TextDecoder = function() { this.decode = function(buf) { return String.fromCharCode.apply(null, buf); }; };
1551  }
1552
1553  // URL / URLSearchParams
1554  if (typeof URL === 'undefined') {
1555    URL = function(url, base) { throw new Error('URL not available'); };
1556  }
1557  if (typeof URLSearchParams === 'undefined') {
1558    URLSearchParams = function(init) {
1559      this._params = [];
1560      this.append = function(k,v) { this._params.push([k,v]); };
1561      this.get = function(k) { for(var i=0;i<this._params.length;i++) if(this._params[i][0]===k) return this._params[i][1]; return null; };
1562      this.toString = function() { return this._params.map(function(p){return p[0]+'='+p[1];}).join('&'); };
1563    };
1564  }
1565
1566  // btoa / atob
1567  if (typeof btoa === 'undefined') {
1568    var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
1569    btoa = function(str) {
1570      var out = '';
1571      for (var i = 0; i < str.length; i += 3) {
1572        var a = str.charCodeAt(i), b = str.charCodeAt(i+1), c = str.charCodeAt(i+2);
1573        out += _b64chars[a>>2] + _b64chars[((a&3)<<4)|(b>>4)] + (isNaN(b)?'=':_b64chars[((b&15)<<2)|(c>>6)]) + (isNaN(b)||isNaN(c)?'=':_b64chars[c&63]);
1574      }
1575      return out;
1576    };
1577    atob = function(str) {
1578      var out = '';
1579      str = str.replace(/=+$/, '');
1580      for (var i = 0; i < str.length; i += 4) {
1581        var a = _b64chars.indexOf(str[i]), b = _b64chars.indexOf(str[i+1]);
1582        var c = _b64chars.indexOf(str[i+2]), d = _b64chars.indexOf(str[i+3]);
1583        out += String.fromCharCode((a<<2)|(b>>4)) + (c>=0?String.fromCharCode(((b&15)<<4)|(c>>2)):'') + (d>=0?String.fromCharCode(((c&3)<<6)|d):'');
1584      }
1585      return out;
1586    };
1587  }
1588
1589  // setImmediate / clearImmediate (Web API extensions used by many libraries)
1590  if (typeof setImmediate === 'undefined') {
1591    setImmediate = function(fn) {
1592      var args = Array.prototype.slice.call(arguments, 1);
1593      return setTimeout(function() { fn.apply(null, args); }, 0);
1594    };
1595    clearImmediate = function(id) { clearTimeout(id); };
1596  }
1597})();"#;
1598
1599const NODE_POLYFILLS: &str = r#"(function() {
1600  // @trace REQ-ENG-007 Node.js API polyfills for Node Realm context
1601
1602  // global alias
1603  if (typeof global === 'undefined') {
1604    global = globalThis;
1605  }
1606
1607  // process
1608  if (typeof process === 'undefined') {
1609    process = {
1610      argv: ['bao', typeof __filename !== 'undefined' ? __filename : ''],
1611      argv0: 'bao',
1612      execArgv: [],
1613      execPath: '/usr/local/bin/bao',
1614      env: (function() {
1615        var e = {};
1616        if (typeof navigator !== 'undefined' && navigator.userAgent) {
1617          e.NODE_VERSION = '20.11.0';
1618          e.BAO_VERSION = '0.1.0';
1619        }
1620        e.HOME = '/';
1621        e.PATH = '/usr/local/bin:/usr/bin:/bin';
1622        e.TERM = 'xterm-256color';
1623        return e;
1624      })(),
1625      version: 'v20.11.0',
1626      versions: {
1627        node: '20.11.0',
1628        v8: '12.4.254.14',
1629        uv: '1.27.0',
1630        zlib: '1.2.13',
1631        brotli: '1.0.9',
1632        ares: '1.19.1',
1633        modules: '115',
1634        openssl: '3.0.12',
1635        icu: '74.2',
1636        bun: '1.0.25',
1637        bao: '0.1.0',
1638      },
1639      pid: 1,
1640      ppid: 0,
1641      title: 'bao',
1642      arch: (function() {
1643        if (typeof navigator !== 'undefined') {
1644          var p = navigator.platform || '';
1645          if (p.indexOf('Win') >= 0) return 'x64';
1646          if (p.indexOf('Mac') >= 0) return 'arm64';
1647          if (p.indexOf('Linux') >= 0) return 'x64';
1648        }
1649        return 'x64';
1650      })(),
1651      platform: (function() {
1652        if (typeof navigator !== 'undefined') {
1653          var p = navigator.platform || '';
1654          if (p.indexOf('Win') >= 0) return 'win32';
1655          if (p.indexOf('Mac') >= 0) return 'darwin';
1656        }
1657        return 'linux';
1658      })(),
1659      cwd: function() { return '/'; },
1660      chdir: function() {},
1661      exit: function(code) { throw new Error('process.exit(' + (code||0) + ')'); },
1662      hrtime: (function() {
1663        var origin = performance.now() * 1e-3;
1664        return function bigtime() {
1665          var diff = performance.now() * 1e-3 - origin;
1666          var sec = Math.floor(diff);
1667          var nsec = Math.round((diff - sec) * 1e9);
1668          if (arguments.length > 0) {
1669            sec += arguments[0][0];
1670            nsec += arguments[0][1];
1671            sec += Math.floor(nsec / 1e9);
1672            nsec = nsec % 1e9;
1673            if (nsec < 0) { nsec += 1e9; sec -= 1; }
1674          }
1675          var result = [sec, nsec];
1676          result.bigint = function() { return BigInt(sec) * 1000000000n + BigInt(nsec); };
1677          return result;
1678        };
1679      })(),
1680      uptime: function() { return performance.now() / 1000; },
1681      memoryUsage: function() {
1682        return { rss: 64*1024*1024, heapTotal: 32*1024*1024, heapUsed: 16*1024*1024, external: 2*1024*1024, arrayBuffers: 1*1024*1024 };
1683      },
1684      cpuUsage: function() { return { user: 100000, system: 50000 }; },
1685      nextTick: function(fn) {
1686        var args = Array.prototype.slice.call(arguments, 1);
1687        Promise.resolve().then(function() { fn.apply(null, args); });
1688      },
1689      binding: function(name) { return {}; },
1690      dlopen: function() { throw new Error('process.dlopen not available in browser context'); },
1691      stdout: { write: function(d) { console.log(d); return true; }, end: function() {} },
1692      stderr: { write: function(d) { console.error(d); return true; }, end: function() {} },
1693      stdin: { on: function() {}, resume: function() { return this; }, pipe: function() {} },
1694      on: function(event, fn) { return this; },
1695      off: function() {},
1696      once: function(event, fn) { return this; },
1697      emit: function(event) { return false; },
1698      removeAllListeners: function() { return this; },
1699      setUncaughtExceptionCallback: function() {},
1700    };
1701  }
1702
1703  // Buffer — browser-compatible implementation backed by Uint8Array
1704  if (typeof Buffer === 'undefined') {
1705    Buffer = (function() {
1706      function B(data, encoding) {
1707        if (!(this instanceof B)) return new B(data, encoding);
1708        if (data instanceof Uint8Array) {
1709          this._buf = new Uint8Array(data);
1710        } else if (data instanceof ArrayBuffer) {
1711          this._buf = new Uint8Array(data);
1712        } else if (typeof data === 'string') {
1713          this._buf = new Uint8Array(Array.from(data).map(function(c) { return c.charCodeAt(0); }));
1714        } else if (Array.isArray(data)) {
1715          this._buf = new Uint8Array(data);
1716        } else {
1717          this._buf = new Uint8Array(0);
1718        }
1719        this.length = this._buf.length;
1720      }
1721
1722      B.isBuffer = function(obj) { return obj instanceof B; };
1723
1724      B.from = function(data, encoding) {
1725        if (data instanceof B) return new B(data._buf);
1726        if (data instanceof Uint8Array) return new B(data);
1727        if (data instanceof ArrayBuffer) return new B(data);
1728        if (typeof data === 'string') {
1729          if (encoding === 'hex') {
1730            var bytes = [];
1731            for (var i = 0; i < data.length; i += 2) {
1732              bytes.push(parseInt(data.substr(i, 2), 16));
1733            }
1734            return new B(bytes);
1735          }
1736          if (encoding === 'base64') {
1737            var bin = atob(data);
1738            var bytes = [];
1739            for (var i = 0; i < bin.length; i++) bytes.push(bin.charCodeAt(i));
1740            return new B(bytes);
1741          }
1742          return new B(data);
1743        }
1744        return new B(data);
1745      };
1746
1747      B.alloc = function(size, fill, encoding) {
1748        var buf = new B(new Uint8Array(size));
1749        if (fill !== undefined) buf.fill(fill);
1750        return buf;
1751      };
1752
1753      B.allocUnsafe = function(size) {
1754        return new B(new Uint8Array(size));
1755      };
1756
1757      B.allocUnsafeSlow = function(size) {
1758        return new B(new Uint8Array(size));
1759      };
1760
1761      B.concat = function(list, totalLength) {
1762        if (!Array.isArray(list) || list.length === 0) return new B(new Uint8Array(0));
1763        var len = totalLength !== undefined ? totalLength : list.reduce(function(a, b) { return a + b.length; }, 0);
1764        var result = new Uint8Array(len);
1765        var offset = 0;
1766        for (var i = 0; i < list.length; i++) {
1767          var buf = list[i] instanceof B ? list[i]._buf : new Uint8Array(list[i]);
1768          result.set(buf, offset);
1769          offset += buf.length;
1770        }
1771        return new B(result);
1772      };
1773
1774      B.byteLength = function(str, encoding) {
1775        if (typeof str === 'string') {
1776          if (encoding === 'base64') return atob(str).length;
1777          if (encoding === 'hex') return str.length / 2;
1778          return new TextEncoder().encode(str).length;
1779        }
1780        if (str instanceof ArrayBuffer) return str.byteLength;
1781        if (str instanceof Uint8Array) return str.length;
1782        return 0;
1783      };
1784
1785      B.compare = function(a, b) {
1786        for (var i = 0; i < Math.min(a.length, b.length); i++) {
1787          if (a._buf[i] < b._buf[i]) return -1;
1788          if (a._buf[i] > b._buf[i]) return 1;
1789        }
1790        return a.length - b.length;
1791      };
1792
1793      B.prototype.slice = function(start, end) {
1794        return new B(this._buf.slice(start || 0, end));
1795      };
1796
1797      B.prototype.subarray = function(start, end) {
1798        return new B(this._buf.subarray(start || 0, end));
1799      };
1800
1801      B.prototype.toString = function(encoding, start, end) {
1802        var s = start || 0;
1803        var e = end !== undefined ? end : this._buf.length;
1804        var slice = this._buf.slice(s, e);
1805        if (encoding === 'hex') {
1806          return Array.from(slice).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
1807        }
1808        if (encoding === 'base64') {
1809          var bin = Array.from(slice).map(function(b) { return String.fromCharCode(b); }).join('');
1810          return btoa(bin);
1811        }
1812        return new TextDecoder().decode(slice);
1813      };
1814
1815      B.prototype.toJSON = function() {
1816        return { type: 'Buffer', data: Array.from(this._buf) };
1817      };
1818
1819      B.prototype.equals = function(other) {
1820        if (!(other instanceof B) || this.length !== other.length) return false;
1821        for (var i = 0; i < this.length; i++) {
1822          if (this._buf[i] !== other._buf[i]) return false;
1823        }
1824        return true;
1825      };
1826
1827      B.prototype.compare = function(other, targetStart, targetEnd, sourceStart, sourceEnd) {
1828        var a = this._buf.slice(sourceStart || 0, sourceEnd);
1829        var b = other._buf.slice(targetStart || 0, targetEnd);
1830        for (var i = 0; i < Math.min(a.length, b.length); i++) {
1831          if (a[i] < b[i]) return -1;
1832          if (a[i] > b[i]) return 1;
1833        }
1834        return a.length - b.length;
1835      };
1836
1837      B.prototype.copy = function(target, targetStart, sourceStart, sourceEnd) {
1838        var src = this._buf.slice(sourceStart || 0, sourceEnd);
1839        for (var i = 0; i < src.length; i++) {
1840          if (target._buf) target._buf[targetStart + i] = src[i];
1841        }
1842        return src.length;
1843      };
1844
1845      B.prototype.fill = function(value, start, end) {
1846        var s = start || 0;
1847        var e = end !== undefined ? end : this._buf.length;
1848        var v = typeof value === 'number' ? value : 0;
1849        for (var i = s; i < e; i++) this._buf[i] = v;
1850        return this;
1851      };
1852
1853      B.prototype.write = function(str, offset, length, encoding) {
1854        var o = offset || 0;
1855        var bytes = new TextEncoder().encode(str);
1856        var len = Math.min(bytes.length, length !== undefined ? length : this._buf.length - o);
1857        for (var i = 0; i < len; i++) this._buf[o + i] = bytes[i];
1858        return len;
1859      };
1860
1861      B.prototype.includes = function(value, offset) {
1862        return this.indexOf(value, offset) !== -1;
1863      };
1864
1865      B.prototype.indexOf = function(value, offset) {
1866        var o = offset || 0;
1867        var search = typeof value === 'number' ? [value] : Array.from(new TextEncoder().encode(String(value)));
1868        for (var i = o; i <= this._buf.length - search.length; i++) {
1869          var found = true;
1870          for (var j = 0; j < search.length; j++) {
1871            if (this._buf[i + j] !== search[j]) { found = false; break; }
1872          }
1873          if (found) return i;
1874        }
1875        return -1;
1876      };
1877
1878      B.prototype.readUInt8 = function(offset) { return this._buf[offset || 0]; };
1879      B.prototype.readUInt16LE = function(offset) { var o = offset||0; return this._buf[o] | (this._buf[o+1]<<8); };
1880      B.prototype.readUInt16BE = function(offset) { var o = offset||0; return (this._buf[o]<<8) | this._buf[o+1]; };
1881      B.prototype.readUInt32LE = function(offset) {
1882        var o = offset||0;
1883        return (this._buf[o]) | (this._buf[o+1]<<8) | (this._buf[o+2]<<16) | (this._buf[o+3]<<24);
1884      };
1885      B.prototype.readInt8 = function(offset) { var v = this._buf[offset||0]; return v > 127 ? v - 256 : v; };
1886      B.prototype.readInt16LE = function(offset) { var v = this.readUInt16LE(offset); return v > 32767 ? v - 65536 : v; };
1887      B.prototype.readInt32LE = function(offset) { var v = this.readUInt32LE(offset); return v > 2147483647 ? v - 4294967296 : v; };
1888      B.prototype.readFloatLE = function(offset) {
1889        var buf = new ArrayBuffer(4); new Float32Array(buf)[0] = 0;
1890        new Uint8Array(buf).set(this._buf.slice(offset||0, (offset||0)+4));
1891        return new Float32Array(buf)[0];
1892      };
1893      B.prototype.readDoubleLE = function(offset) {
1894        var buf = new ArrayBuffer(8);
1895        new Uint8Array(buf).set(this._buf.slice(offset||0, (offset||0)+8));
1896        return new Float64Array(buf)[0];
1897      };
1898
1899      B.prototype.writeUInt8 = function(v, offset) { this._buf[offset||0] = v & 0xFF; return (offset||0)+1; };
1900      B.prototype.writeUInt16LE = function(v, offset) { var o = offset||0; this._buf[o]=v&0xFF; this._buf[o+1]=(v>>8)&0xFF; return o+2; };
1901      B.prototype.writeUInt32LE = function(v, offset) { var o = offset||0; this._buf[o]=v&0xFF; this._buf[o+1]=(v>>8)&0xFF; this._buf[o+2]=(v>>16)&0xFF; this._buf[o+3]=(v>>24)&0xFF; return o+4; };
1902      B.prototype.writeInt8 = function(v, offset) { return this.writeUInt8(v < 0 ? v + 256 : v, offset); };
1903      B.prototype.writeInt16LE = function(v, offset) { return this.writeUInt16LE(v < 0 ? v + 65536 : v, offset); };
1904      B.prototype.writeInt32LE = function(v, offset) { return this.writeUInt32LE(v < 0 ? v + 4294967296 : v, offset); };
1905      B.prototype.writeFloatLE = function(v, offset) {
1906        var buf = new ArrayBuffer(4); new Float32Array(buf)[0] = v;
1907        this._buf.set(new Uint8Array(buf), offset||0); return (offset||0)+4;
1908      };
1909      B.prototype.writeDoubleLE = function(v, offset) {
1910        var buf = new ArrayBuffer(8); new Float64Array(buf)[0] = v;
1911        this._buf.set(new Uint8Array(buf), offset||0); return (offset||0)+8;
1912      };
1913
1914      B.prototype[Symbol.iterator] = function() {
1915        var idx = 0; var buf = this._buf;
1916        return { next: function() { return idx < buf.length ? { value: buf[idx++], done: false } : { done: true }; } };
1917      };
1918
1919      return B;
1920    })();
1921  }
1922
1923  // require — basic module loader for browser context
1924  if (typeof require === 'undefined') {
1925    var _module_cache = {};
1926    var _module_builtin = {
1927      'fs': { readFileSync: function() { throw new Error('fs not available in browser context'); }, existsSync: function() { return false; } },
1928      'path': {
1929        join: function() { return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/'); },
1930        resolve: function() { var parts = Array.prototype.slice.call(arguments); return '/' + parts.join('/').replace(/\/+/g, '/'); },
1931        dirname: function(p) { return p.split('/').slice(0, -1).join('/') || '.'; },
1932        basename: function(p, ext) { var b = p.split('/').pop(); return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
1933        extname: function(p) { var i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; },
1934        sep: '/', delimiter: ':',
1935        posix: {
1936          join: function() { return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/'); },
1937          resolve: function() { var parts = Array.prototype.slice.call(arguments); return '/' + parts.join('/').replace(/\/+/g, '/'); },
1938          dirname: function(p) { return p.split('/').slice(0, -1).join('/') || '.'; },
1939          basename: function(p, ext) { var b = p.split('/').pop(); return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
1940          extname: function(p) { var i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; },
1941          sep: '/', delimiter: ':',
1942        },
1943        win32: { sep: '\\', delimiter: ';' },
1944      },
1945      'url': {
1946        parse: function(u) { try { var p = new URL(u); return { href: p.href, protocol: p.protocol, host: p.host, hostname: p.hostname, pathname: p.pathname, search: p.search, hash: p.hash }; } catch(e) { return {}; } },
1947        format: function(u) { return typeof u === 'string' ? u : (u.protocol||'http:') + '//' + (u.host||u.hostname||'localhost') + (u.pathname||'/'); },
1948        resolve: function(from, to) { try { return new URL(to, from).href; } catch(e) { return to; } },
1949        URL: typeof URL !== 'undefined' ? URL : function() {},
1950        URLSearchParams: typeof URLSearchParams !== 'undefined' ? URLSearchParams : function() {},
1951      },
1952      'querystring': {
1953        parse: function(str, sep, eq) {
1954          sep = sep || '&'; eq = eq || '=';
1955          var obj = {};
1956          if (!str) return obj;
1957          str.split(sep).forEach(function(pair) {
1958            var idx = pair.indexOf(eq);
1959            var key = idx >= 0 ? pair.substring(0, idx) : pair;
1960            var val = idx >= 0 ? pair.substring(idx + 1) : '';
1961            obj[decodeURIComponent(key)] = decodeURIComponent(val);
1962          });
1963          return obj;
1964        },
1965        stringify: function(obj, sep, eq) {
1966          sep = sep || '&'; eq = eq || '=';
1967          return Object.keys(obj || {}).map(function(k) {
1968            return encodeURIComponent(k) + eq + encodeURIComponent(obj[k]);
1969          }).join(sep);
1970        },
1971        escape: encodeURIComponent,
1972        unescape: decodeURIComponent,
1973      },
1974      'events': {
1975        EventEmitter: (function() {
1976          function EE() { this._events = {}; }
1977          EE.prototype.on = function(e, fn) { (this._events[e] = this._events[e] || []).push(fn); return this; };
1978          EE.prototype.once = function(e, fn) { var self = this; function g() { self.off(e, g); fn.apply(this, arguments); } g._orig = fn; this.on(e, g); return this; };
1979          EE.prototype.off = function(e, fn) {
1980            if (!this._events[e]) return this;
1981            if (!fn) { delete this._events[e]; return this; }
1982            this._events[e] = this._events[e].filter(function(f) { return f !== fn && f._orig !== fn; });
1983            return this;
1984          };
1985          EE.prototype.emit = function(e) {
1986            var args = Array.prototype.slice.call(arguments, 1);
1987            (this._events[e] || []).forEach(function(fn) { fn.apply(null, args); });
1988            return this;
1989          };
1990          EE.prototype.removeListener = EE.prototype.off;
1991          EE.prototype.removeAllListeners = function(e) { if (e) delete this._events[e]; else this._events = {}; return this; };
1992          EE.prototype.listeners = function(e) { return this._events[e] || []; };
1993          EE.prototype.listenerCount = function(e) { return (this._events[e] || []).length; };
1994          return EE;
1995        })(),
1996      },
1997      'util': {
1998        inspect: function(obj) { return JSON.stringify(obj, null, 2); },
1999        inherits: function(ctor, superCtor) { ctor.prototype = Object.create(superCtor.prototype); ctor.prototype.constructor = ctor; },
2000        isFunction: function(v) { return typeof v === 'function'; },
2001        isNull: function(v) { return v === null; },
2002        isUndefined: function(v) { return v === undefined; },
2003        isObject: function(v) { return v !== null && typeof v === 'object'; },
2004        isString: function(v) { return typeof v === 'string'; },
2005        promisify: function(fn) {
2006          return function() {
2007            var args = Array.prototype.slice.call(arguments);
2008            return new Promise(function(resolve, reject) {
2009              args.push(function(err, result) { if (err) reject(err); else resolve(result); });
2010              fn.apply(null, args);
2011            });
2012          };
2013        },
2014        format: function(fmt) {
2015          var args = Array.prototype.slice.call(arguments, 1);
2016          return fmt.replace(/%[sdjifo]/g, function(m) { return args.length ? String(args.shift()) : m; });
2017        },
2018        types: {
2019          isDate: function(v) { return v instanceof Date; },
2020          isRegExp: function(v) { return v instanceof RegExp; },
2021          isArray: function(v) { return Array.isArray(v); },
2022          isPromise: function(v) { return v && typeof v.then === 'function'; },
2023        },
2024      },
2025      'stream': { Readable: function(){}, Writable: function(){}, Duplex: function(){}, Transform: function(){} },
2026      'buffer': { Buffer: typeof Buffer !== 'undefined' ? Buffer : function(){} },
2027      'crypto': {
2028        randomBytes: function(size, cb) {
2029          var arr = new Uint8Array(size);
2030          if (typeof crypto !== 'undefined' && crypto.getRandomValues) crypto.getRandomValues(arr);
2031          if (cb) cb(null, Buffer.from(arr));
2032          return Buffer.from(arr);
2033        },
2034        createHash: function(algo) {
2035          var chunks = [];
2036          return {
2037            update: function(data) { chunks.push(typeof data === 'string' ? data : String(data)); return this; },
2038            digest: function(enc) {
2039              var str = chunks.join('');
2040              if (typeof crypto !== 'undefined' && crypto.subtle) {
2041                return crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)).then(function(buf) {
2042                  var arr = new Uint8Array(buf); return enc === 'hex' ? Array.from(arr).map(function(b){return b.toString(16).padStart(2,'0');}).join('') : Buffer.from(arr);
2043                });
2044              }
2045              return enc === 'hex' ? '00000000' : Buffer.alloc(0);
2046            },
2047          };
2048        },
2049      },
2050      'os': {
2051        platform: function() { return 'linux'; },
2052        arch: function() { return 'x64'; },
2053        homedir: function() { return '/'; },
2054        tmpdir: function() { return '/tmp'; },
2055        type: function() { return 'Linux'; },
2056        release: function() { return '6.8.0'; },
2057        hostname: function() { return 'bao'; },
2058        cpus: function() { return [{ model: 'bao', speed: 3000 }]; },
2059        totalmem: function() { return 8*1024*1024*1024; },
2060        freemem: function() { return 4*1024*1024*1024; },
2061        uptime: function() { return 3600; },
2062        EOL: '\n',
2063      },
2064      'assert': {
2065        ok: function(v, msg) { if (!v) throw new Error(msg || 'assertion failed'); },
2066        equal: function(a, b, msg) { if (a !== b) throw new Error(msg || a + ' !== ' + b); },
2067        deepEqual: function(a, b, msg) { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(msg || 'not deep equal'); },
2068        throws: function(fn, msg) { try { fn(); throw new Error(msg || 'expected throw'); } catch(e) { if (e.message === (msg || 'expected throw')) throw e; } },
2069      },
2070      'timers': {
2071        setTimeout: typeof setTimeout !== 'undefined' ? setTimeout : function(fn) { fn(); return 0; },
2072        setInterval: typeof setInterval !== 'undefined' ? setInterval : function(fn) { return 0; },
2073        clearTimeout: typeof clearTimeout !== 'undefined' ? clearTimeout : function() {},
2074        clearInterval: typeof clearInterval !== 'undefined' ? clearInterval : function() {},
2075        setImmediate: typeof setImmediate !== 'undefined' ? setImmediate : function(fn) { return setTimeout(fn, 0); },
2076        clearImmediate: typeof clearImmediate !== 'undefined' ? clearImmediate : function() {},
2077      },
2078    };
2079
2080    require = function(name) {
2081      if (_module_cache[name]) return _module_cache[name];
2082      if (_module_builtin[name]) { _module_cache[name] = _module_builtin[name]; return _module_builtin[name]; }
2083      throw new Error("Cannot find module '" + name + "' in browser context");
2084    };
2085
2086    require.resolve = function(name) { return name; };
2087    require.cache = _module_cache;
2088  }
2089
2090  // setImmediate / clearImmediate
2091  if (typeof setImmediate === 'undefined') {
2092    setImmediate = function(fn) {
2093      var args = Array.prototype.slice.call(arguments, 1);
2094      return setTimeout(function() { fn.apply(null, args); }, 0);
2095    };
2096    clearImmediate = function(id) { clearTimeout(id); };
2097  }
2098
2099  // __dirname / __filename
2100  if (typeof __dirname === 'undefined') {
2101    __dirname = '/';
2102    __filename = '/index.js';
2103  }
2104
2105  // TextEncoder / TextDecoder (most browsers have these, but ensure)
2106  if (typeof TextEncoder === 'undefined') {
2107    TextEncoder = function() { this.encode = function(str) { return new Uint8Array(Array.from(str).map(function(c){return c.charCodeAt(0);})); }; };
2108  }
2109  if (typeof TextDecoder === 'undefined') {
2110    TextDecoder = function() { this.decode = function(buf) { return String.fromCharCode.apply(null, buf); }; };
2111  }
2112
2113  // URL / URLSearchParams (most browsers have these, but ensure)
2114  if (typeof URL === 'undefined') {
2115    URL = function(url, base) { throw new Error('URL not available'); };
2116  }
2117  if (typeof URLSearchParams === 'undefined') {
2118    URLSearchParams = function(init) {
2119      this._params = [];
2120      this.append = function(k,v) { this._params.push([k,v]); };
2121      this.get = function(k) { for(var i=0;i<this._params.length;i++) if(this._params[i][0]===k) return this._params[i][1]; return null; };
2122      this.toString = function() { return this._params.map(function(p){return p[0]+'='+p[1];}).join('&'); };
2123    };
2124  }
2125
2126  // btoa / atob (most browsers have these, but ensure)
2127  if (typeof btoa === 'undefined') {
2128    var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
2129    btoa = function(str) {
2130      var out = '';
2131      for (var i = 0; i < str.length; i += 3) {
2132        var a = str.charCodeAt(i), b = str.charCodeAt(i+1), c = str.charCodeAt(i+2);
2133        out += _b64chars[a>>2] + _b64chars[((a&3)<<4)|(b>>4)] + (isNaN(b)?'=':_b64chars[((b&15)<<2)|(c>>6)]) + (isNaN(b)||isNaN(c)?'=':_b64chars[c&63]);
2134      }
2135      return out;
2136    };
2137    atob = function(str) {
2138      var out = '';
2139      str = str.replace(/=+$/, '');
2140      for (var i = 0; i < str.length; i += 4) {
2141        var a = _b64chars.indexOf(str[i]), b = _b64chars.indexOf(str[i+1]);
2142        var c = _b64chars.indexOf(str[i+2]), d = _b64chars.indexOf(str[i+3]);
2143        out += String.fromCharCode((a<<2)|(b>>4)) + (c>=0?String.fromCharCode(((b&15)<<4)|(c>>2)):'') + (d>=0?String.fromCharCode(((c&3)<<6)|d):'');
2144      }
2145      return out;
2146    };
2147  }
2148})();"#;
2149
2150// ── Bridge types ────────────────────────────────────────────────────
2151
2152/// Commands sent through the runtime bridge for execution in a page context.
2153///
2154/// Each variant maps to a [`PageHandle`] operation. The bridge decouples
2155/// command submission from execution — a worker loop reads from the
2156/// [`BridgeReceiver`] and drives the real servo page.
2157///
2158/// @trace REQ-BRW-003 [entity:RuntimeBridge]
2159#[derive(Debug, Clone, PartialEq, Eq)]
2160pub enum BridgeCommand {
2161    /// Navigate the page to a URL.
2162    Navigate(String),
2163    /// Evaluate JavaScript in the page and return the result as a string.
2164    Evaluate(String),
2165    /// Capture a screenshot of the current page.
2166    Screenshot,
2167    /// Close the page and mark the bridge as inactive.
2168    Close,
2169    /// Resize the page viewport to width × height.
2170    Resize(u32, u32),
2171    /// Retrieve the current page title.
2172    GetTitle,
2173    /// Retrieve the current page URL.
2174    GetUrl,
2175}
2176
2177/// Response returned after executing a [`BridgeCommand`].
2178///
2179/// @trace REQ-BRW-003 [entity:RuntimeBridge]
2180#[derive(Debug, Clone, PartialEq, Eq)]
2181pub enum BridgeResponse {
2182    /// Command succeeded with no return value.
2183    Ok,
2184    /// Command failed with a descriptive message.
2185    Err(String),
2186    /// Command returned a null / void result.
2187    Null,
2188    /// Command returned a string value (evaluation result, title, URL, …).
2189    Value(String),
2190    /// Command returned binary data (screenshot image bytes).
2191    Binary(Vec<u8>),
2192}
2193
2194impl BridgeResponse {
2195    /// Returns `true` when the response is [`Ok`](BridgeResponse::Ok).
2196    pub fn is_ok(&self) -> bool {
2197        matches!(self, BridgeResponse::Ok)
2198    }
2199
2200    /// Returns `true` when the response is [`Err`](BridgeResponse::Err).
2201    pub fn is_err(&self) -> bool {
2202        matches!(self, BridgeResponse::Err(_))
2203    }
2204
2205    /// Converts [`Err`](BridgeResponse::Err) into `Result::Err`, wrapping all other
2206    /// variants in `Result::Ok`.
2207    pub fn ok(self) -> Result<Self, String> {
2208        match self {
2209            BridgeResponse::Err(e) => Err(e),
2210            other => Ok(other),
2211        }
2212    }
2213}
2214
2215/// Receiving end of a [`BridgeChannel`].
2216///
2217/// A worker thread (or event-loop iteration) calls [`recv`](BridgeReceiver::recv)
2218/// to obtain commands and their optional response channels, executes them against
2219/// the page, and sends back [`BridgeResponse`] values.
2220pub struct BridgeReceiver {
2221    rx: mpsc::Receiver<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>)>,
2222    alive: Arc<AtomicBool>,
2223}
2224
2225impl std::fmt::Debug for BridgeReceiver {
2226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2227        f.debug_struct("BridgeReceiver")
2228            .field("alive", &self.alive)
2229            .finish()
2230    }
2231}
2232
2233impl BridgeReceiver {
2234    /// Block until a command arrives or the channel is disconnected.
2235    pub fn recv(&self) -> Result<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>), String> {
2236        self.rx.recv().map_err(|_| "channel closed".to_string())
2237    }
2238
2239    /// Block for at most `timeout`, returning the command or a timeout error.
2240    pub fn recv_timeout(
2241        &self,
2242        timeout: Duration,
2243    ) -> Result<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>), String> {
2244        self.rx.recv_timeout(timeout).map_err(|e| format!("{}", e))
2245    }
2246
2247    /// Whether the bridge has been marked alive (both sides share the flag).
2248    pub fn is_alive(&self) -> bool {
2249        self.alive.load(Ordering::SeqCst)
2250    }
2251}
2252
2253/// Producer half of the bridge command channel.
2254///
2255/// Methods are thread-safe (`&self`) so a single channel can be shared across
2256/// threads for concurrent submission.
2257///
2258/// @trace REQ-BRW-003 [entity:BridgeChannel]
2259#[derive(Debug)]
2260pub struct BridgeChannel {
2261    tx: mpsc::Sender<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>)>,
2262    alive: Arc<AtomicBool>,
2263}
2264
2265impl BridgeChannel {
2266    /// Create a new bridge channel pair.
2267    ///
2268    /// Returns `(sender, receiver)` where commands flow sender → receiver and
2269    /// responses flow back via per-command one-shot channels.
2270    pub fn new() -> (Self, BridgeReceiver) {
2271        let (tx, rx) = mpsc::channel();
2272        let alive = Arc::new(AtomicBool::new(true));
2273        let channel = BridgeChannel {
2274            tx,
2275            alive: alive.clone(),
2276        };
2277        let receiver = BridgeReceiver { rx, alive };
2278        (channel, receiver)
2279    }
2280
2281    /// Send a command and block until the worker returns a response.
2282    pub fn send(&self, cmd: BridgeCommand) -> Result<BridgeResponse, String> {
2283        let (resp_tx, resp_rx) = mpsc::channel();
2284        self.tx
2285            .send((cmd, Some(resp_tx)))
2286            .map_err(|_| "bridge closed".to_string())?;
2287        resp_rx
2288            .recv()
2289            .map_err(|_| "response channel closed".to_string())
2290    }
2291
2292    /// Send a command and wait at most `timeout` for a response.
2293    pub fn send_timeout(
2294        &self,
2295        cmd: BridgeCommand,
2296        timeout: Duration,
2297    ) -> Result<BridgeResponse, String> {
2298        let (resp_tx, resp_rx) = mpsc::channel();
2299        self.tx
2300            .send((cmd, Some(resp_tx)))
2301            .map_err(|_| "bridge closed".to_string())?;
2302        resp_rx.recv_timeout(timeout).map_err(|e| format!("{}", e))
2303    }
2304
2305    /// Send a command without waiting for a response.
2306    ///
2307    /// The worker receives `None` for the responder slot and can skip
2308    /// the response-send step.
2309    pub fn fire_and_forget(&self, cmd: BridgeCommand) -> Result<(), String> {
2310        self.tx
2311            .send((cmd, None))
2312            .map_err(|_| "bridge closed".to_string())
2313    }
2314
2315    /// Whether the bridge is marked alive (both sender and receiver).
2316    pub fn is_alive(&self) -> bool {
2317        self.alive.load(Ordering::SeqCst)
2318    }
2319
2320    /// Mark the bridge as closed.
2321    ///
2322    /// This only sets a flag — the underlying channel remains connected.
2323    /// Dropping the [`BridgeChannel`] / [`BridgeReceiver`] pair fully tears
2324    /// down the transport.
2325    pub fn close(&self) {
2326        self.alive.store(false, Ordering::SeqCst);
2327    }
2328}
2329
2330/// High-level bridge that owns a [`BridgeChannel`] and provides the public
2331/// command API for the bao_browser runtime.
2332///
2333/// In production, a worker loop reads from the associated [`BridgeReceiver`]
2334/// and dispatches commands to a servo [`PageHandle`].  In tests the channel
2335/// alone is exercised.
2336///
2337/// @trace REQ-BRW-003 [entity:RuntimeBridge]
2338#[derive(Debug)]
2339pub struct RuntimeBridge {
2340    channel: BridgeChannel,
2341}
2342
2343impl RuntimeBridge {
2344    /// Create a fresh bridge, returning the sending half and the receiver.
2345    pub fn new() -> (Self, BridgeReceiver) {
2346        let (channel, receiver) = BridgeChannel::new();
2347        (RuntimeBridge { channel }, receiver)
2348    }
2349
2350    /// Send a command and wait for the response.  See [`BridgeChannel::send`].
2351    pub fn send(&self, cmd: BridgeCommand) -> Result<BridgeResponse, String> {
2352        self.channel.send(cmd)
2353    }
2354
2355    /// Send a command and wait at most `timeout` for a response.
2356    /// See [`BridgeChannel::send_timeout`].
2357    pub fn send_timeout(
2358        &self,
2359        cmd: BridgeCommand,
2360        timeout: Duration,
2361    ) -> Result<BridgeResponse, String> {
2362        self.channel.send_timeout(cmd, timeout)
2363    }
2364
2365    /// Send a command without waiting for a response.
2366    /// See [`BridgeChannel::fire_and_forget`].
2367    pub fn fire_and_forget(&self, cmd: BridgeCommand) -> Result<(), String> {
2368        self.channel.fire_and_forget(cmd)
2369    }
2370
2371    /// Whether the bridge is alive.  See [`BridgeChannel::is_alive`].
2372    pub fn is_alive(&self) -> bool {
2373        self.channel.is_alive()
2374    }
2375
2376    /// Mark the bridge closed.  See [`BridgeChannel::close`].
2377    pub fn close(&self) {
2378        self.channel.close();
2379    }
2380}
2381
2382#[cfg(test)]
2383mod tests {
2384    use std::sync::OnceLock;
2385    // ─── Polyfill validation ──────────────────────────────────────
2386    // @trace REQ-BRW-003 [req:REQ-BRW-003] [level:unit]
2387
2388    #[test]
2389    fn test_polyfills_are_valid_js() {
2390        assert!(!super::NODE_POLYFILLS.is_empty());
2391        assert!(super::NODE_POLYFILLS.contains("Buffer"));
2392        assert!(super::NODE_POLYFILLS.contains("require"));
2393        assert!(super::NODE_POLYFILLS.contains("process"));
2394    }
2395
2396    // ─── BridgeCommand / BridgeResponse / BridgeChannel extended tests ──
2397    // @trace REQ-BRW-003 [req:REQ-BRW-003] [level:unit]
2398
2399    #[test]
2400    fn bridge_command_navigate_equality() {
2401        let cmd1 = super::BridgeCommand::Navigate("https://example.com".into());
2402        let cmd2 = super::BridgeCommand::Navigate("https://example.com".into());
2403        let cmd3 = super::BridgeCommand::Navigate("https://other.com".into());
2404        assert_eq!(cmd1, cmd2);
2405        assert_ne!(cmd1, cmd3);
2406    }
2407
2408    #[test]
2409    fn bridge_command_evaluate_equality() {
2410        let cmd1 = super::BridgeCommand::Evaluate("1+1".into());
2411        let cmd2 = super::BridgeCommand::Evaluate("1+1".into());
2412        assert_eq!(cmd1, cmd2);
2413        assert_ne!(cmd1, super::BridgeCommand::Evaluate("2+2".into()));
2414    }
2415
2416    #[test]
2417    fn bridge_command_resize_equality() {
2418        assert_eq!(
2419            super::BridgeCommand::Resize(800, 600),
2420            super::BridgeCommand::Resize(800, 600)
2421        );
2422        assert_ne!(
2423            super::BridgeCommand::Resize(800, 600),
2424            super::BridgeCommand::Resize(1024, 768)
2425        );
2426    }
2427
2428    #[test]
2429    fn bridge_command_variants_distinct() {
2430        let cmds = [
2431            super::BridgeCommand::Navigate("x".into()),
2432            super::BridgeCommand::Evaluate("y".into()),
2433            super::BridgeCommand::Screenshot,
2434            super::BridgeCommand::Close,
2435            super::BridgeCommand::Resize(1, 1),
2436            super::BridgeCommand::GetTitle,
2437            super::BridgeCommand::GetUrl,
2438        ];
2439        for i in 0..cmds.len() {
2440            for j in 0..cmds.len() {
2441                if i != j {
2442                    assert_ne!(cmds[i], cmds[j]);
2443                }
2444            }
2445        }
2446    }
2447
2448    #[test]
2449    fn bridge_response_ok_is_ok() {
2450        let resp = super::BridgeResponse::Ok;
2451        assert!(resp.is_ok());
2452        assert!(!resp.is_err());
2453    }
2454
2455    #[test]
2456    fn bridge_response_err_is_err() {
2457        let resp = super::BridgeResponse::Err("failed".into());
2458        assert!(!resp.is_ok());
2459        assert!(resp.is_err());
2460    }
2461
2462    #[test]
2463    fn bridge_response_null_not_err() {
2464        let resp = super::BridgeResponse::Null;
2465        assert!(!resp.is_ok()); // Null is not BridgeResponse::Ok
2466        assert!(!resp.is_err()); // Null is also not an error
2467    }
2468
2469    #[test]
2470    fn bridge_response_value_not_err() {
2471        let resp = super::BridgeResponse::Value("result".into());
2472        assert!(!resp.is_ok()); // Value is not BridgeResponse::Ok
2473        assert!(!resp.is_err());
2474    }
2475
2476    #[test]
2477    fn bridge_response_binary_not_err() {
2478        let resp = super::BridgeResponse::Binary(vec![1, 2, 3]);
2479        assert!(!resp.is_ok()); // Binary is not BridgeResponse::Ok
2480        assert!(!resp.is_err());
2481    }
2482
2483    #[test]
2484    fn bridge_response_ok_method_wraps_non_err() {
2485        // .ok() converts Err → Result::Err, all others → Result::Ok
2486        assert!(super::BridgeResponse::Null.ok().is_ok());
2487        assert!(super::BridgeResponse::Value("v".into()).ok().is_ok());
2488        assert!(super::BridgeResponse::Binary(vec![]).ok().is_ok());
2489    }
2490
2491    #[test]
2492    fn bridge_response_ok_method_on_err() {
2493        let resp = super::BridgeResponse::Err("error msg".into());
2494        let result = resp.ok();
2495        assert!(result.is_err());
2496        assert_eq!(result.unwrap_err(), "error msg");
2497    }
2498
2499    #[test]
2500    fn bridge_response_ok_method_on_ok_variants() {
2501        assert!(super::BridgeResponse::Ok.ok().is_ok());
2502        assert!(super::BridgeResponse::Null.ok().is_ok());
2503        assert!(super::BridgeResponse::Value("v".into()).ok().is_ok());
2504        assert!(super::BridgeResponse::Binary(vec![]).ok().is_ok());
2505    }
2506
2507    #[test]
2508    fn bridge_channel_new_alive() {
2509        let (channel, _receiver) = super::BridgeChannel::new();
2510        assert!(channel.is_alive());
2511    }
2512
2513    #[test]
2514    fn bridge_channel_close_sets_not_alive() {
2515        let (channel, _receiver) = super::BridgeChannel::new();
2516        channel.close();
2517        assert!(!channel.is_alive());
2518    }
2519
2520    #[test]
2521    fn bridge_receiver_alive_shares_flag() {
2522        let (channel, receiver) = super::BridgeChannel::new();
2523        assert!(receiver.is_alive());
2524        channel.close();
2525        assert!(!receiver.is_alive());
2526    }
2527
2528    #[test]
2529    fn bridge_channel_fire_and_forget() {
2530        let (channel, receiver) = super::BridgeChannel::new();
2531        assert!(channel
2532            .fire_and_forget(super::BridgeCommand::GetTitle)
2533            .is_ok());
2534        let (cmd, responder) = receiver.recv().unwrap();
2535        assert_eq!(cmd, super::BridgeCommand::GetTitle);
2536        assert!(responder.is_none());
2537    }
2538
2539    #[test]
2540    fn bridge_channel_send_with_response() {
2541        let (channel, receiver) = super::BridgeChannel::new();
2542        // send() blocks until response — we need a worker thread
2543        let worker = std::thread::spawn(move || {
2544            let (_cmd, responder) = receiver.recv().unwrap();
2545            if let Some(resp_tx) = responder {
2546                resp_tx
2547                    .send(super::BridgeResponse::Value("title".into()))
2548                    .unwrap();
2549            }
2550        });
2551        let result = channel.send(super::BridgeCommand::GetTitle).unwrap();
2552        assert_eq!(result, super::BridgeResponse::Value("title".into()));
2553        worker.join().unwrap();
2554    }
2555
2556    #[test]
2557    fn runtime_bridge_new_alive() {
2558        let (bridge, _receiver) = super::RuntimeBridge::new();
2559        assert!(bridge.is_alive());
2560    }
2561
2562    #[test]
2563    fn runtime_bridge_close() {
2564        let (bridge, _receiver) = super::RuntimeBridge::new();
2565        bridge.close();
2566        assert!(!bridge.is_alive());
2567    }
2568
2569    #[test]
2570    fn runtime_bridge_fire_and_forget() {
2571        let (bridge, receiver) = super::RuntimeBridge::new();
2572        assert!(bridge.fire_and_forget(super::BridgeCommand::Close).is_ok());
2573        let (cmd, responder) = receiver.recv().unwrap();
2574        assert_eq!(cmd, super::BridgeCommand::Close);
2575        assert!(responder.is_none());
2576    }
2577
2578    // ═══════════════════════════════════════════════════════════════════════
2579    // Extended unit tests for bridge types and polyfills
2580    // @trace REQ-BRW-003 [req:REQ-BRW-003] [level:unit]
2581    // ═══════════════════════════════════════════════════════════════════════
2582
2583    // ─── BridgeCommand Debug format tests ──────────────────────────────────
2584
2585    #[test]
2586    fn bridge_command_debug_format_navigate() {
2587        let cmd = super::BridgeCommand::Navigate("https://example.com".into());
2588        let debug_str = format!("{:?}", cmd);
2589        assert!(debug_str.contains("Navigate"));
2590        assert!(debug_str.contains("https://example.com"));
2591    }
2592
2593    #[test]
2594    fn bridge_command_debug_format_evaluate() {
2595        let cmd = super::BridgeCommand::Evaluate("return 42".into());
2596        let debug_str = format!("{:?}", cmd);
2597        assert!(debug_str.contains("Evaluate"));
2598        assert!(debug_str.contains("return 42"));
2599    }
2600
2601    #[test]
2602    fn bridge_command_debug_format_screenshot() {
2603        let cmd = super::BridgeCommand::Screenshot;
2604        let debug_str = format!("{:?}", cmd);
2605        assert!(debug_str.contains("Screenshot"));
2606    }
2607
2608    #[test]
2609    fn bridge_command_debug_format_close() {
2610        let cmd = super::BridgeCommand::Close;
2611        let debug_str = format!("{:?}", cmd);
2612        assert!(debug_str.contains("Close"));
2613    }
2614
2615    #[test]
2616    fn bridge_command_debug_format_resize() {
2617        let cmd = super::BridgeCommand::Resize(1920, 1080);
2618        let debug_str = format!("{:?}", cmd);
2619        assert!(debug_str.contains("Resize"));
2620        assert!(debug_str.contains("1920"));
2621        assert!(debug_str.contains("1080"));
2622    }
2623
2624    #[test]
2625    fn bridge_command_debug_format_get_title() {
2626        let cmd = super::BridgeCommand::GetTitle;
2627        let debug_str = format!("{:?}", cmd);
2628        assert!(debug_str.contains("GetTitle"));
2629    }
2630
2631    #[test]
2632    fn bridge_command_debug_format_get_url() {
2633        let cmd = super::BridgeCommand::GetUrl;
2634        let debug_str = format!("{:?}", cmd);
2635        assert!(debug_str.contains("GetUrl"));
2636    }
2637
2638    // ─── BridgeCommand Clone tests ────────────────────────────────────────
2639
2640    #[test]
2641    fn bridge_command_clone_navigate() {
2642        let cmd = super::BridgeCommand::Navigate("https://test.com".into());
2643        let cloned = cmd.clone();
2644        assert_eq!(cmd, cloned);
2645    }
2646
2647    #[test]
2648    fn bridge_command_clone_evaluate() {
2649        let cmd = super::BridgeCommand::Evaluate("x + y".into());
2650        let cloned = cmd.clone();
2651        assert_eq!(cmd, cloned);
2652    }
2653
2654    #[test]
2655    fn bridge_command_clone_resize() {
2656        let cmd = super::BridgeCommand::Resize(1024, 768);
2657        let cloned = cmd.clone();
2658        assert_eq!(cmd, cloned);
2659    }
2660
2661    // ─── BridgeResponse Debug/Clone/Equality tests ────────────────────────
2662
2663    #[test]
2664    fn bridge_response_debug_format_ok() {
2665        let resp = super::BridgeResponse::Ok;
2666        let debug_str = format!("{:?}", resp);
2667        assert!(debug_str.contains("Ok"));
2668    }
2669
2670    #[test]
2671    fn bridge_response_debug_format_err() {
2672        let resp = super::BridgeResponse::Err("something went wrong".into());
2673        let debug_str = format!("{:?}", resp);
2674        assert!(debug_str.contains("Err"));
2675        assert!(debug_str.contains("something went wrong"));
2676    }
2677
2678    #[test]
2679    fn bridge_response_debug_format_null() {
2680        let resp = super::BridgeResponse::Null;
2681        let debug_str = format!("{:?}", resp);
2682        assert!(debug_str.contains("Null"));
2683    }
2684
2685    #[test]
2686    fn bridge_response_debug_format_value() {
2687        let resp = super::BridgeResponse::Value("result string".into());
2688        let debug_str = format!("{:?}", resp);
2689        assert!(debug_str.contains("Value"));
2690        assert!(debug_str.contains("result string"));
2691    }
2692
2693    #[test]
2694    fn bridge_response_debug_format_binary() {
2695        let resp = super::BridgeResponse::Binary(vec![0xDE, 0xAD, 0xBE, 0xEF]);
2696        let debug_str = format!("{:?}", resp);
2697        assert!(debug_str.contains("Binary"));
2698    }
2699
2700    #[test]
2701    fn bridge_response_clone_ok() {
2702        let resp = super::BridgeResponse::Ok;
2703        let cloned = resp.clone();
2704        assert_eq!(resp, cloned);
2705    }
2706
2707    #[test]
2708    fn bridge_response_clone_err() {
2709        let resp = super::BridgeResponse::Err("error".into());
2710        let cloned = resp.clone();
2711        assert_eq!(resp, cloned);
2712    }
2713
2714    #[test]
2715    fn bridge_response_clone_value() {
2716        let resp = super::BridgeResponse::Value("value".into());
2717        let cloned = resp.clone();
2718        assert_eq!(resp, cloned);
2719    }
2720
2721    #[test]
2722    fn bridge_response_clone_binary() {
2723        let resp = super::BridgeResponse::Binary(vec![1, 2, 3, 4]);
2724        let cloned = resp.clone();
2725        assert_eq!(resp, cloned);
2726    }
2727
2728    #[test]
2729    fn bridge_response_equality_ok() {
2730        assert_eq!(super::BridgeResponse::Ok, super::BridgeResponse::Ok);
2731    }
2732
2733    #[test]
2734    fn bridge_response_equality_err() {
2735        assert_eq!(
2736            super::BridgeResponse::Err("same error".into()),
2737            super::BridgeResponse::Err("same error".into())
2738        );
2739        assert_ne!(
2740            super::BridgeResponse::Err("error a".into()),
2741            super::BridgeResponse::Err("error b".into())
2742        );
2743    }
2744
2745    #[test]
2746    fn bridge_response_equality_value() {
2747        assert_eq!(
2748            super::BridgeResponse::Value("same".into()),
2749            super::BridgeResponse::Value("same".into())
2750        );
2751        assert_ne!(
2752            super::BridgeResponse::Value("a".into()),
2753            super::BridgeResponse::Value("b".into())
2754        );
2755    }
2756
2757    #[test]
2758    fn bridge_response_equality_binary() {
2759        assert_eq!(
2760            super::BridgeResponse::Binary(vec![1, 2, 3]),
2761            super::BridgeResponse::Binary(vec![1, 2, 3])
2762        );
2763        assert_ne!(
2764            super::BridgeResponse::Binary(vec![1, 2, 3]),
2765            super::BridgeResponse::Binary(vec![1, 2, 4])
2766        );
2767    }
2768
2769    #[test]
2770    fn bridge_response_variants_distinct() {
2771        let responses = [
2772            super::BridgeResponse::Ok,
2773            super::BridgeResponse::Err("e".into()),
2774            super::BridgeResponse::Null,
2775            super::BridgeResponse::Value("v".into()),
2776            super::BridgeResponse::Binary(vec![1]),
2777        ];
2778        for i in 0..responses.len() {
2779            for j in 0..responses.len() {
2780                if i != j {
2781                    assert_ne!(responses[i], responses[j]);
2782                }
2783            }
2784        }
2785    }
2786
2787    // ─── BridgeChannel edge case tests ────────────────────────────────────
2788
2789    #[test]
2790    fn bridge_channel_send_timeout_zero_timeout_returns_err() {
2791        // send_timeout with Duration::ZERO: command is sent to channel,
2792        // but no worker responds within 0ms → timeout error.
2793        let (channel, receiver) = super::BridgeChannel::new();
2794        // Drain the receiver in a separate thread so the send doesn't block
2795        let _drainer = std::thread::spawn(move || {
2796            // Just drain the command, don't respond
2797            let _ = receiver.recv();
2798        });
2799        let result = channel.send_timeout(
2800            super::BridgeCommand::GetTitle,
2801            std::time::Duration::from_secs(0),
2802        );
2803        assert!(result.is_err());
2804    }
2805
2806    #[test]
2807    fn bridge_channel_send_timeout_short_timeout() {
2808        let (channel, _receiver) = super::BridgeChannel::new();
2809        // No worker to respond — should timeout
2810        let result = channel.send_timeout(
2811            super::BridgeCommand::GetTitle,
2812            std::time::Duration::from_millis(1),
2813        );
2814        assert!(result.is_err());
2815    }
2816
2817    #[test]
2818    fn bridge_channel_fire_and_forget_multiple() {
2819        let (channel, receiver) = super::BridgeChannel::new();
2820        assert!(channel
2821            .fire_and_forget(super::BridgeCommand::GetTitle)
2822            .is_ok());
2823        assert!(channel
2824            .fire_and_forget(super::BridgeCommand::GetUrl)
2825            .is_ok());
2826        assert!(channel
2827            .fire_and_forget(super::BridgeCommand::Screenshot)
2828            .is_ok());
2829
2830        let (cmd1, _) = receiver.recv().unwrap();
2831        let (cmd2, _) = receiver.recv().unwrap();
2832        let (cmd3, _) = receiver.recv().unwrap();
2833
2834        assert_eq!(cmd1, super::BridgeCommand::GetTitle);
2835        assert_eq!(cmd2, super::BridgeCommand::GetUrl);
2836        assert_eq!(cmd3, super::BridgeCommand::Screenshot);
2837    }
2838
2839    #[test]
2840    fn bridge_channel_close_then_send_fails() {
2841        let (channel, receiver) = super::BridgeChannel::new();
2842        channel.close();
2843        // Channel is marked closed but underlying mpsc still works
2844        // The alive flag is just a marker, not a hard barrier
2845        // Verify the alive flag is set
2846        assert!(!channel.is_alive());
2847        // Drop receiver to actually close the channel
2848        drop(receiver);
2849        // Now send should fail
2850        let result = channel.send(super::BridgeCommand::GetTitle);
2851        assert!(result.is_err());
2852    }
2853
2854    #[test]
2855    fn bridge_channel_close_then_fire_and_forget_fails() {
2856        let (channel, receiver) = super::BridgeChannel::new();
2857        channel.close();
2858        // Drop receiver to actually close the channel
2859        drop(receiver);
2860        let result = channel.fire_and_forget(super::BridgeCommand::Close);
2861        assert!(result.is_err());
2862    }
2863
2864    #[test]
2865    fn bridge_channel_multiple_send_response_pairs() {
2866        let (channel, receiver) = super::BridgeChannel::new();
2867
2868        let worker = std::thread::spawn(move || {
2869            for _ in 0..3 {
2870                let (cmd, responder) = receiver.recv().unwrap();
2871                if let Some(resp_tx) = responder {
2872                    let resp = match cmd {
2873                        super::BridgeCommand::GetTitle => {
2874                            super::BridgeResponse::Value("Title".into())
2875                        }
2876                        super::BridgeCommand::GetUrl => {
2877                            super::BridgeResponse::Value("https://url.com".into())
2878                        }
2879                        _ => super::BridgeResponse::Ok,
2880                    };
2881                    resp_tx.send(resp).unwrap();
2882                }
2883            }
2884        });
2885
2886        let r1 = channel.send(super::BridgeCommand::GetTitle).unwrap();
2887        let r2 = channel.send(super::BridgeCommand::GetUrl).unwrap();
2888        let r3 = channel.send(super::BridgeCommand::Screenshot).unwrap();
2889
2890        assert_eq!(r1, super::BridgeResponse::Value("Title".into()));
2891        assert_eq!(r2, super::BridgeResponse::Value("https://url.com".into()));
2892        assert_eq!(r3, super::BridgeResponse::Ok);
2893
2894        worker.join().unwrap();
2895    }
2896
2897    // ─── BridgeReceiver edge case tests ───────────────────────────────────
2898
2899    #[test]
2900    fn bridge_receiver_recv_timeout_short() {
2901        let (_channel, receiver) = super::BridgeChannel::new();
2902        // No command sent — should timeout
2903        let result = receiver.recv_timeout(std::time::Duration::from_millis(1));
2904        assert!(result.is_err());
2905    }
2906
2907    #[test]
2908    fn bridge_receiver_recv_after_channel_dropped() {
2909        let (channel, receiver) = super::BridgeChannel::new();
2910        drop(channel);
2911        // recv should return error when sender is dropped
2912        let result = receiver.recv();
2913        assert!(result.is_err());
2914        assert_eq!(result.unwrap_err(), "channel closed");
2915    }
2916
2917    #[test]
2918    fn bridge_receiver_debug_format() {
2919        let (_channel, receiver) = super::BridgeChannel::new();
2920        let debug_str = format!("{:?}", receiver);
2921        assert!(debug_str.contains("BridgeReceiver"));
2922        assert!(debug_str.contains("alive"));
2923    }
2924
2925    // ─── RuntimeBridge edge case tests ────────────────────────────────────
2926
2927    #[test]
2928    fn runtime_bridge_send_timeout() {
2929        let (bridge, receiver) = super::RuntimeBridge::new();
2930
2931        let worker = std::thread::spawn(move || {
2932            let (cmd, responder) = receiver.recv().unwrap();
2933            if let Some(resp_tx) = responder {
2934                let resp = match cmd {
2935                    super::BridgeCommand::Evaluate(ref code) => {
2936                        super::BridgeResponse::Value(format!("evaluated: {}", code))
2937                    }
2938                    _ => super::BridgeResponse::Ok,
2939                };
2940                resp_tx.send(resp).unwrap();
2941            }
2942        });
2943
2944        let result = bridge
2945            .send_timeout(
2946                super::BridgeCommand::Evaluate("1+1".into()),
2947                std::time::Duration::from_secs(5),
2948            )
2949            .unwrap();
2950        assert_eq!(
2951            result,
2952            super::BridgeResponse::Value("evaluated: 1+1".into())
2953        );
2954
2955        worker.join().unwrap();
2956    }
2957
2958    #[test]
2959    fn runtime_bridge_close_propagates() {
2960        let (bridge, receiver) = super::RuntimeBridge::new();
2961        assert!(bridge.is_alive());
2962        assert!(receiver.is_alive());
2963        bridge.close();
2964        assert!(!bridge.is_alive());
2965        assert!(!receiver.is_alive());
2966    }
2967
2968    #[test]
2969    fn runtime_bridge_fire_and_forget_after_close_still_works() {
2970        let (bridge, receiver) = super::RuntimeBridge::new();
2971        bridge.close();
2972        // close() only sets the alive flag, doesn't close the channel
2973        // fire_and_forget should still work until receiver is dropped
2974        assert!(bridge.fire_and_forget(super::BridgeCommand::Close).is_ok());
2975        let (cmd, responder) = receiver.recv().unwrap();
2976        assert_eq!(cmd, super::BridgeCommand::Close);
2977        assert!(responder.is_none());
2978    }
2979
2980    #[test]
2981    fn runtime_bridge_send_after_receiver_dropped() {
2982        let (bridge, receiver) = super::RuntimeBridge::new();
2983        drop(receiver);
2984        let result = bridge.send(super::BridgeCommand::GetTitle);
2985        assert!(result.is_err());
2986    }
2987
2988    #[test]
2989    fn runtime_bridge_debug_format() {
2990        let (bridge, _receiver) = super::RuntimeBridge::new();
2991        let debug_str = format!("{:?}", bridge);
2992        assert!(debug_str.contains("RuntimeBridge"));
2993    }
2994
2995    // ─── NODE_POLYFILLS content tests ─────────────────────────────────────
2996
2997    #[test]
2998    fn node_polyfills_process_version() {
2999        let poly = super::NODE_POLYFILLS;
3000        assert!(poly.contains("version: 'v20.11.0'"));
3001    }
3002
3003    #[test]
3004    fn node_polyfills_process_versions_structure() {
3005        let poly = super::NODE_POLYFILLS;
3006        // Check key version fields exist
3007        assert!(poly.contains("node: '20.11.0'"));
3008        assert!(poly.contains("v8: '12.4.254.14'"));
3009        assert!(poly.contains("uv: '1.27.0'"));
3010        assert!(poly.contains("zlib: '1.2.13'"));
3011        assert!(poly.contains("brotli: '1.0.9'"));
3012        assert!(poly.contains("ares: '1.19.1'"));
3013        assert!(poly.contains("modules: '115'"));
3014        assert!(poly.contains("openssl: '3.0.12'"));
3015        assert!(poly.contains("icu: '74.2'"));
3016        assert!(poly.contains("bun: '1.0.25'"));
3017        assert!(poly.contains("bao: '0.1.0'"));
3018    }
3019
3020    #[test]
3021    fn node_polyfills_process_env() {
3022        let poly = super::NODE_POLYFILLS;
3023        assert!(poly.contains("env:"));
3024        assert!(poly.contains("e.HOME = '/'"));
3025        assert!(poly.contains("e.PATH = '/usr/local/bin:/usr/bin:/bin'"));
3026        assert!(poly.contains("e.TERM = 'xterm-256color'"));
3027        assert!(poly.contains("e.NODE_VERSION = '20.11.0'"));
3028        assert!(poly.contains("e.BAO_VERSION = '0.1.0'"));
3029    }
3030
3031    #[test]
3032    fn node_polyfills_process_argv() {
3033        let poly = super::NODE_POLYFILLS;
3034        assert!(poly.contains("argv:"));
3035        assert!(poly.contains("argv0: 'bao'"));
3036    }
3037
3038    #[test]
3039    fn node_polyfills_buffer_from() {
3040        let poly = super::NODE_POLYFILLS;
3041        assert!(poly.contains("B.from = function"));
3042        assert!(poly.contains("if (data instanceof B)"));
3043        assert!(poly.contains("if (encoding === 'hex')"));
3044        assert!(poly.contains("if (encoding === 'base64')"));
3045    }
3046
3047    #[test]
3048    fn node_polyfills_buffer_alloc() {
3049        let poly = super::NODE_POLYFILLS;
3050        assert!(poly.contains("B.alloc = function"));
3051        assert!(poly.contains("B.allocUnsafe = function"));
3052        assert!(poly.contains("B.allocUnsafeSlow = function"));
3053    }
3054
3055    #[test]
3056    fn node_polyfills_buffer_static_methods() {
3057        let poly = super::NODE_POLYFILLS;
3058        assert!(poly.contains("B.isBuffer = function"));
3059        assert!(poly.contains("B.concat = function"));
3060        assert!(poly.contains("B.byteLength = function"));
3061        assert!(poly.contains("B.compare = function"));
3062    }
3063
3064    #[test]
3065    fn node_polyfills_buffer_instance_methods() {
3066        let poly = super::NODE_POLYFILLS;
3067        assert!(poly.contains("B.prototype.slice = function"));
3068        assert!(poly.contains("B.prototype.toString = function"));
3069        assert!(poly.contains("B.prototype.toJSON = function"));
3070        assert!(poly.contains("B.prototype.equals = function"));
3071        assert!(poly.contains("B.prototype.compare = function"));
3072        assert!(poly.contains("B.prototype.copy = function"));
3073        assert!(poly.contains("B.prototype.fill = function"));
3074        assert!(poly.contains("B.prototype.write = function"));
3075        assert!(poly.contains("B.prototype.indexOf = function"));
3076    }
3077
3078    #[test]
3079    fn node_polyfills_buffer_read_methods() {
3080        let poly = super::NODE_POLYFILLS;
3081        assert!(poly.contains("B.prototype.readUInt8 = function"));
3082        assert!(poly.contains("B.prototype.readUInt16LE = function"));
3083        assert!(poly.contains("B.prototype.readUInt16BE = function"));
3084        assert!(poly.contains("B.prototype.readUInt32LE = function"));
3085        assert!(poly.contains("B.prototype.readInt8 = function"));
3086        assert!(poly.contains("B.prototype.readInt16LE = function"));
3087        assert!(poly.contains("B.prototype.readInt32LE = function"));
3088        assert!(poly.contains("B.prototype.readFloatLE = function"));
3089        assert!(poly.contains("B.prototype.readDoubleLE = function"));
3090    }
3091
3092    #[test]
3093    fn node_polyfills_buffer_write_methods() {
3094        let poly = super::NODE_POLYFILLS;
3095        assert!(poly.contains("B.prototype.writeUInt8 = function"));
3096        assert!(poly.contains("B.prototype.writeUInt16LE = function"));
3097        assert!(poly.contains("B.prototype.writeUInt32LE = function"));
3098        assert!(poly.contains("B.prototype.writeInt8 = function"));
3099        assert!(poly.contains("B.prototype.writeInt16LE = function"));
3100        assert!(poly.contains("B.prototype.writeInt32LE = function"));
3101        assert!(poly.contains("B.prototype.writeFloatLE = function"));
3102        assert!(poly.contains("B.prototype.writeDoubleLE = function"));
3103    }
3104
3105    #[test]
3106    fn node_polyfills_require_cache() {
3107        let poly = super::NODE_POLYFILLS;
3108        assert!(poly.contains("require.cache = _module_cache"));
3109        assert!(poly.contains("_module_cache = {}"));
3110    }
3111
3112    #[test]
3113    fn node_polyfills_require_builtin_modules() {
3114        let poly = super::NODE_POLYFILLS;
3115        // Check key built-in modules are defined
3116        assert!(poly.contains("'fs':"));
3117        assert!(poly.contains("'path':"));
3118        assert!(poly.contains("'url':"));
3119        assert!(poly.contains("'querystring':"));
3120        assert!(poly.contains("'events':"));
3121        assert!(poly.contains("'util':"));
3122        assert!(poly.contains("'stream':"));
3123        assert!(poly.contains("'buffer':"));
3124        assert!(poly.contains("'crypto':"));
3125        assert!(poly.contains("'os':"));
3126        assert!(poly.contains("'assert':"));
3127        assert!(poly.contains("'timers':"));
3128    }
3129
3130    #[test]
3131    fn node_polyfills_path_module() {
3132        let poly = super::NODE_POLYFILLS;
3133        assert!(poly.contains("join: function"));
3134        assert!(poly.contains("resolve: function"));
3135        assert!(poly.contains("dirname: function"));
3136        assert!(poly.contains("basename: function"));
3137        assert!(poly.contains("extname: function"));
3138        assert!(poly.contains("sep: '/'"));
3139        assert!(poly.contains("posix:"));
3140        assert!(poly.contains("win32:"));
3141    }
3142
3143    #[test]
3144    fn node_polyfills_global_alias() {
3145        let poly = super::NODE_POLYFILLS;
3146        assert!(poly.contains("global = globalThis"));
3147    }
3148
3149    #[test]
3150    fn node_polyfills_text_encoder_decoder() {
3151        let poly = super::NODE_POLYFILLS;
3152        assert!(poly.contains("TextEncoder"));
3153        assert!(poly.contains("TextDecoder"));
3154    }
3155
3156    #[test]
3157    fn node_polyfills_btoa_atob() {
3158        let poly = super::NODE_POLYFILLS;
3159        assert!(poly.contains("btoa = function"));
3160        assert!(poly.contains("atob = function"));
3161        assert!(poly.contains("_b64chars"));
3162    }
3163
3164    // ─── Edge case tests ──────────────────────────────────────────────────
3165
3166    #[test]
3167    fn bridge_command_empty_navigate_url() {
3168        let cmd = super::BridgeCommand::Navigate("".into());
3169        let cloned = cmd.clone();
3170        assert_eq!(cmd, cloned);
3171        let debug_str = format!("{:?}", cmd);
3172        assert!(debug_str.contains("Navigate"));
3173    }
3174
3175    #[test]
3176    fn bridge_command_empty_evaluate_string() {
3177        let cmd = super::BridgeCommand::Evaluate("".into());
3178        let cloned = cmd.clone();
3179        assert_eq!(cmd, cloned);
3180        let debug_str = format!("{:?}", cmd);
3181        assert!(debug_str.contains("Evaluate"));
3182    }
3183
3184    #[test]
3185    fn bridge_response_empty_value() {
3186        let resp = super::BridgeResponse::Value("".into());
3187        assert!(!resp.is_ok());
3188        assert!(!resp.is_err());
3189        let result = resp.ok();
3190        assert!(result.is_ok());
3191        assert_eq!(result.unwrap(), super::BridgeResponse::Value("".into()));
3192    }
3193
3194    #[test]
3195    fn bridge_response_empty_binary() {
3196        let resp = super::BridgeResponse::Binary(vec![]);
3197        assert!(!resp.is_ok());
3198        assert!(!resp.is_err());
3199        let cloned = resp.clone();
3200        assert_eq!(resp, cloned);
3201    }
3202
3203    #[test]
3204    fn bridge_response_large_binary_payload() {
3205        // Create a large binary payload (1MB)
3206        let large_data: Vec<u8> = (0..=255).cycle().take(1024 * 1024).collect();
3207        let resp = super::BridgeResponse::Binary(large_data.clone());
3208        assert!(!resp.is_ok());
3209        assert!(!resp.is_err());
3210        let cloned = resp.clone();
3211        assert_eq!(resp, cloned);
3212        // Verify the data is intact
3213        if let super::BridgeResponse::Binary(data) = cloned {
3214            assert_eq!(data.len(), 1024 * 1024);
3215            assert_eq!(data[0], 0);
3216            assert_eq!(data[255], 255);
3217            assert_eq!(data[256], 0); // cycles back
3218        } else {
3219            panic!("Expected Binary variant");
3220        }
3221    }
3222
3223    #[test]
3224    fn bridge_command_unicode_navigate_url() {
3225        let unicode_url = "https://例子.测试/路径?查询=值#片段";
3226        let cmd = super::BridgeCommand::Navigate(unicode_url.into());
3227        let cloned = cmd.clone();
3228        assert_eq!(cmd, cloned);
3229        let debug_str = format!("{:?}", cmd);
3230        assert!(debug_str.contains(unicode_url));
3231    }
3232
3233    #[test]
3234    fn bridge_command_unicode_evaluate_string() {
3235        let unicode_code = "console.log('你好世界 🎉')";
3236        let cmd = super::BridgeCommand::Evaluate(unicode_code.into());
3237        let cloned = cmd.clone();
3238        assert_eq!(cmd, cloned);
3239        let debug_str = format!("{:?}", cmd);
3240        assert!(debug_str.contains(unicode_code));
3241    }
3242
3243    #[test]
3244    fn bridge_response_unicode_value() {
3245        let unicode_value = "结果: 成功 ✅ 日本語 한국어 العربية";
3246        let resp = super::BridgeResponse::Value(unicode_value.into());
3247        let cloned = resp.clone();
3248        assert_eq!(resp, cloned);
3249        let debug_str = format!("{:?}", resp);
3250        assert!(debug_str.contains(unicode_value));
3251    }
3252
3253    #[test]
3254    fn bridge_response_unicode_error() {
3255        let unicode_error = "错误: 文件未找到 📁❌";
3256        let resp = super::BridgeResponse::Err(unicode_error.into());
3257        assert!(resp.is_err());
3258        let result = resp.ok();
3259        assert!(result.is_err());
3260        assert_eq!(result.unwrap_err(), unicode_error);
3261    }
3262
3263    #[test]
3264    fn bridge_channel_debug_format() {
3265        let (channel, _receiver) = super::BridgeChannel::new();
3266        let debug_str = format!("{:?}", channel);
3267        assert!(debug_str.contains("BridgeChannel"));
3268        assert!(debug_str.contains("alive"));
3269    }
3270
3271    // ── REQ-SEC-002/003: Runtime bridge security structural verification ──
3272    // @trace TEST-SEC-003 [req:REQ-SEC-001,REQ-SEC-002,REQ-SEC-003] [level:unit]
3273
3274    /// Verify install_all_native calls install_web_apis (NOT install_all or install_node_apis).
3275    /// REQ-SEC-003: The bridge must NOT inject Node APIs on page global.
3276    #[test]
3277    fn runtime_bridge_calls_web_apis_not_install_all() {
3278        let source = include_str!("runtime_bridge.rs");
3279        let func_start = source
3280            .find("unsafe fn install_all_native")
3281            .expect("install_all_native function not found");
3282        // Extract just the function body — 5000 chars max to avoid test code.
3283        let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
3284
3285        assert!(
3286            func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3287            "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
3288        );
3289        assert!(
3290            func_body.contains("bun_runtime::timers::install_timer_globals"),
3291            "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
3292        );
3293        assert!(
3294            !func_body.contains("globals::install_all("),
3295            "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
3296        );
3297        assert!(
3298            !func_body.contains("globals::install_node_apis("),
3299            "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis()"
3300        );
3301    }
3302
3303    /// Verify worker_scope_init_native installs ONLY stealth properties, NOT
3304    /// any bun_runtime Web APIs (fetch/timers/crypto/performance/etc.).
3305    /// BCE-20260627-009: servo's DedicatedWorkerGlobalScope provides Web APIs
3306    /// natively via its resource thread; Bao's duplicate installation caused
3307    /// SIGSEGV in js::Atomize (Compartment not entered) and promise execution
3308    /// model conflicts. stealth getter install (DEC-WK-007) is the only allowed
3309    /// installation in worker scope_init.
3310    #[test]
3311    fn worker_scope_init_native_is_stealth_only() {
3312        let source = include_str!("runtime_bridge.rs");
3313        let func_start = source
3314            .find("unsafe fn worker_scope_init_native")
3315            .expect("worker_scope_init_native function not found");
3316        // Extract just the function body — bounded to next fn/doc to avoid overflow.
3317        let search_end = source[func_start..]
3318            .find("\n// @trace REQ-SEC-003 [entity:WebPolyfills]")
3319            .or_else(|| source[func_start..].find("\nconst WEB_POLYFILLS"))
3320            .unwrap_or(5000)
3321            .min(5000);
3322        let func_body = &source[func_start..func_start + search_end];
3323
3324        // stealth getter install MUST be present (DEC-WK-007)
3325        assert!(
3326            func_body.contains("install_stealth_props"),
3327            "BCE-20260627-009 REGRESSION: worker_scope_init_native must install stealth props (DEC-WK-007)"
3328        );
3329
3330        // Web APIs MUST NOT be present — servo DedicatedWorkerGlobalScope provides them natively
3331        assert!(
3332            !func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3333            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install fetch (servo native)"
3334        );
3335        assert!(
3336            !func_body.contains("bun_runtime::timers::install_timer_globals"),
3337            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install timers (servo native)"
3338        );
3339        assert!(
3340            !func_body.contains("bun_runtime::web_api::install_performance"),
3341            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install performance (servo native)"
3342        );
3343        assert!(
3344            !func_body.contains("bun_runtime::globals::install_crypto_global"),
3345            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install crypto (servo native)"
3346        );
3347        assert!(
3348            !func_body.contains("bun_runtime::globals::install_structured_clone"),
3349            "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install structuredClone (servo native)"
3350        );
3351    }
3352
3353    /// Verify create_node_realm_native creates Node Realm in NewCompartmentAndZone.
3354    /// REQ-SEC-002: Node Realm must be in its own Compartment — physically isolated.
3355    #[test]
3356    fn runtime_bridge_node_realm_uses_new_compartment() {
3357        let source = include_str!("runtime_bridge.rs");
3358
3359        let func_start = source
3360            .find("unsafe fn create_node_realm_native")
3361            .expect("create_node_realm_native function not found");
3362        let func_body_start = source[func_start..]
3363            .find("{")
3364            .expect("function body start not found");
3365        let search_limit = source[func_start + func_body_start..]
3366            .find("pub fn inject_node_apis")
3367            .or_else(|| {
3368                source[func_start + func_body_start..].find("/// Inject Node.js APIs as native")
3369            })
3370            .unwrap_or(3000)
3371            .min(3000);
3372        let func_body =
3373            &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3374
3375        assert!(
3376            func_body.contains("NewCompartmentAndZone"),
3377            "REQ-SEC-002 REGRESSION: create_node_realm_native must use NewCompartmentAndZone"
3378        );
3379        assert!(
3380            func_body.contains("SIMPLE_GLOBAL_CLASS"),
3381            "REQ-SEC-002 REGRESSION: create_node_realm_native must use SIMPLE_GLOBAL_CLASS"
3382        );
3383        assert!(
3384            func_body.contains("AutoRealm::new_from_handle"),
3385            "REQ-SEC-002 REGRESSION: create_node_realm_native must use AutoRealm"
3386        );
3387        assert!(
3388            func_body.contains("bun_runtime::globals::install_node_apis"),
3389            "REQ-SEC-002 REGRESSION: Node APIs must be installed on Node Realm global"
3390        );
3391    }
3392
3393    /// Verify evaluate_in_node_realm uses AutoRealm for Compartment isolation.
3394    /// REQ-SEC-002: Scripts must execute in Node Realm, not Page Realm.
3395    #[test]
3396    fn runtime_bridge_evaluate_in_node_realm_uses_auto_realm() {
3397        let source = include_str!("runtime_bridge.rs");
3398
3399        let func_start = source
3400            .find("pub unsafe fn evaluate_in_node_realm")
3401            .expect("evaluate_in_node_realm function not found");
3402        let func_body_start = source[func_start..]
3403            .find("{")
3404            .expect("function body start not found");
3405        let search_limit = source[func_start + func_body_start..]
3406            .find("unsafe fn create_node_realm_native")
3407            .unwrap_or(3000)
3408            .min(3000);
3409        let func_body =
3410            &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3411
3412        assert!(
3413            func_body.contains("AutoRealm::new"),
3414            "REQ-SEC-002 REGRESSION: evaluate_in_node_realm must use AutoRealm"
3415        );
3416        assert!(
3417            func_body.contains("evaluate_script"),
3418            "REQ-SEC-002: evaluate_in_node_realm must call evaluate_script"
3419        );
3420    }
3421
3422    /// Verify per-page Node Realm storage exists (REQ-SEC-002).
3423    /// Node Realm globals are stored keyed by WebViewId (NOT *mut JSObject).
3424    /// BCE-20260621-001: WebViewId-keyed storage eliminates cross-thread
3425    /// *mut JSObject dereferences. servo routes callbacks by WebViewId, so
3426    /// pointers stored under WebViewId are always accessed on the owning
3427    /// ScriptThread — no activation-stack corruption.
3428    #[test]
3429    fn runtime_bridge_has_per_page_node_realm_storage() {
3430        let source = include_str!("runtime_bridge.rs");
3431        assert!(
3432            source.contains("NODE_REALM_BY_WEBVIEW"),
3433            "REQ-SEC-002 REGRESSION: must have NODE_REALM_BY_WEBVIEW per-page storage"
3434        );
3435        assert!(
3436            source.contains("store_node_realm"),
3437            "REQ-SEC-002 REGRESSION: must have store_node_realm accessor"
3438        );
3439        assert!(
3440            source.contains("get_node_realm_by_id"),
3441            "REQ-SEC-002 REGRESSION: must have get_node_realm_by_id accessor (WebViewId-keyed)"
3442        );
3443        assert!(
3444            source.contains("get_node_realm_global"),
3445            "REQ-SEC-002 REGRESSION: must have get_node_realm_global accessor"
3446        );
3447        // BCE-20260621-001: enforce NO cross-thread *mut JSObject globals.
3448        // Use compile-time symbol references (positive) + word-boundary source
3449        // scan for stale globals (the source-grep must avoid matching its own
3450        // assertion text, so we use the `static NAME:` declaration prefix
3451        // combined with line-start anchoring via split).
3452        for line in source.lines() {
3453            let trimmed = line.trim_start();
3454            // Only flag actual top-level/static declarations of the BUG globals.
3455            // Comments and prose mentions are excluded by requiring leading `static`.
3456            assert!(
3457                !(trimmed.starts_with("static NODE_REALMS:")
3458                    && !trimmed.starts_with("static NODE_REALM_BY_WEBVIEW")),
3459                "BCE-20260621-001 REGRESSION: cross-thread *mut JSObject-keyed NODE_REALMS must not exist"
3460            );
3461            assert!(
3462                !(trimmed.starts_with("static PAGE_GLOBALS:")
3463                    && !trimmed.starts_with("static PAGE_GLOBAL_BY_WEBVIEW")),
3464                "BCE-20260621-001 REGRESSION: cross-thread *mut JSObject-keyed PAGE_GLOBALS must not exist"
3465            );
3466            assert!(
3467                !trimmed.starts_with("static LAST_PAGE_GLOBAL:"),
3468                "BCE-20260621-001 REGRESSION: process-wide LAST_PAGE_GLOBAL must not exist"
3469            );
3470        }
3471    }
3472
3473    /// Verify inject_node_apis_with_stealth uses drain_callbacks (not evaluate_js).
3474    /// REQ-SEC-002: Internal drain must NOT trigger Node API injection (avoid recursion).
3475    /// drain_callbacks handles InternalError from pending pipeline gracefully.
3476    #[test]
3477    fn runtime_bridge_drain_uses_callbacks_method() {
3478        let source = include_str!("runtime_bridge.rs");
3479
3480        let func_start = source
3481            .find("pub fn inject_node_apis_with_stealth")
3482            .expect("inject_node_apis_with_stealth function not found");
3483        let func_end = source[func_start..]
3484            .find("fn register_native_host_functions")
3485            .expect("end boundary not found");
3486        let func_body = &source[func_start..func_start + func_end];
3487
3488        assert!(
3489            func_body.contains("drain_callbacks"),
3490            "REQ-SEC-002 REGRESSION: inject_node_apis_with_stealth must use drain_callbacks (not evaluate_js)"
3491        );
3492        assert!(
3493            !func_body.contains("page.evaluate_js(\""),
3494            "REQ-SEC-002 REGRESSION: inject_node_apis_with_stealth must NOT call evaluate_js with string arg (would cause recursion)"
3495        );
3496        assert!(
3497            !func_body.contains("let _"),
3498            "REQ-SEC-003 REGRESSION: inject_node_apis_with_stealth must NOT swallow errors with let _"
3499        );
3500    }
3501
3502    /// Verify NODE_POLYFILLS contains Node API names (for fallback mode).
3503    #[test]
3504    fn node_polyfills_contains_security_sensitive_names() {
3505        let poly = super::NODE_POLYFILLS;
3506        assert!(
3507            poly.contains("require"),
3508            "NODE_POLYFILLS must contain 'require'"
3509        );
3510        assert!(
3511            poly.contains("Buffer"),
3512            "NODE_POLYFILLS must contain 'Buffer'"
3513        );
3514        assert!(
3515            poly.contains("process"),
3516            "NODE_POLYFILLS must contain 'process'"
3517        );
3518    }
3519
3520    // ── TEST-SEC-003: Node API Sandbox Isolation ────────────────────────
3521
3522    /// Verify WEB_POLYFILLS exists and does NOT contain Node.js API names.
3523    /// REQ-SEC-003: Page Realm fallback polyfills must NOT include Node APIs.
3524    #[test]
3525    fn web_polyfills_excludes_node_apis() {
3526        let poly = super::WEB_POLYFILLS;
3527        assert!(
3528            !poly.contains("require"),
3529            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'require'"
3530        );
3531        assert!(
3532            !poly.contains("Buffer"),
3533            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'Buffer'"
3534        );
3535        assert!(
3536            !poly.contains("process"),
3537            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'process'"
3538        );
3539        assert!(
3540            !poly.contains("Bun"),
3541            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'Bun'"
3542        );
3543        assert!(
3544            !poly.contains("module"),
3545            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'module'"
3546        );
3547        assert!(
3548            !poly.contains("__dirname"),
3549            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain '__dirname'"
3550        );
3551        assert!(
3552            !poly.contains("__filename"),
3553            "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain '__filename'"
3554        );
3555    }
3556
3557    /// Verify WEB_POLYFILLS includes essential Web APIs.
3558    /// REQ-SEC-003 criterion 6-8: console/fetch/URL/URLSearchParams must work.
3559    #[test]
3560    fn web_polyfills_includes_web_apis() {
3561        let poly = super::WEB_POLYFILLS;
3562        assert!(
3563            poly.contains("TextEncoder"),
3564            "WEB_POLYFILLS must contain TextEncoder"
3565        );
3566        assert!(
3567            poly.contains("TextDecoder"),
3568            "WEB_POLYFILLS must contain TextDecoder"
3569        );
3570        assert!(poly.contains("URL"), "WEB_POLYFILLS must contain URL");
3571        assert!(
3572            poly.contains("URLSearchParams"),
3573            "WEB_POLYFILLS must contain URLSearchParams"
3574        );
3575        assert!(poly.contains("btoa"), "WEB_POLYFILLS must contain btoa");
3576        assert!(poly.contains("atob"), "WEB_POLYFILLS must contain atob");
3577    }
3578
3579    /// Verify fallback path uses WEB_POLYFILLS (not NODE_POLYFILLS).
3580    /// REQ-SEC-003: inject_node_apis_with_stealth fallback must not inject Node APIs.
3581    #[test]
3582    fn fallback_uses_web_polyfills_not_node_polyfills() {
3583        let source = include_str!("runtime_bridge.rs");
3584
3585        let func_start = source
3586            .find("pub fn inject_node_apis_with_stealth")
3587            .expect("inject_node_apis_with_stealth function not found");
3588        let func_end = source[func_start..]
3589            .find("fn register_native_host_functions")
3590            .expect("end boundary not found");
3591        let func_body = &source[func_start..func_start + func_end];
3592
3593        assert!(
3594            func_body.contains("WEB_POLYFILLS"),
3595            "REQ-SEC-003 REGRESSION: fallback must use WEB_POLYFILLS (not NODE_POLYFILLS)"
3596        );
3597        // The fallback path should NOT reference NODE_POLYFILLS
3598        let fallback_section = func_body
3599            .find("if !registered")
3600            .map(|i| &func_body[i..])
3601            .unwrap_or("");
3602        assert!(
3603            !fallback_section.contains("NODE_POLYFILLS"),
3604            "REQ-SEC-003 REGRESSION: fallback path must NOT reference NODE_POLYFILLS"
3605        );
3606    }
3607
3608    /// Verify install_all_native does NOT call install_node_apis or install_all.
3609    /// REQ-SEC-003 criterion 1+10: Page Realm must only get Web APIs.
3610    #[test]
3611    fn install_all_native_web_apis_only() {
3612        let source = include_str!("runtime_bridge.rs");
3613
3614        let func_start = source
3615            .find("unsafe fn install_all_native")
3616            .expect("install_all_native function not found");
3617        let func_body_start = source[func_start..]
3618            .find("{")
3619            .expect("function body start not found");
3620        let search_limit = source[func_start + func_body_start..]
3621            .find("const NODE_POLYFILLS")
3622            .or_else(|| {
3623                source[func_start + func_body_start..].find("/// Inject Node.js APIs as native")
3624            })
3625            .unwrap_or(5000)
3626            .min(5000);
3627        let func_body =
3628            &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3629
3630        assert!(
3631            func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3632            "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
3633        );
3634        assert!(
3635            func_body.contains("bun_runtime::timers::install_timer_globals"),
3636            "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
3637        );
3638        assert!(
3639            !func_body.contains("globals::install_all("),
3640            "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
3641        );
3642        assert!(
3643            !func_body.contains("globals::install_node_apis("),
3644            "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis()"
3645        );
3646    }
3647
3648    /// Verify Node APIs are installed in Node Realm (create_node_realm_native).
3649    /// REQ-SEC-003 criterion 9: Node APIs must exist ONLY in Node Realm.
3650    #[test]
3651    fn node_apis_installed_in_node_realm_only() {
3652        let source = include_str!("runtime_bridge.rs");
3653
3654        let func_start = source
3655            .find("unsafe fn create_node_realm_native")
3656            .expect("create_node_realm_native function not found");
3657        let func_body_start = source[func_start..]
3658            .find("{")
3659            .expect("function body start not found");
3660        let search_limit = source[func_start + func_body_start..]
3661            .find("unsafe fn wrap_and_install_dom_proxy")
3662            .or_else(|| source[func_start + func_body_start..].find("/// Wrap a DOM property"))
3663            .unwrap_or(3000)
3664            .min(3000);
3665        let func_body =
3666            &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3667
3668        assert!(
3669            func_body.contains("bun_runtime::globals::install_node_apis"),
3670            "REQ-SEC-003 REGRESSION: Node Realm must install Node APIs (install_node_apis)"
3671        );
3672        assert!(
3673            func_body.contains("NewCompartmentAndZone"),
3674            "REQ-SEC-003 REGRESSION: Node Realm must be isolated via NewCompartmentAndZone"
3675        );
3676    }
3677
3678    /// Verify WEB_POLYFILLS is valid JS (self-executing function).
3679    #[test]
3680    fn web_polyfills_is_valid_js() {
3681        let poly = super::WEB_POLYFILLS;
3682        assert!(
3683            poly.starts_with("(function()"),
3684            "WEB_POLYFILLS must be an IIFE"
3685        );
3686        assert!(poly.ends_with("})();"), "WEB_POLYFILLS must close IIFE");
3687    }
3688
3689    /// REQ-SEC-002: remove_node_realm_by_id is pub and is a no-op for unknown WebViewId.
3690    /// BCE-20260621-001: by-WebViewId API; null/raw-pointer API removed.
3691    #[test]
3692    fn remove_node_realm_by_id_is_safe_no_op() {
3693        // Synthesize a WebViewId via servo's mock helper. We do not exercise
3694        // real servo script-thread routing here — we only assert the API does
3695        // not panic when called with an unknown WebViewId.
3696        // Using a sentinel-style test: call remove on a freshly-cleared map.
3697        let _guard = super::test_serial_lock().lock().unwrap();
3698        super::clear_all_node_realms();
3699        // Constructing a WebViewId requires PainterId. servo exposes
3700        // WebViewId::new(PainterId::next()) but PainterId is not re-exported
3701        // from the `servo` crate root. We rely on the fact that remove is
3702        // a no-op for unknown keys — we simply assert the function exists
3703        // and is callable. Compile-time check.
3704        let _f: fn(servo::WebViewId) = super::remove_node_realm_by_id;
3705    }
3706
3707    /// REQ-SEC-002: WebViewId-keyed storage API exists and is structurally sound.
3708    /// BCE-20260621-001: all accessor signatures are WebViewId-based.
3709    #[test]
3710    fn webview_id_keyed_storage_api_exists() {
3711        // Compile-time check that the WebViewId-keyed API exists.
3712        let _store: fn(servo::WebViewId, *mut mozjs::jsapi::JSObject, *mut mozjs::jsapi::JSObject) =
3713            super::store_node_realm;
3714        let _get_node: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3715            super::get_node_realm_by_id;
3716        let _get_page: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3717            super::get_page_global_by_id;
3718        let _get_node_global: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3719            super::get_node_realm_global;
3720        let _get_page_global: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3721            super::get_page_global;
3722        let _remove: fn(servo::WebViewId) = super::remove_node_realm_by_id;
3723    }
3724
3725    /// REQ-SEC-002: clear_all_node_realms still works (empties both maps).
3726    /// BCE-20260621-001: clears WebViewId-keyed maps; no raw-pointer cleanup needed.
3727    #[test]
3728    fn clear_all_removes_all_entries() {
3729        let _guard = super::test_serial_lock().lock().unwrap();
3730        // Clear twice — must be idempotent.
3731        super::clear_all_node_realms();
3732        super::clear_all_node_realms();
3733    }
3734
3735    /// REQ-SEC-002: lazy getter functions exist and have correct ABI.
3736    #[test]
3737    fn lazy_dom_getters_are_valid_jsnative() {
3738        // Verify the functions can be cast to JSNative (Option<extern "C" fn>).
3739        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_window);
3740        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_document);
3741        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_navigator);
3742        // @trace REQ-BRW-004: Worker/SharedWorker/ServiceWorker lazy getters
3743        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_worker);
3744        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_shared_worker);
3745        let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_service_worker);
3746    }
3747
3748    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] DF-WK-11
3749    /// Structural assertion: Worker/SharedWorker/ServiceWorker constructor lazy
3750    /// getters use JSPROP_PERMANENT (non-configurable), matching Web IDL semantics.
3751    /// Ordinary DOM object getters (window/document/navigator) use JSPROP_READONLY
3752    /// without JSPROP_PERMANENT.
3753    #[test]
3754    fn worker_constructor_getters_use_permanent_attribute() {
3755        let source = include_str!("runtime_bridge.rs");
3756        // Constructor getters must include JSPROP_PERMANENT
3757        assert!(
3758            source.contains("ctor_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY | mozjs::jsapi::JSPROP_PERMANENT)"),
3759            "REQ-BRW-004 REGRESSION: Worker/SharedWorker/ServiceWorker constructors must use JSPROP_PERMANENT"
3760        );
3761        // Object getters must NOT include JSPROP_PERMANENT
3762        assert!(
3763            source.contains("obj_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY)"),
3764            "REQ-BRW-004 REGRESSION: window/document/navigator getters should use obj_attrs without JSPROP_PERMANENT"
3765        );
3766        // Verify Worker/SharedWorker/ServiceWorker are in ctor_getters
3767        assert!(
3768            source.contains("(c\"Worker\", Some(lazy_dom_getter_worker))"),
3769            "REQ-BRW-004 REGRESSION: Worker must be in ctor_getters"
3770        );
3771        assert!(
3772            source.contains("(c\"SharedWorker\", Some(lazy_dom_getter_shared_worker))"),
3773            "REQ-BRW-004 REGRESSION: SharedWorker must be in ctor_getters"
3774        );
3775        assert!(
3776            source.contains("(c\"ServiceWorker\", Some(lazy_dom_getter_service_worker))"),
3777            "REQ-BRW-004 REGRESSION: ServiceWorker must be in ctor_getters"
3778        );
3779    }
3780
3781    // ── DF-WK-11: Cross-Compartment Worker Constructor Proxy Behavioral Tests ──
3782    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3783
3784    /// DF-WK-11: lazy_constructor_getter_impl reads from Page Realm (not Node Realm).
3785    ///
3786    /// Behavioral assertion: the getter fetches the constructor from the
3787    /// PER_THREAD_PAGE_GLOBAL (Page Realm's Window global), NOT from
3788    /// CurrentGlobalOrNull (which would be the Node Realm global in the
3789    /// getter's execution context). This is the core of cross-Compartment
3790    /// proxying — the constructor physically lives in Page Realm's Compartment,
3791    /// and JS_WrapObject creates the proxy for Node Realm access.
3792    ///
3793    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3794    #[test]
3795    fn constructor_getter_reads_from_page_realm_not_node_realm() {
3796        let source = include_str!("runtime_bridge.rs");
3797        let func_start = source
3798            .find("unsafe fn lazy_constructor_getter_impl")
3799            .expect("lazy_constructor_getter_impl not found");
3800        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3801
3802        // Must read page_global from PER_THREAD_PAGE_GLOBAL
3803        assert!(
3804            func_body.contains("PER_THREAD_PAGE_GLOBAL.with"),
3805            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must read Page Realm global from PER_THREAD_PAGE_GLOBAL"
3806        );
3807        // Must get the property from page_global_root (Page Realm), not node_global
3808        assert!(
3809            func_body.contains("page_global_root.handle()"),
3810            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must JS_GetProperty from page_global_root (Page Realm)"
3811        );
3812    }
3813
3814    /// DF-WK-11: lazy_constructor_getter_impl uses JS_WrapObject for cross-Compartment proxy.
3815    ///
3816    /// Behavioral assertion: the getter wraps the fetched Page Realm constructor
3817    /// with JS_WrapObject, creating a cross-Compartment proxy. This is what
3818    /// enables Node Realm scripts to call `new Worker(url)` — SpiderMonkey
3819    /// transparently enters Page Realm's Compartment when [[Construct]] is
3820    /// invoked on the proxy.
3821    ///
3822    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3823    #[test]
3824    fn constructor_getter_uses_js_wrap_object_for_cross_compartment_proxy() {
3825        let source = include_str!("runtime_bridge.rs");
3826        let func_start = source
3827            .find("unsafe fn lazy_constructor_getter_impl")
3828            .expect("lazy_constructor_getter_impl not found");
3829        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3830
3831        assert!(
3832            func_body.contains("JS_WrapObject"),
3833            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call JS_WrapObject for cross-Compartment proxy"
3834        );
3835    }
3836
3837    /// DF-WK-11: lazy_constructor_getter_impl validates IsConstructor before returning.
3838    ///
3839    /// Behavioral assertion: the getter checks IsConstructor on the fetched
3840    /// object before wrapping it. If the Page Realm property is not a constructor
3841    /// (e.g., it's a plain object or undefined), a ReferenceError is thrown
3842    /// instead of returning an unusable value. This prevents cryptic
3843    /// "X is not a constructor" TypeErrors at `new Worker()` call sites.
3844    ///
3845    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3846    #[test]
3847    fn constructor_getter_validates_is_constructor_before_wrapping() {
3848        let source = include_str!("runtime_bridge.rs");
3849        let func_start = source
3850            .find("unsafe fn lazy_constructor_getter_impl")
3851            .expect("lazy_constructor_getter_impl not found");
3852        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3853
3854        assert!(
3855            func_body.contains("mozjs::jsapi::IsConstructor"),
3856            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call IsConstructor to validate the constructor"
3857        );
3858    }
3859
3860    /// DF-WK-11: constructor getter throws ReferenceError (not TypeError) on failure.
3861    ///
3862    /// Behavioral assertion: when the constructor is unavailable (no page loaded,
3863    /// property not an object, or not a constructor), the getter throws a
3864    /// ReferenceError via report_reference_error. This matches Web IDL semantics
3865    /// where accessing an unsupported interface constructor should produce
3866    /// ReferenceError, not a confusing TypeError at the call site.
3867    ///
3868    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3869    #[test]
3870    fn constructor_getter_throws_reference_error_on_failure() {
3871        let source = include_str!("runtime_bridge.rs");
3872        let func_start = source
3873            .find("unsafe fn lazy_constructor_getter_impl")
3874            .expect("lazy_constructor_getter_impl not found");
3875        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3876
3877        // Must call report_reference_error for each failure case
3878        assert!(
3879            func_body.contains("report_reference_error"),
3880            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call report_reference_error for error reporting"
3881        );
3882        // Error messages must contain the property name for diagnosability
3883        assert!(
3884            func_body.contains("property_name"),
3885            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl error messages must reference property_name"
3886        );
3887    }
3888
3889    /// DF-WK-11: null page_global triggers ReferenceError (not silent undefined).
3890    ///
3891    /// Behavioral assertion: when PER_THREAD_PAGE_GLOBAL returns null (no page
3892    /// loaded yet), the getter throws a ReferenceError instead of silently
3893    /// returning undefined. This is critical for Node Realm scripts that use
3894    /// `new Worker()` — they need a clear diagnostic that the browser context
3895    /// isn't ready, not a mysterious "Worker is not a constructor" TypeError.
3896    ///
3897    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3898    #[test]
3899    fn constructor_getter_throws_on_null_page_global() {
3900        let source = include_str!("runtime_bridge.rs");
3901        let func_start = source
3902            .find("unsafe fn lazy_constructor_getter_impl")
3903            .expect("lazy_constructor_getter_impl not found");
3904        let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3905
3906        // Must check for null page_global and throw
3907        assert!(
3908            func_body.contains("page_global.is_null()"),
3909            "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must check page_global.is_null()"
3910        );
3911        // Must throw ReferenceError (return false) on null page_global
3912        // Search specifically for "page_global.is_null()" context — there's also
3913        // node_global.is_null() earlier in the function which returns true silently.
3914        let page_global_null_pos = func_body
3915            .find("page_global.is_null()")
3916            .expect("DF-WK-11: page_global.is_null() check not found");
3917        let after_null_check = &func_body[page_global_null_pos..page_global_null_pos + 300];
3918        assert!(
3919            after_null_check.contains("report_reference_error"),
3920            "DF-WK-11 REGRESSION: null page_global must trigger report_reference_error, not silent return"
3921        );
3922    }
3923
3924    /// DF-WK-11: install_lazy_dom_getters registers constructors with PERMANENT attribute.
3925    ///
3926    /// Behavioral assertion: the ctor_getters are registered with
3927    /// JSPROP_PERMANENT (non-deletable), matching Web IDL semantics where
3928    /// interface constructors on the global must not be configurable.
3929    /// This prevents page JS (or Node Realm scripts) from accidentally
3930    /// deleting Worker/SharedWorker/ServiceWorker from the global.
3931    ///
3932    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3933    #[test]
3934    fn install_lazy_dom_getters_uses_js_define_property1_for_constructors() {
3935        // Structural DF-WK-11 regression guard. Search the whole file (not a
3936        // fixed byte window) so the assertions survive rustfmt reflowing the
3937        // function's layout — the ctor_getters loop can land past 2000 bytes.
3938        let source = include_str!("runtime_bridge.rs");
3939        assert!(
3940            source.find("unsafe fn install_lazy_dom_getters").is_some(),
3941            "install_lazy_dom_getters not found"
3942        );
3943
3944        // Must use JS_DefineProperty1 (not JS_SetProperty) for initial registration
3945        assert!(
3946            source.contains("JS_DefineProperty1"),
3947            "DF-WK-11 REGRESSION: install_lazy_dom_getters must use JS_DefineProperty1 for property registration"
3948        );
3949        // Must iterate ctor_getters separately from obj_getters
3950        assert!(
3951            source.contains("for &(name, getter) in ctor_getters"),
3952            "DF-WK-11 REGRESSION: ctor_getters must be registered with their own (PERMANENT) attributes"
3953        );
3954    }
3955
3956    /// DF-WK-11: constructor getters are called from Node Realm (not Page Realm).
3957    ///
3958    /// Structural assertion: install_lazy_dom_getters is called inside
3959    /// create_node_realm_native, which creates the Node Realm's global.
3960    /// The lazy getters are installed on the Node Realm global, meaning
3961    /// they execute in the Node Realm context (CurrentGlobalOrNull returns
3962    /// the Node Realm global). The getter then reads from Page Realm via
3963    /// PER_THREAD_PAGE_GLOBAL and wraps via JS_WrapObject.
3964    ///
3965    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3966    #[test]
3967    fn lazy_dom_getters_installed_on_node_realm_not_page_realm() {
3968        let source = include_str!("runtime_bridge.rs");
3969        let func_start = source
3970            .find("unsafe fn create_node_realm_native")
3971            .expect("create_node_realm_native not found");
3972        // Search the function body for install_lazy_dom_getters call
3973        let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
3974
3975        assert!(
3976            func_body.contains("install_lazy_dom_getters(realm_cx, global.handle())"),
3977            "DF-WK-11 REGRESSION: install_lazy_dom_getters must be called from create_node_realm_native on the Node Realm global"
3978        );
3979        // The 'global' in that call is the Node Realm global (created via JS_NewGlobalObject
3980        // with NewCompartmentAndZone), NOT the servo Window global
3981        assert!(
3982            func_body.contains("JS_NewGlobalObject") && func_body.contains("NewCompartmentAndZone"),
3983            "DF-WK-11 REGRESSION: create_node_realm_native must create Node Realm with NewCompartmentAndZone before installing lazy getters"
3984        );
3985    }
3986
3987    /// DF-WK-11: report_reference_error produces JSEXN_REFERENCEERR.
3988    ///
3989    /// Behavioral assertion: the error reporting function uses
3990    /// JSEXN_REFERENCEERR (not JSEXN_TYPEERR or generic error) when
3991    /// constructor access fails. This matches the Web IDL convention
3992    /// where accessing an undefined interface produces ReferenceError.
3993    ///
3994    /// @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-11]
3995    #[test]
3996    fn report_reference_error_uses_reference_err_type() {
3997        let source = include_str!("runtime_bridge.rs");
3998        let func_start = source
3999            .find("unsafe fn report_reference_error")
4000            .expect("report_reference_error not found");
4001        let func_body = &source[func_start..func_start + 2000.min(source.len() - func_start)];
4002
4003        assert!(
4004            func_body.contains("JSEXN_REFERENCEERR"),
4005            "DF-WK-11 REGRESSION: report_reference_error must use JSEXN_REFERENCEERR error type"
4006        );
4007        assert!(
4008            func_body.contains("JS_ReportErrorNumberUTF8"),
4009            "DF-WK-11 REGRESSION: report_reference_error must use JS_ReportErrorNumberUTF8 for proper error reporting"
4010        );
4011    }
4012
4013    // ── DashMap + OnceLock refactoring tests ──────────────────────────────
4014    // @trace REQ-PURE-002 [req:REQ-PURE-002] [level:unit]
4015
4016    /// BCE-20260621-001: storage is WebViewId-keyed DashMap (not raw-pointer).
4017    /// Structural assertion: source contains the WebViewId-keyed static.
4018    #[test]
4019    fn storage_is_webview_id_keyed_dashmap() {
4020        let source = include_str!("runtime_bridge.rs");
4021        assert!(
4022            source.contains(
4023                "static NODE_REALM_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>>"
4024            ),
4025            "BCE-20260621-001 REGRESSION: NODE_REALM_BY_WEBVIEW must be WebViewId-keyed"
4026        );
4027        assert!(
4028            source.contains(
4029                "static PAGE_GLOBAL_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>>"
4030            ),
4031            "BCE-20260621-001 REGRESSION: PAGE_GLOBAL_BY_WEBVIEW must be WebViewId-keyed"
4032        );
4033        assert!(
4034            source.contains("thread_local! {\n    static PER_THREAD_PAGE_GLOBAL"),
4035            "BCE-20260621-001 REGRESSION: PER_THREAD_PAGE_GLOBAL thread_local must exist for lazy getters"
4036        );
4037    }
4038
4039    /// BCE-20260621-001: no process-wide cross-thread *mut JSObject storage remains.
4040    /// Structural sweep — confirms the BUG pattern signature has zero residual.
4041    /// Uses line-start scan to avoid matching assertion strings inside tests.
4042    #[test]
4043    fn no_cross_thread_raw_jsobject_storage_residual() {
4044        let source = include_str!("runtime_bridge.rs");
4045        for line in source.lines() {
4046            let trimmed = line.trim_start();
4047            assert!(
4048                !(trimmed.starts_with("static NODE_REALMS:")
4049                    && !trimmed.starts_with("static NODE_REALM_BY_WEBVIEW")),
4050                "BCE-20260621-001 RESIDUAL: NODE_REALMS usize-keyed DashMap still present"
4051            );
4052            assert!(
4053                !(trimmed.starts_with("static PAGE_GLOBALS:")
4054                    && !trimmed.starts_with("static PAGE_GLOBAL_BY_WEBVIEW")),
4055                "BCE-20260621-001 RESIDUAL: PAGE_GLOBALS usize-keyed DashMap still present"
4056            );
4057            assert!(
4058                !trimmed.starts_with("static LAST_PAGE_GLOBAL:"),
4059                "BCE-20260621-001 RESIDUAL: LAST_PAGE_GLOBAL AtomicUsize still present"
4060            );
4061            assert!(
4062                !trimmed.starts_with("fn get_last_page_global"),
4063                "BCE-20260621-001 RESIDUAL: get_last_page_global accessor still present"
4064            );
4065            assert!(
4066                !trimmed.starts_with("fn set_last_page_global"),
4067                "BCE-20260621-001 RESIDUAL: set_last_page_global accessor still present"
4068            );
4069            assert!(
4070                !trimmed.starts_with("pub fn get_last_page_global"),
4071                "BCE-20260621-001 RESIDUAL: pub get_last_page_global accessor still present"
4072            );
4073        }
4074    }
4075
4076    /// OnceLock EvaluateResult: set + get works.
4077    #[test]
4078    fn oncelock_evaluate_result_set_and_get() {
4079        use std::sync::Arc;
4080        let lock: Arc<OnceLock<super::EvaluateResult>> = Arc::new(OnceLock::new());
4081        assert!(lock.get().is_none(), "OnceLock should be unset initially");
4082
4083        let result = super::EvaluateResult::ok("42".into());
4084        assert!(lock.set(result).is_ok(), "First set should succeed");
4085
4086        let got = lock.get().unwrap();
4087        assert_eq!(got.value, Some("42".into()));
4088        assert!(got.error.is_none());
4089    }
4090
4091    /// OnceLock EvaluateResult: second set() fails gracefully (returns Err).
4092    #[test]
4093    fn oncelock_evaluate_result_second_set_fails() {
4094        use std::sync::Arc;
4095        let lock: Arc<OnceLock<super::EvaluateResult>> = Arc::new(OnceLock::new());
4096
4097        let first = super::EvaluateResult::ok("first".into());
4098        assert!(lock.set(first).is_ok());
4099
4100        let second = super::EvaluateResult::err("second".into());
4101        let set_result = lock.set(second);
4102        assert!(set_result.is_err(), "Second set should return Err");
4103        // Original value is preserved
4104        assert_eq!(lock.get().unwrap().value, Some("first".into()));
4105    }
4106}