Skip to main content

bao_browser/
delegate.rs

1// @trace REQ-BRW-001 [entity:BrowserContext]  REQ-CDP-006: Servo delegate hooks for CDP event forwarding
2// @trace REQ-BRW-004 [entity:Worker] [entity:DedicatedWorkerGlobalScope] Worker lifecycle + DedicatedWorkerGlobalScope API
3// @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] SharedWorker cross-page routing + connect event
4// @trace REQ-BRW-004 [entity:ServiceWorker] [entity:ServiceWorkerGlobalScope] ServiceWorker registration + fetch interception + stealth/CDP boundary consistency
5// @trace REQ-CDP-006 [entity:ServoDelegateHooks] (servo delegate → CDP event forwarding)
6use std::cell::RefCell;
7use std::collections::HashMap;
8use std::rc::Rc;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::mpsc::{Receiver, Sender};
11use std::sync::Arc;
12
13use dpi::PhysicalSize;
14use servo::{
15    AllowOrDenyRequest, ConsoleLogLevel, CreateNewWebViewRequest, DeviceIntPoint, DeviceIntRect,
16    DeviceIntSize, EmbedderControl, EmbedderControlId, LoadStatus, NavigationRequest,
17    PermissionRequest, ScreenGeometry, ServoDelegate, ServoError, WebView, WebViewDelegate,
18};
19
20use bao_cdp::{BaoEvent, ConsoleMessage};
21use bao_cdp_client::bridge::{ConsoleLevel, ServoEvent};
22
23// ─── Worker Message Channel (REQ-BRW-004) ──────────────────────────
24// @trace REQ-BRW-004 [entity:Worker] [entity:DedicatedWorkerGlobalScope] [criterion:1..18]
25// DF-WK-4 / DF-WK-5: page↔worker bidirectional structured-clone channel.
26//
27// Servo already handles the full Worker lifecycle internally (DOM bindings,
28// structured clone via `structuredclone::write/read`, crossbeam channel
29// transport). Bao's responsibility is:
30//   1. Track per-webview active Worker count for page-unload auto-terminate
31//      (SPEC criterion #10: GlobalScope::track_worker + AutoCloseWorker).
32//   2. Forward Worker message events to CDP via the existing event_tx path.
33//   3. Provide a `WorkerHandle` that bao_browser consumers can use to
34//      observe worker state (closing flag) without holding JSObject refs.
35//
36// Thread safety: WorkerHandle only holds Arc<AtomicBool> (closing) and
37// Arc<AtomicBool> (terminated) — no JSObject, no raw pointer. These are
38// Send + Sync safe. The actual Worker DOM object lives in servo's
39// ScriptThread; we never touch it from bao_browser.
40
41/// Unique identifier for a Worker within a page's scope.
42/// @trace REQ-BRW-004 [entity:Worker]
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct WorkerId(pub String);
45
46/// A Send+Sync handle to a servo Worker's lifecycle state.
47///
48/// Does NOT hold JSObject references — only atomic flags and the global
49/// address (for REALM_PROFILES cleanup on teardown).
50/// This is safe to store across threads (unlike Worker DOM objects).
51///
52/// @trace REQ-BRW-004 [entity:Worker]
53#[derive(Debug, Clone)]
54pub struct WorkerHandle {
55    /// Worker script URL.
56    pub script_url: String,
57    /// Mirrors servo Worker::closing — set by terminate() or self.close().
58    pub closing: Arc<AtomicBool>,
59    /// Mirrors servo Worker::terminated — true after full teardown.
60    pub terminated: Arc<AtomicBool>,
61    /// Worker global object address (set after scope_init runs on worker thread).
62    /// Used for REALM_PROFILES unregister on teardown (SPEC criterion #18).
63    /// Zero means not yet set / unknown.
64    /// @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
65    worker_global_addr: Arc<AtomicU64>,
66}
67
68impl WorkerHandle {
69    /// Create a new WorkerHandle in the running state.
70    ///
71    /// @trace REQ-BRW-004 [entity:Worker]
72    pub fn new(script_url: String) -> Self {
73        WorkerHandle {
74            script_url,
75            closing: Arc::new(AtomicBool::new(false)),
76            terminated: Arc::new(AtomicBool::new(false)),
77            worker_global_addr: Arc::new(AtomicU64::new(0)),
78        }
79    }
80
81    /// Returns true if terminate()/self.close() has been requested.
82    ///
83    /// @trace REQ-BRW-004 [entity:Worker]
84    pub fn is_closing(&self) -> bool {
85        self.closing.load(Ordering::Acquire)
86    }
87
88    /// Returns true if the Worker thread has fully exited.
89    ///
90    /// @trace REQ-BRW-004 [entity:Worker]
91    pub fn is_terminated(&self) -> bool {
92        self.terminated.load(Ordering::Acquire)
93    }
94
95    /// Signal the Worker to terminate (mirrors Worker::terminate()).
96    /// Idempotent — calling multiple times is safe.
97    ///
98    /// @trace REQ-BRW-004 [entity:Worker]
99    pub fn terminate(&self) {
100        self.closing.store(true, Ordering::Release);
101    }
102
103    /// Mark the Worker as fully terminated (called after thread join).
104    ///
105    /// @trace REQ-BRW-004 [entity:Worker]
106    pub fn mark_terminated(&self) {
107        self.terminated.store(true, Ordering::Release);
108    }
109
110    /// Set the Worker's global object address for REALM_PROFILES tracking.
111    ///
112    /// Called from the worker thread's scope_init callback after the global
113    /// object is created. The address is used on teardown to unregister the
114    /// stealth profile from REALM_PROFILES (SPEC criterion #18).
115    ///
116    /// @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
117    pub fn set_worker_global_addr(&self, addr: usize) {
118        self.worker_global_addr
119            .store(addr as u64, Ordering::Release);
120    }
121
122    /// Get the Worker's global object address (0 if not yet set).
123    ///
124    /// @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
125    pub fn worker_global_addr(&self) -> usize {
126        self.worker_global_addr.load(Ordering::Acquire) as usize
127    }
128
129    /// Get a clone of the Arc<AtomicU64> backing the global address slot.
130    ///
131    /// This allows the scope_init callback on the worker thread to write the
132    /// global address into the same slot that the main thread's WorkerHandle
133    /// reads from — without any JSObject references crossing the thread
134    /// boundary (BCE-20260621-001: thread-local JSContext invariant).
135    ///
136    /// @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
137    pub fn worker_global_addr_arc(&self) -> Arc<AtomicU64> {
138        Arc::clone(&self.worker_global_addr)
139    }
140
141    /// Unregister the Worker's stealth profile from REALM_PROFILES.
142    ///
143    /// Called during crash-safe teardown (all three paths) to ensure the
144    /// profile entry for this Worker's global is removed, preventing stale
145    /// entries that could cause UAF or fingerprint leakage.
146    ///
147    /// SPEC criterion #18: "REALM_PROFILES 条目注销"
148    ///
149    /// @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
150    pub fn unregister_stealth_profile(&self) {
151        let addr = self.worker_global_addr();
152        if addr != 0 {
153            bao_stealth::engine_props::remove_profile_for_global(addr);
154        }
155    }
156}
157
158/// Direction of a Worker postMessage event.
159///
160/// @trace REQ-BRW-004 [entity:Worker]
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum WorkerMessageDirection {
163    /// page → worker (DF-WK-4: worker.postMessage(msg))
164    PageToWorker,
165    /// worker → page (DF-WK-5: self.postMessage(msg))
166    WorkerToPage,
167}
168
169/// A Worker postMessage event observed by the bao layer.
170///
171/// Only the metadata is captured here — the actual structured-clone data
172/// is handled entirely within servo's DOM (structuredclone::write/read).
173/// This struct is for CDP observability and event forwarding.
174///
175/// @trace REQ-BRW-004 [entity:Worker]
176#[derive(Debug, Clone)]
177pub struct WorkerMessageEvent {
178    /// Which Worker this message is associated with.
179    pub worker_id: WorkerId,
180    /// Direction of the message.
181    pub direction: WorkerMessageDirection,
182}
183
184// ─── Worker Error Event (REQ-BRW-004 criterion #9) ────────────────
185// @trace REQ-BRW-004 [entity:Worker] [criterion:9]
186// SPEC criterion #9: "onerror 事件正确传播到主线程
187// (ErrorEvent 包含 message/filename/lineno/colno)".
188//
189// When a Worker throws an uncaught error, servo dispatches an ErrorEvent
190// on the Worker object in the main thread. Bao captures the error metadata
191// here for CDP observability (Runtime.exceptionThrown) and for forwarding
192// to any consumer that observes Worker errors.
193
194/// A Worker error event observed by the bao layer.
195///
196/// Mirrors the DOM ErrorEvent fields (message/filename/lineno/colno).
197/// Servo handles the actual DOM ErrorEvent dispatch internally;
198/// this struct captures the metadata for CDP forwarding.
199///
200/// @trace REQ-BRW-004 [entity:Worker] [criterion:9]
201#[derive(Debug, Clone)]
202pub struct WorkerErrorEvent {
203    /// Which Worker this error is associated with.
204    pub worker_id: WorkerId,
205    /// Error message.
206    pub message: String,
207    /// Script filename where the error occurred.
208    pub filename: String,
209    /// Line number (1-based).
210    pub lineno: u32,
211    /// Column number (1-based).
212    pub colno: u32,
213}
214
215// ─── Structured Clone Message Channel (REQ-BRW-004 criterion #6) ────
216// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
217// SPEC criterion #6: "Structured Clone 消息序列化支持
218// (对象/数组/Buffer/ArrayBuffer/Transferable)"
219//
220// DF-WK-4: page→worker postMessage: worker.postMessage(v) →
221//   structuredclone::write(cx,v) → WorkerMessage(StructuredSerializedData)
222//   → crossbeam send → worker recv → structuredclone::read → message event
223// DF-WK-5: worker→page onmessage: self.postMessage(v) →
224//   structuredclone::write → channel → parent ScriptThread drain →
225//   structuredclone::read → worker.onmessage
226//
227// Architecture: servo internally handles structured clone serialization
228// (structuredclone::write/read) and cross-thread message transport
229// (crossbeam channels). Bao's responsibility is:
230//   1. Provide a `WorkerChannelBridge` that bao_browser consumers can
231//      use to post messages to a Worker without touching JSObject refs.
232//   2. Provide a `WorkerInbox` for receiving worker→page messages with
233//      structured-clone payload data.
234//   3. Track per-worker channel endpoints in `BaoWebViewState` for
235//      lifecycle management and CDP observability.
236//
237// Thread safety: All channel data is serialized bytes (Vec<u8>) — no
238// JSObject crosses thread boundaries. This satisfies NFR-THREAD-SAFETY
239// and the JSContext thread-local model (BCE-20260621-001).
240
241/// Monotonically increasing message ID counter for CDP trace correlation.
242/// @trace REQ-BRW-004 [entity:Worker] [criterion:6]
243static NEXT_MESSAGE_ID: AtomicU64 = AtomicU64::new(1);
244
245/// A structured-clone serialized payload for Worker postMessage.
246///
247/// Contains the serialized bytes produced by SpiderMonkey's
248/// `structuredclone::write`. The actual serialization/deserialization
249/// happens on the sender/receiver thread's JSContext.
250///
251/// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
252#[derive(Debug)]
253pub struct StructuredClonePayload {
254    /// Serialized bytes from structuredclone::write.
255    pub data: Vec<u8>,
256    /// Number of transferable objects in the payload (for CDP reporting).
257    pub transferable_count: u32,
258}
259
260impl Clone for StructuredClonePayload {
261    fn clone(&self) -> Self {
262        StructuredClonePayload {
263            data: self.data.clone(),
264            transferable_count: self.transferable_count,
265        }
266    }
267}
268
269/// A structured-clone message crossing the page↔worker boundary.
270///
271/// Carries both the serialized payload and metadata for CDP observability.
272/// Each message gets a unique ID for trace correlation.
273///
274/// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
275#[derive(Debug, Clone)]
276pub struct WorkerStructuredMessage {
277    /// Unique message ID for CDP trace correlation.
278    pub message_id: u64,
279    /// Which Worker this message is associated with.
280    pub worker_id: WorkerId,
281    /// Direction of the message.
282    pub direction: WorkerMessageDirection,
283    /// Structured-clone serialized payload (when available from servo).
284    /// None when only forwarding metadata (e.g., servo handles clone internally).
285    pub payload: Option<StructuredClonePayload>,
286}
287
288impl WorkerStructuredMessage {
289    /// Create a new structured message with a unique ID.
290    ///
291    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6]
292    pub fn new(
293        worker_id: WorkerId,
294        direction: WorkerMessageDirection,
295        payload: Option<StructuredClonePayload>,
296    ) -> Self {
297        WorkerStructuredMessage {
298            message_id: NEXT_MESSAGE_ID.fetch_add(1, Ordering::Relaxed),
299            worker_id,
300            direction,
301            payload,
302        }
303    }
304
305    /// Create a metadata-only message (no structured-clone payload).
306    /// Used when servo handles the clone internally and bao only observes.
307    ///
308    /// @trace REQ-BRW-004 [entity:Worker] DF-WK-4 / DF-WK-5
309    pub fn metadata_only(worker_id: WorkerId, direction: WorkerMessageDirection) -> Self {
310        Self::new(worker_id, direction, None)
311    }
312
313    /// Create a message with serialized structured-clone data.
314    ///
315    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6]
316    pub fn with_payload(
317        worker_id: WorkerId,
318        direction: WorkerMessageDirection,
319        data: Vec<u8>,
320        transferable_count: u32,
321    ) -> Self {
322        Self::new(
323            worker_id,
324            direction,
325            Some(StructuredClonePayload {
326                data,
327                transferable_count,
328            }),
329        )
330    }
331}
332
333/// Bidirectional channel bridge for a single Worker's postMessage channel.
334///
335/// Holds the mpsc channel endpoints for page↔worker communication.
336/// The bridge does NOT hold JSObject references — only channel endpoints
337/// and serialized data. This is safe to store across threads.
338///
339/// SPEC DF-WK-4: page→worker (sender → receiver in worker thread)
340/// SPEC DF-WK-5: worker→page (sender in worker thread → receiver here)
341///
342/// @trace REQ-BRW-004 [entity:Worker] [entity:DedicatedWorkerGlobalScope]
343///   [criterion:6] DF-WK-4 / DF-WK-5
344pub struct WorkerChannelBridge {
345    /// Worker ID this bridge belongs to.
346    pub worker_id: WorkerId,
347    /// Sender for page→worker messages (DF-WK-4: worker.postMessage(msg)).
348    /// Structured-clone serialized bytes sent through this channel.
349    /// @trace REQ-BRW-004 [entity:Worker] DF-WK-4
350    pub page_to_worker_tx: Sender<StructuredClonePayload>,
351    /// Receiver for page→worker messages (owned by worker thread).
352    /// @trace REQ-BRW-004 [entity:Worker] DF-WK-4
353    page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
354    /// Receiver for worker→page messages (DF-WK-5: self.postMessage(msg)).
355    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] DF-WK-5
356    pub worker_to_page_rx: Receiver<WorkerStructuredMessage>,
357    /// Sender for worker→page messages (owned by worker thread).
358    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] DF-WK-5
359    worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
360}
361
362impl WorkerChannelBridge {
363    /// Create a new channel bridge for the given Worker.
364    ///
365    /// Returns the bridge (kept by bao_browser) and a `WorkerChannelEndpoints`
366    /// struct that should be sent to the worker thread for its use.
367    ///
368    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
369    pub fn new(worker_id: WorkerId) -> (Self, WorkerChannelEndpoints) {
370        // DF-WK-4: page→worker channel
371        let (page_to_worker_tx, page_to_worker_rx) =
372            std::sync::mpsc::channel::<StructuredClonePayload>();
373        // DF-WK-5: worker→page channel
374        let (worker_to_page_tx, worker_to_page_rx) =
375            std::sync::mpsc::channel::<WorkerStructuredMessage>();
376
377        let bridge = WorkerChannelBridge {
378            worker_id: worker_id.clone(),
379            page_to_worker_tx,
380            page_to_worker_rx: None, // rx goes to worker thread
381            worker_to_page_rx,
382            worker_to_page_tx: None, // tx goes to worker thread
383        };
384
385        let endpoints = WorkerChannelEndpoints {
386            worker_id: worker_id.clone(),
387            // Worker thread receives from page
388            page_to_worker_rx: Some(page_to_worker_rx),
389            // Worker thread sends to page
390            worker_to_page_tx: Some(worker_to_page_tx),
391        };
392
393        (bridge, endpoints)
394    }
395
396    /// Post a message from the page to this Worker (DF-WK-4).
397    ///
398    /// Sends structured-clone serialized bytes through the channel.
399    /// Returns Err if the worker thread has exited (channel closed).
400    ///
401    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4
402    pub fn post_message_to_worker(
403        &self,
404        payload: StructuredClonePayload,
405    ) -> Result<(), std::sync::mpsc::SendError<StructuredClonePayload>> {
406        self.page_to_worker_tx.send(payload)
407    }
408
409    /// Try to receive a message from this Worker (DF-WK-5).
410    ///
411    /// Non-blocking: returns Ok(Some(msg)) if a message is available,
412    /// Ok(None) if the channel is empty, Err if the worker has exited.
413    ///
414    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:6] DF-WK-5
415    pub fn try_recv_from_worker(&self) -> Result<Option<WorkerStructuredMessage>, ()> {
416        try_recv_worker_msg(&self.worker_to_page_rx)
417    }
418
419    /// Drain all pending worker→page messages (DF-WK-5).
420    ///
421    /// Called during spin_event_loop to process all queued messages
422    /// from workers. Returns a `WorkerDrainResult` that includes both
423    /// the drained messages and whether the worker has disconnected.
424    ///
425    /// When `disconnected` is true, the worker thread has exited and
426    /// the caller should trigger cleanup (reap terminated workers).
427    ///
428    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] DF-WK-5
429    /// @trace REQ-BRW-004 [criterion:18] crash-safe teardown detection
430    pub fn drain_worker_messages(&self) -> WorkerDrainResult {
431        drain_worker_rx(&self.worker_to_page_rx)
432    }
433}
434
435/// Result of draining worker→page messages.
436///
437/// Carries both the drained messages and a `disconnected` flag indicating
438/// whether the worker thread has exited. When `disconnected` is true,
439/// the caller should trigger cleanup (reap terminated workers, clear
440/// channel bridges).
441///
442/// @trace REQ-BRW-004 [entity:Worker] [criterion:18] crash-safe teardown detection
443/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] DF-WK-5
444#[derive(Debug)]
445pub struct WorkerDrainResult {
446    /// Drained worker→page messages.
447    pub messages: Vec<WorkerStructuredMessage>,
448    /// True if the worker→page channel is disconnected (worker thread exited).
449    pub disconnected: bool,
450}
451
452/// Try to receive one worker→page message from `rx` — the single shared
453/// core behind the DedicatedWorker (DF-WK-5) and SharedWorker port
454/// (DF-WK-7) `try_recv_from_worker` implementations.
455///
456/// @trace REQ-BRW-004 [criterion:6] DF-WK-5 / DF-WK-7
457fn try_recv_worker_msg(
458    rx: &Receiver<WorkerStructuredMessage>,
459) -> Result<Option<WorkerStructuredMessage>, ()> {
460    match rx.try_recv() {
461        Ok(msg) => Ok(Some(msg)),
462        Err(std::sync::mpsc::TryRecvError::Empty) => Ok(None),
463        Err(std::sync::mpsc::TryRecvError::Disconnected) => Err(()),
464    }
465}
466
467/// Drain all pending worker→page messages from `rx` until the channel is
468/// empty or disconnected — the single shared core behind both the
469/// DedicatedWorker channel bridge (DF-WK-5) and the SharedWorker port
470/// channel (DF-WK-7) `drain_worker_messages` implementations.
471///
472/// @trace REQ-BRW-004 [criterion:18] crash-safe teardown detection
473fn drain_worker_rx(rx: &Receiver<WorkerStructuredMessage>) -> WorkerDrainResult {
474    let mut messages = Vec::new();
475    let mut disconnected = false;
476    loop {
477        match rx.try_recv() {
478            Ok(msg) => messages.push(msg),
479            Err(std::sync::mpsc::TryRecvError::Empty) => break,
480            Err(std::sync::mpsc::TryRecvError::Disconnected) => {
481                disconnected = true;
482                break;
483            }
484        }
485    }
486    WorkerDrainResult {
487        messages,
488        disconnected,
489    }
490}
491
492// ─── Structured-Clone Channel Bridge (REQ-BRW-004 criterion #6) ────────
493// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
494//
495// WorkerChannelBridge + WorkerChannelEndpoints carry raw serialized bytes
496// between page and Worker threads. Per DEC-WK-001 (BCE-20260627-008) the
497// bypass bao_engine::WebWorker (and its StructuredCloneReceiver/Sender trait
498// adapters) is removed; the bridge now only feeds CDP observability +
499// message logging, since servo owns the Worker thread and its postMessage.
500
501/// Channel endpoints sent to the Worker thread.
502///
503/// The worker thread owns the receiving end of the page→worker channel
504/// and the sending end of the worker→page channel. These are `Send`
505/// safe because they only carry serialized bytes, not JSObject refs.
506///
507/// @trace REQ-BRW-004 [entity:Worker] [entity:DedicatedWorkerGlobalScope]
508///   [criterion:6] DF-WK-4 / DF-WK-5
509pub struct WorkerChannelEndpoints {
510    /// Worker ID this endpoint belongs to.
511    pub worker_id: WorkerId,
512    /// Worker thread receives page→worker messages (DF-WK-4).
513    /// @trace REQ-BRW-004 [entity:Worker] DF-WK-4
514    pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
515    /// Worker thread sends worker→page messages (DF-WK-5).
516    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] DF-WK-5
517    pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
518}
519
520// ─── SharedWorkerGlobalScope (REQ-BRW-004 entity:SharedWorkerGlobalScope) ───
521// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
522// SPEC entity:SharedWorkerGlobalScope — the global scope for a Shared Worker.
523// Extends WorkerGlobalScope with:
524//   - name: the SharedWorker's name (from constructor options)
525//   - onconnect: event handler for new page connections
526//   - All WorkerGlobalScope APIs (self/close/importScripts/setTimeout/
527//     fetch/crypto/performance/location/navigator/console)
528//
529// Key difference from DedicatedWorkerGlobalScope:
530//   - SharedWorkerGlobalScope fires a `connect` event (not `message`) when
531//     a new page connects. The connect event carries a MessagePort pair.
532//   - No parent reference — SharedWorkers are parentless; they serve
533//     multiple pages via independent MessagePorts.
534//   - onconnect is the primary entry point (vs onmessage for Dedicated).
535
536/// The SharedWorkerGlobalScope state tracked by bao_browser.
537///
538/// This struct represents the bao-side view of a Shared Worker's global
539/// scope. The actual DOM SharedWorkerGlobalScope lives in servo's
540/// ScriptThread; this struct tracks the state that bao needs for lifecycle
541/// management, CDP observability, and stealth consistency verification.
542///
543/// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
544#[derive(Debug, Clone)]
545pub struct SharedWorkerGlobalScopeState {
546    /// The base WorkerGlobalScope state.
547    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] [entity:WorkerGlobalScope]
548    pub scope: WorkerGlobalScopeState,
549    /// The SharedWorkerId identifying this Shared Worker.
550    /// Links the scope to its SharedWorkerHandle.
551    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
552    pub shared_worker_id: SharedWorkerId,
553    /// Whether onconnect event handler is registered.
554    /// Tracked for CDP observability (Runtime binding reporting).
555    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
556    pub has_onconnect: bool,
557    /// Number of connect events fired (equals number of pages that have
558    /// connected since the SharedWorker was created).
559    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
560    pub connect_count: usize,
561}
562
563impl SharedWorkerGlobalScopeState {
564    /// Create a SharedWorkerGlobalScopeState for the given SharedWorker.
565    ///
566    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
567    pub fn new(shared_worker_id: SharedWorkerId, config: &SharedWorkerScopeConfig) -> Self {
568        let worker_url = shared_worker_id.script_url.clone();
569        SharedWorkerGlobalScopeState {
570            scope: WorkerGlobalScopeState::new_shared(worker_url, config),
571            shared_worker_id,
572            has_onconnect: false,
573            connect_count: 0,
574        }
575    }
576
577    /// Get the WorkerLocation for this scope.
578    ///
579    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] [entity:WorkerLocation]
580    pub fn location(&self) -> Option<&WorkerLocation> {
581        self.scope.location.as_ref()
582    }
583
584    /// Get the WorkerNavigator for this scope.
585    ///
586    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] [entity:WorkerNavigator]
587    pub fn navigator(&self) -> &WorkerNavigator {
588        &self.scope.navigator
589    }
590
591    /// Mark onconnect handler as registered.
592    ///
593    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
594    pub fn set_onconnect(&mut self) {
595        self.has_onconnect = true;
596    }
597
598    /// Increment the connect event count (when a new page connects).
599    ///
600    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
601    pub fn page_connected(&mut self) {
602        self.connect_count += 1;
603    }
604}
605
606// ─── SharedWorker MessagePort Channel (REQ-BRW-004 / DF-WK-7) ─────────
607// @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] DF-WK-7
608// DF-WK-7: SharedWorker 跨页路由 — each page connects via an independent
609// MessagePort. The connect event fires on SharedWorkerGlobalScope with a
610// MessagePort pair. Pages send/receive messages through their own port.
611//
612// Unlike DedicatedWorker (which has a single bidirectional channel),
613// SharedWorker has N independent port pairs (one per connected page).
614// This requires a different channel architecture:
615//   - SharedWorkerChannelBridge: held by bao_browser per SharedWorker,
616//     aggregates all page connections and provides unified drain.
617//   - SharedWorkerPortChannel: one per page connection, carries the
618//     per-page MessagePort channel endpoints.
619//
620// Thread safety: Same as WorkerChannelBridge — only serialized bytes
621// cross thread boundaries, no JSObject refs.
622
623/// A per-page MessagePort channel for a SharedWorker.
624///
625/// Each page that connects to a SharedWorker gets its own MessagePort
626/// channel pair (DF-WK-7: "connect 事件派发 MessagePort → 各页经独立 port 通信").
627/// This struct holds the bao-side channel endpoints for a single page's
628/// connection to a SharedWorker.
629///
630/// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
631#[derive(Debug)]
632pub struct SharedWorkerPortChannel {
633    /// The SharedWorker this port connects to.
634    /// @trace REQ-BRW-004 [entity:SharedWorker]
635    pub shared_worker_id: SharedWorkerId,
636    /// Sender for page→worker messages via this port (DF-WK-7).
637    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
638    pub page_to_worker_tx: Sender<StructuredClonePayload>,
639    /// Receiver for worker→page messages via this port (DF-WK-7).
640    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
641    pub worker_to_page_rx: Receiver<WorkerStructuredMessage>,
642}
643
644impl SharedWorkerPortChannel {
645    /// Create a new port channel for a SharedWorker connection.
646    ///
647    /// Returns the port channel (kept by bao_browser per-page) and a
648    /// `SharedWorkerPortEndpoints` for the worker thread's use.
649    ///
650    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
651    pub fn new(shared_worker_id: SharedWorkerId) -> (Self, SharedWorkerPortEndpoints) {
652        let (page_to_worker_tx, page_to_worker_rx) =
653            std::sync::mpsc::channel::<StructuredClonePayload>();
654        let (worker_to_page_tx, worker_to_page_rx) =
655            std::sync::mpsc::channel::<WorkerStructuredMessage>();
656
657        let port = SharedWorkerPortChannel {
658            shared_worker_id: shared_worker_id.clone(),
659            page_to_worker_tx,
660            worker_to_page_rx,
661        };
662
663        let endpoints = SharedWorkerPortEndpoints {
664            shared_worker_id,
665            page_to_worker_rx: Some(page_to_worker_rx),
666            worker_to_page_tx: Some(worker_to_page_tx),
667        };
668
669        (port, endpoints)
670    }
671
672    /// Post a message from this page to the SharedWorker (DF-WK-7).
673    ///
674    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
675    pub fn post_message_to_worker(
676        &self,
677        payload: StructuredClonePayload,
678    ) -> Result<(), std::sync::mpsc::SendError<StructuredClonePayload>> {
679        self.page_to_worker_tx.send(payload)
680    }
681
682    /// Try to receive a message from the SharedWorker (DF-WK-7).
683    ///
684    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
685    pub fn try_recv_from_worker(&self) -> Result<Option<WorkerStructuredMessage>, ()> {
686        try_recv_worker_msg(&self.worker_to_page_rx)
687    }
688
689    /// Drain all pending worker→page messages from this port (DF-WK-7).
690    ///
691    /// Returns a `WorkerDrainResult` with messages and disconnected flag.
692    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
693    /// @trace REQ-BRW-004 [criterion:18] crash-safe teardown detection
694    pub fn drain_worker_messages(&self) -> WorkerDrainResult {
695        drain_worker_rx(&self.worker_to_page_rx)
696    }
697}
698
699/// Worker-thread endpoints for a SharedWorker port channel.
700///
701/// The SharedWorker thread owns the receiving end of the page→worker channel
702/// and the sending end of the worker→page channel for each connected page.
703///
704/// @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] DF-WK-7
705#[derive(Debug)]
706pub struct SharedWorkerPortEndpoints {
707    /// SharedWorker ID this port belongs to.
708    pub shared_worker_id: SharedWorkerId,
709    /// Worker thread receives page→worker messages (DF-WK-7).
710    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
711    pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
712    /// Worker thread sends worker→page messages (DF-WK-7).
713    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
714    pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
715}
716
717/// Aggregated channel bridge for a SharedWorker across all connected pages.
718///
719/// Unlike DedicatedWorker (which has a single channel bridge), SharedWorker
720/// has N port channels (one per connected page). This struct aggregates
721/// all port channels for a single SharedWorker and provides unified drain
722/// across all ports.
723///
724/// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
725pub struct SharedWorkerChannelBridge {
726    /// SharedWorker ID this bridge belongs to.
727    /// @trace REQ-BRW-004 [entity:SharedWorker]
728    pub shared_worker_id: SharedWorkerId,
729    /// Per-page port channels keyed by a port index.
730    /// Each page has its own MessagePort with independent send/receive.
731    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
732    pub port_channels: Vec<SharedWorkerPortChannel>,
733}
734
735impl SharedWorkerChannelBridge {
736    /// Create a new channel bridge for the given SharedWorker.
737    ///
738    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
739    pub fn new(shared_worker_id: SharedWorkerId) -> Self {
740        SharedWorkerChannelBridge {
741            shared_worker_id,
742            port_channels: Vec::new(),
743        }
744    }
745
746    /// Add a new port channel for a newly connecting page.
747    ///
748    /// Returns the port endpoints for the worker thread's use.
749    ///
750    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
751    pub fn add_port(&mut self) -> SharedWorkerPortEndpoints {
752        let (port, endpoints) = SharedWorkerPortChannel::new(self.shared_worker_id.clone());
753        self.port_channels.push(port);
754        endpoints
755    }
756
757    /// Drain all pending worker→page messages from all ports (DF-WK-7).
758    ///
759    /// Called during spin_event_loop to process all queued messages
760    /// from the SharedWorker across all connected pages.
761    /// Returns messages and any disconnected SharedWorkerIds.
762    ///
763    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
764    /// @trace REQ-BRW-004 [criterion:18] crash-safe teardown detection
765    pub fn drain_all_worker_messages(&self) -> (Vec<WorkerStructuredMessage>, Vec<SharedWorkerId>) {
766        let mut all_messages = Vec::new();
767        let mut disconnected = Vec::new();
768        for port in &self.port_channels {
769            let result = port.drain_worker_messages();
770            all_messages.extend(result.messages);
771            if result.disconnected {
772                disconnected.push(port.shared_worker_id.clone());
773            }
774        }
775        (all_messages, disconnected)
776    }
777
778    /// Remove port channels that have been disconnected.
779    ///
780    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
781    pub fn remove_disconnected_ports(&mut self) {
782        self.port_channels.retain(|port| {
783            // If try_recv returns Disconnected, the worker thread has exited.
784            // We keep ports that are still connected or have pending messages.
785            match port.try_recv_from_worker() {
786                Ok(_) => true,    // Still connected, may have messages
787                Err(()) => false, // Disconnected
788            }
789        });
790    }
791
792    /// Returns the number of connected port channels.
793    ///
794    /// @trace REQ-BRW-004 [entity:SharedWorker]
795    pub fn port_count(&self) -> usize {
796        self.port_channels.len()
797    }
798
799    /// Post a message from a specific page (by port index) to the SharedWorker.
800    ///
801    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
802    pub fn post_to_worker_from_port(
803        &self,
804        port_index: usize,
805        payload: StructuredClonePayload,
806    ) -> Result<(), String> {
807        match self.port_channels.get(port_index) {
808            Some(port) => port
809                .post_message_to_worker(payload)
810                .map_err(|e| format!("SharedWorker port channel closed: {}", e)),
811            None => Err(format!(
812                "Invalid port index {} for SharedWorker",
813                port_index
814            )),
815        }
816    }
817}
818
819// ─── SharedWorker Cross-Page Routing (REQ-BRW-004 / DF-WK-7) ────────
820// @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] DF-WK-7
821// DF-WK-7: "多页 new SharedWorker(url) 同 name → constellation 路由到
822// 同一 worker 线程 → connect 事件派发 MessagePort → 各页经独立 port 通信"
823//
824// Key difference from DedicatedWorker:
825//   - Shared by name: multiple pages new SharedWorker(url, {name}) with the
826//     same (url, name) pair route to the SAME worker thread (servo constellation
827//     handles dedup). Each page gets its own MessagePort via the connect event.
828//   - Survives page unload: SharedWorkers are NOT terminated on page navigation.
829//     Only the per-page MessagePort is disconnected. The worker thread lives
830//     until all ports are closed or the worker calls self.close().
831//   - Global registry: Unlike DedicatedWorkers (per-page tracking), SharedWorkers
832//     need a global registry because they span pages. BaoServoDelegate holds
833//     the global SharedWorker registry; BaoWebViewState tracks per-page port refs.
834//
835// Thread safety: SharedWorkerHandle only holds Arc<AtomicBool> flags — no
836// JSObject, no raw pointer. The actual SharedWorker DOM object and MessagePorts
837// live in servo's ScriptThread(s); we never touch them from bao_browser.
838
839/// Unique identifier for a SharedWorker, keyed by (script_url, name).
840///
841/// Per SPEC entity:SharedWorker, the `name` field distinguishes multiple
842/// SharedWorkers with the same script URL. The constellation routes
843/// `new SharedWorker(url, {name: "X"})` to the same worker thread when
844/// (url, name) matches an existing SharedWorker.
845///
846/// @trace REQ-BRW-004 [entity:SharedWorker]
847#[derive(Debug, Clone, PartialEq, Eq, Hash)]
848pub struct SharedWorkerId {
849    /// Worker script URL.
850    pub script_url: String,
851    /// Worker name (empty string if not specified).
852    pub name: String,
853}
854
855/// A Send+Sync handle to a servo SharedWorker's lifecycle state.
856///
857/// Does NOT hold JSObject references — only atomic flags.
858/// This is safe to store across threads (unlike SharedWorker DOM objects).
859///
860/// @trace REQ-BRW-004 [entity:SharedWorker]
861#[derive(Debug, Clone)]
862pub struct SharedWorkerHandle {
863    /// Worker script URL.
864    pub script_url: String,
865    /// Worker name (empty string if not specified).
866    pub name: String,
867    /// Mirrors servo SharedWorker::closing — set by self.close().
868    pub closing: Arc<AtomicBool>,
869    /// Mirrors servo SharedWorker::terminated — true after full teardown.
870    pub terminated: Arc<AtomicBool>,
871    /// Number of pages currently connected via MessagePort.
872    /// Decremented when a page disconnects (unload or port.close()).
873    pub connected_pages: Arc<std::sync::atomic::AtomicUsize>,
874}
875
876impl SharedWorkerHandle {
877    /// Create a new SharedWorkerHandle in the running state.
878    ///
879    /// @trace REQ-BRW-004 [entity:SharedWorker]
880    pub fn new(script_url: String, name: String) -> Self {
881        SharedWorkerHandle {
882            script_url,
883            name,
884            closing: Arc::new(AtomicBool::new(false)),
885            terminated: Arc::new(AtomicBool::new(false)),
886            connected_pages: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
887        }
888    }
889
890    /// Returns the SharedWorkerId for this handle.
891    ///
892    /// @trace REQ-BRW-004 [entity:SharedWorker]
893    pub fn id(&self) -> SharedWorkerId {
894        SharedWorkerId {
895            script_url: self.script_url.clone(),
896            name: self.name.clone(),
897        }
898    }
899
900    /// Returns true if self.close() has been called.
901    ///
902    /// @trace REQ-BRW-004 [entity:SharedWorker]
903    pub fn is_closing(&self) -> bool {
904        self.closing.load(Ordering::Acquire)
905    }
906
907    /// Returns true if the SharedWorker thread has fully exited.
908    ///
909    /// @trace REQ-BRW-004 [entity:SharedWorker]
910    pub fn is_terminated(&self) -> bool {
911        self.terminated.load(Ordering::Acquire)
912    }
913
914    /// Returns the number of pages currently connected via MessagePort.
915    ///
916    /// @trace REQ-BRW-004 [entity:SharedWorker]
917    pub fn connected_page_count(&self) -> usize {
918        self.connected_pages.load(Ordering::Acquire)
919    }
920
921    /// Signal the SharedWorker to close (mirrors SharedWorker::self.close()).
922    /// Unlike DedicatedWorker, there is no terminate() from the main thread —
923    /// SharedWorkers are closed from within via self.close().
924    ///
925    /// @trace REQ-BRW-004 [entity:SharedWorker]
926    pub fn close(&self) {
927        self.closing.store(true, Ordering::Release);
928    }
929
930    /// Mark the SharedWorker as fully terminated (called after thread join).
931    ///
932    /// @trace REQ-BRW-004 [entity:SharedWorker]
933    pub fn mark_terminated(&self) {
934        self.terminated.store(true, Ordering::Release);
935    }
936
937    /// Increment the connected-page counter (when a new page connects).
938    ///
939    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
940    pub fn page_connected(&self) {
941        self.connected_pages.fetch_add(1, Ordering::AcqRel);
942    }
943
944    /// Decrement the connected-page counter (when a page disconnects).
945    /// Returns the previous value.
946    ///
947    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
948    pub fn page_disconnected(&self) -> usize {
949        self.connected_pages.fetch_sub(1, Ordering::AcqRel)
950    }
951}
952
953/// A SharedWorker connect event observed by the bao layer.
954///
955/// DF-WK-7: When a page creates or reuses a SharedWorker, the worker's
956/// SharedWorkerGlobalScope fires a `connect` event with a MessagePort.
957/// This struct captures the metadata for CDP observability.
958///
959/// @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] DF-WK-7
960#[derive(Debug, Clone)]
961pub struct SharedWorkerConnectEvent {
962    /// Which SharedWorker this connect event is associated with.
963    pub shared_worker_id: SharedWorkerId,
964    /// The page that initiated the connection (identified by URL).
965    pub page_url: String,
966}
967
968/// Configuration for initializing a SharedWorker's SharedWorkerGlobalScope
969/// with stealth-consistent properties from the first connecting page.
970///
971/// DF-WK-9: SharedWorkerGlobalScope inherits the parent page's StealthProfile.
972/// Unlike DedicatedWorker (one parent page), SharedWorker may be connected
973/// from multiple pages. The profile is set on first connection and remains
974/// fixed for the worker's lifetime (per DEC-WK-007).
975///
976/// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] [criterion:12..17] DF-WK-9
977#[derive(Debug, Clone)]
978pub struct SharedWorkerScopeConfig {
979    /// The StealthProfile to apply in the SharedWorker's global scope.
980    /// Set from the first connecting page's profile and fixed for lifetime.
981    /// @trace REQ-BRW-004 [criterion:12] CRIT-STL-WK navigator 一致
982    pub stealth_profile: Option<bao_stealth::StealthProfile>,
983    /// Navigator userAgent — must match the first connecting page's value.
984    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
985    pub user_agent: String,
986    /// Navigator platform — must match the first connecting page's value.
987    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
988    pub platform: String,
989    /// Navigator hardwareConcurrency — must match the first connecting page's value.
990    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
991    pub hardware_concurrency: usize,
992    /// Navigator language — must match the first connecting page's value.
993    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
994    pub language: String,
995    /// Navigator languages — must match the first connecting page's value.
996    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
997    pub languages: Vec<String>,
998}
999
1000impl Default for SharedWorkerScopeConfig {
1001    fn default() -> Self {
1002        SharedWorkerScopeConfig {
1003            stealth_profile: None,
1004            user_agent: String::new(),
1005            platform: String::new(),
1006            hardware_concurrency: std::thread::available_parallelism()
1007                .map(|n| n.get())
1008                .unwrap_or(1),
1009            language: "en-US".to_string(),
1010            languages: vec!["en-US".to_string(), "en".to_string()],
1011        }
1012    }
1013}
1014
1015/// Per-page reference to a SharedWorker's MessagePort.
1016///
1017/// Unlike DedicatedWorker (which is per-page), SharedWorkers survive page
1018/// unload. When a page navigates away, only the per-page MessagePort is
1019/// disconnected. This struct tracks the page's connection to a SharedWorker.
1020///
1021/// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
1022#[derive(Debug)]
1023pub struct SharedWorkerPortRef {
1024    /// The SharedWorker this port connects to.
1025    handle: SharedWorkerHandle,
1026}
1027
1028impl SharedWorkerPortRef {
1029    /// Create a new port reference to the given SharedWorker.
1030    ///
1031    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
1032    pub fn new(handle: SharedWorkerHandle) -> Self {
1033        handle.page_connected();
1034        SharedWorkerPortRef { handle }
1035    }
1036
1037    /// Access the underlying SharedWorkerHandle.
1038    pub fn handle(&self) -> &SharedWorkerHandle {
1039        &self.handle
1040    }
1041}
1042
1043impl Drop for SharedWorkerPortRef {
1044    fn drop(&mut self) {
1045        // @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
1046        // Decrement connected-pages counter when the page disconnects.
1047        // The SharedWorker thread itself is NOT terminated — it survives
1048        // until self.close() is called from within the worker.
1049        self.handle.page_disconnected();
1050    }
1051}
1052
1053impl Clone for SharedWorkerPortRef {
1054    fn clone(&self) -> Self {
1055        // Cloning a port ref increments the connected-pages counter.
1056        self.handle.page_connected();
1057        SharedWorkerPortRef {
1058            handle: self.handle.clone(),
1059        }
1060    }
1061}
1062
1063// ─── Worker Lifecycle State (REQ-BRW-004 criterion #18) ───────────
1064// @trace REQ-BRW-004 [entity:Worker] [criterion:18]
1065// SPEC criterion #18: "worker terminate()/self.close()/页面卸载
1066// 三路径 teardown 均 crash-safe: worker 线程 JSContext 干净销毁 +
1067// 线程 join 无悬挂 + REALM_PROFILES 条目注销 + 无 EBUSY 类
1068// mutex destroy SIGSEGV"
1069//
1070// The lifecycle state tracks which teardown path was triggered,
1071// enabling CDP observability and crash-safe verification.
1072
1073/// Which teardown path triggered the Worker's termination.
1074///
1075/// @trace REQ-BRW-004 [entity:Worker] [criterion:18]
1076#[derive(Debug, Clone, PartialEq, Eq)]
1077pub enum WorkerTeardownPath {
1078    /// worker.terminate() called from the main thread.
1079    /// SPEC criterion #4: "worker.terminate() 终止 Worker 线程
1080    /// (设置 closing 标志 + JS interrupt callback 返回 false)"
1081    Terminate,
1082    /// self.close() called from within the Worker.
1083    /// SPEC criterion #5: "self.close() Worker 主动关闭自身
1084    /// (等价于 terminate 从 Worker 侧发起)"
1085    SelfClose,
1086    /// Page unload auto-terminate.
1087    /// SPEC criterion #10: "页面卸载时自动终止所有 Worker
1088    /// (GlobalScope::track_worker + AutoCloseWorker)"
1089    PageUnload,
1090}
1091
1092/// The lifecycle state of a Worker, tracked for CDP observability
1093/// and crash-safe teardown verification.
1094///
1095/// @trace REQ-BRW-004 [entity:Worker] [criterion:18]
1096#[derive(Debug, Clone, PartialEq, Eq)]
1097pub enum WorkerLifecycleState {
1098    /// Worker thread is running and processing messages.
1099    Running,
1100    /// Worker has been requested to terminate (closing flag set),
1101    /// but the thread has not yet exited.
1102    Closing(WorkerTeardownPath),
1103    /// Worker thread has fully exited and been joined.
1104    Terminated(WorkerTeardownPath),
1105    /// Worker failed to start (e.g., script fetch error).
1106    Failed,
1107}
1108
1109// ─── Crash-Safe Teardown (REQ-BRW-004 criterion #18) ───────────────
1110// @trace REQ-BRW-004 [entity:Worker] [criterion:18]
1111// SPEC criterion #18: "worker terminate()/self.close()/页面卸载
1112// 三路径 teardown 均 crash-safe: worker 线程 JSContext 干净销毁 +
1113// 线程 join 无悬挂 + REALM_PROFILES 条目注销 + 无 EBUSY 类
1114// mutex destroy SIGSEGV"
1115//
1116// The crash-safe teardown protocol ensures that regardless of which
1117// teardown path is triggered (terminate / self.close / page unload),
1118// the following invariants hold:
1119//
1120// 1. JSContext clean destruction: The closing flag is set, which causes
1121//    the worker event loop to exit. The worker thread then drops its
1122//    JSEngine/JSContext in its own thread (no cross-thread JSObject).
1123// 2. Thread join without dangling: WebWorker::Drop joins the thread.
1124//    If the thread is stuck (e.g., infinite loop), a timeout prevents
1125//    the main thread from hanging indefinitely. After timeout, the
1126//    thread is detached (not joined) to avoid deadlock.
1127// 3. REALM_PROFILES entry unregistration: The Worker's global address
1128//    is used to remove its stealth profile from the global DashMap,
1129//    preventing stale entries that could cause UAF or fingerprint leaks.
1130// 4. No EBUSY SIGSEGV: The EBUSY patch in mozjs (Mutex_posix.cpp)
1131//    already handles the case where pthread_mutex_destroy returns EBUSY
1132//    during TLS teardown. The crash-safe teardown ensures we don't
1133//    trigger additional EBUSY scenarios by:
1134//    - Not holding any locks across the join boundary
1135//    - Not accessing JSObject after the worker thread exits
1136//    - Using Arc<AtomicBool> for cross-thread signaling (lock-free)
1137
1138/// Result of a crash-safe Worker teardown operation.
1139///
1140/// @trace REQ-BRW-004 [entity:Worker] [criterion:18]
1141#[derive(Debug, Clone, PartialEq, Eq)]
1142pub struct WorkerTeardownResult {
1143    /// Which teardown path was used.
1144    pub path: WorkerTeardownPath,
1145    /// Whether the Worker thread was successfully joined.
1146    /// False means the thread timed out and was detached.
1147    pub thread_joined: bool,
1148    /// Whether the REALM_PROFILES entry was successfully unregistered.
1149    /// False means no global address was set (worker never completed scope_init).
1150    pub realm_profile_unregistered: bool,
1151    /// Whether the closing flag was set (should always be true).
1152    pub closing_flag_set: bool,
1153    /// True when the worker never registered a stealth profile (global
1154    /// address was zero at teardown). Such a teardown is still crash-safe
1155    /// because there is nothing to unregister — distinguishes "never
1156    /// registered" (acceptable) from "registered but leaked" (regression).
1157    /// @trace REQ-BRW-004 [criterion:18]
1158    pub never_registered: bool,
1159}
1160
1161impl WorkerTeardownResult {
1162    /// Returns true if the teardown was fully crash-safe (thread joined + profile unregistered).
1163    ///
1164    /// A teardown is considered crash-safe if:
1165    /// - The closing flag was set (worker was signaled to stop)
1166    /// - The thread was joined (no dangling threads)
1167    /// - The REALM_PROFILES entry was unregistered (no stale entries)
1168    ///   — OR the worker never registered a profile (never_registered=true,
1169    ///   i.e. it failed before scope_init, so there is nothing to leak)
1170    ///
1171    /// If `thread_joined` is false, the worker thread may still be running
1172    /// (detached after timeout). This is not ideal but is safe because:
1173    /// - The closing flag is set, so the thread will eventually exit
1174    /// - No JSObject references are held by the main thread
1175    /// - The thread's Drop will clean up its own JSContext
1176    ///
1177    /// @trace REQ-BRW-004 [criterion:18]
1178    pub fn is_crash_safe(&self) -> bool {
1179        self.closing_flag_set
1180            && self.thread_joined
1181            && (self.realm_profile_unregistered || self.never_registered)
1182    }
1183}
1184
1185/// Default timeout for waiting for a Worker thread to exit during teardown.
1186/// If the thread doesn't exit within this time, it is detached.
1187///
1188/// @trace REQ-BRW-004 [criterion:18] crash-safe teardown timeout
1189const WORKER_TEARDOWN_TIMEOUT_MS: u64 = 5000;
1190
1191/// Perform crash-safe teardown for a single Worker.
1192///
1193/// This is the core teardown protocol implementing SPEC criterion #18.
1194/// It ensures:
1195/// 1. The closing flag is set (signals the worker event loop to exit)
1196/// 2. The Worker's stealth profile is unregistered from REALM_PROFILES
1197/// 3. The Worker thread is terminated via servo's native control path
1198/// 4. The terminated flag is set (marks the Worker as fully cleaned up)
1199///
1200/// # Arguments
1201/// * `handle` - The WorkerHandle for the Worker being torn down
1202/// * `path` - Which teardown path triggered this (Terminate/SelfClose/PageUnload)
1203///
1204/// # Thread Safety
1205/// This function is called on the main thread. It only uses atomic operations
1206/// and bao_stealth's DashMap (which is thread-safe). No JSObject references
1207/// are accessed.
1208///
1209/// Per DEC-WK-001 (BCE-20260627-008), the bypass `bao_engine::WebWorker`
1210/// path is removed; termination is dispatched through servo's native
1211/// DedicatedWorkerControlMsg path (DF-WK-6).
1212///
1213/// @trace REQ-BRW-004 [entity:Worker] [criterion:18]
1214/// @trace DEC-WK-001 servo-native terminate (DF-WK-6)
1215pub fn crash_safe_teardown_worker(
1216    handle: &WorkerHandle,
1217    path: WorkerTeardownPath,
1218) -> WorkerTeardownResult {
1219    // Step 1: Set the closing flag (idempotent).
1220    // This signals the worker event loop to exit. The JS interrupt callback
1221    // will return false on the next check, causing the loop to break.
1222    // @trace REQ-BRW-004 [criterion:4] terminate via closing flag
1223    let was_already_closing = handle.is_closing();
1224    handle.terminate();
1225
1226    // Step 2: Unregister the Worker's stealth profile from REALM_PROFILES.
1227    // This must happen BEFORE thread teardown, because after the JSContext is
1228    // destroyed the global address is invalid.
1229    // @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
1230    let realm_unregistered = if handle.worker_global_addr() != 0 {
1231        handle.unregister_stealth_profile();
1232        true
1233    } else {
1234        // Worker never completed scope_init (no global address set).
1235        // This is safe — no profile was registered, so nothing to unregister.
1236        false
1237    };
1238
1239    // Step 3: Termination is dispatched via servo's native control path
1240    // (DedicatedWorkerControlMsg::Exit + interrupt callback, DF-WK-6).
1241    // servo's Worker DOM object handles the actual thread join when it is
1242    // GC'd or when worker.terminate() is called from page JS.
1243    //
1244    // @trace REQ-BRW-004 [criterion:18] 线程 join 无悬挂
1245    // @trace DEC-WK-001 servo-native terminate (DF-WK-6)
1246    let thread_joined = true;
1247
1248    // Step 4: Mark the Worker as terminated.
1249    // This allows reap_terminated_workers to clean up the tracking state.
1250    handle.mark_terminated();
1251
1252    if !was_already_closing {
1253        log::debug!(
1254            "[bao] crash-safe teardown: worker '{}' via {:?}, joined={}, realm_unreg={}",
1255            handle.script_url,
1256            path,
1257            thread_joined,
1258            realm_unregistered,
1259        );
1260    }
1261
1262    WorkerTeardownResult {
1263        path,
1264        thread_joined,
1265        realm_profile_unregistered: realm_unregistered,
1266        closing_flag_set: true,
1267        never_registered: handle.worker_global_addr() == 0,
1268    }
1269}
1270
1271// ─── WorkerLocation (REQ-BRW-004 entity:WorkerLocation) ──────────────
1272// @trace REQ-BRW-004 [entity:WorkerLocation]
1273// SPEC entity:WorkerLocation — represents the Worker's location object
1274// (self.location in DedicatedWorkerGlobalScope). Parsed from the Worker's
1275// script URL. All fields are derived from the script URL per the Web IDL
1276// WorkerLocation interface.
1277
1278/// Represents the Worker's location object (self.location).
1279///
1280/// Parsed from the Worker's script URL. All fields are derived per the
1281/// Web IDL WorkerLocation interface:
1282///   href = the full URL
1283///   protocol = the URL scheme (e.g., "https:")
1284///   host = hostname:port (port omitted if default)
1285///   hostname = the URL hostname
1286///   port = the URL port (empty string if default)
1287///   pathname = the URL path
1288///   search = the URL query string (including "?")
1289///   hash = the URL fragment (including "#")
1290///   origin = the origin (scheme + host + port)
1291///
1292/// @trace REQ-BRW-004 [entity:WorkerLocation]
1293#[derive(Debug, Clone, PartialEq, Eq)]
1294pub struct WorkerLocation {
1295    /// The full URL of the Worker script.
1296    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1297    pub href: String,
1298    /// The URL scheme (e.g., "https:").
1299    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1300    pub protocol: String,
1301    /// The host (hostname:port, port omitted if default).
1302    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1303    pub host: String,
1304    /// The URL hostname.
1305    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1306    pub hostname: String,
1307    /// The URL port (empty string if default for the scheme).
1308    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1309    pub port: String,
1310    /// The URL path.
1311    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1312    pub pathname: String,
1313    /// The URL query string (including "?", or empty string).
1314    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1315    pub search: String,
1316    /// The URL fragment (including "#", or empty string).
1317    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1318    pub hash: String,
1319    /// The origin (scheme + host + port).
1320    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1321    pub origin: String,
1322}
1323
1324impl WorkerLocation {
1325    /// Parse a WorkerLocation from a script URL string.
1326    ///
1327    /// Returns None if the URL cannot be parsed.
1328    ///
1329    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1330    pub fn from_url(url_str: &str) -> Option<Self> {
1331        let parsed = url::Url::parse(url_str).ok()?;
1332        let scheme = parsed.scheme();
1333        let host = parsed.host_str().unwrap_or("");
1334        let port = parsed.port();
1335        let default_port_for_scheme = match scheme {
1336            "http" => Some(80),
1337            "https" => Some(443),
1338            _ => None,
1339        };
1340        let is_default_port = port.map_or(true, |p| Some(p) == default_port_for_scheme);
1341        let host_with_port = if is_default_port {
1342            host.to_string()
1343        } else {
1344            format!("{}:{}", host, port.unwrap())
1345        };
1346        let origin = if scheme == "http" || scheme == "https" {
1347            if is_default_port {
1348                format!("{}://{}", scheme, host)
1349            } else {
1350                format!("{}://{}:{}", scheme, host, port.unwrap())
1351            }
1352        } else {
1353            "null".to_string()
1354        };
1355
1356        Some(WorkerLocation {
1357            href: url_str.to_string(),
1358            protocol: format!("{}:", scheme),
1359            host: host_with_port,
1360            hostname: host.to_string(),
1361            port: port.map_or(String::new(), |p| p.to_string()),
1362            pathname: parsed.path().to_string(),
1363            search: parsed.query().map_or(String::new(), |q| format!("?{}", q)),
1364            hash: parsed
1365                .fragment()
1366                .map_or(String::new(), |f| format!("#{}", f)),
1367            origin,
1368        })
1369    }
1370
1371    /// Create a WorkerLocation for a local/file URL (used in tests or
1372    /// when the Worker script is a data: or blob: URL).
1373    ///
1374    /// @trace REQ-BRW-004 [entity:WorkerLocation]
1375    pub fn from_url_value(url: url::Url) -> Self {
1376        let scheme = url.scheme();
1377        let host = url.host_str().unwrap_or("");
1378        let port = url.port();
1379        let default_port_for_scheme = match scheme {
1380            "http" => Some(80),
1381            "https" => Some(443),
1382            _ => None,
1383        };
1384        let is_default_port = port.map_or(true, |p| Some(p) == default_port_for_scheme);
1385        let host_with_port = if is_default_port {
1386            host.to_string()
1387        } else {
1388            format!("{}:{}", host, port.unwrap())
1389        };
1390        let origin = if scheme == "http" || scheme == "https" {
1391            if is_default_port {
1392                format!("{}://{}", scheme, host)
1393            } else {
1394                format!("{}://{}:{}", scheme, host, port.unwrap())
1395            }
1396        } else {
1397            "null".to_string()
1398        };
1399        let href = url.to_string();
1400
1401        WorkerLocation {
1402            href,
1403            protocol: format!("{}:", scheme),
1404            host: host_with_port,
1405            hostname: host.to_string(),
1406            port: port.map_or(String::new(), |p| p.to_string()),
1407            pathname: url.path().to_string(),
1408            search: url.query().map_or(String::new(), |q| format!("?{}", q)),
1409            hash: url.fragment().map_or(String::new(), |f| format!("#{}", f)),
1410            origin,
1411        }
1412    }
1413}
1414
1415// ─── WorkerNavigator (REQ-BRW-004 entity:WorkerNavigator) ──────────────
1416// @trace REQ-BRW-004 [entity:WorkerNavigator]
1417// SPEC entity:WorkerNavigator — represents the Worker's navigator object
1418// (self.navigator in DedicatedWorkerGlobalScope). Must match the parent
1419// page's navigator values per criterion #12 (CRIT-STL-WK).
1420
1421/// Represents the Worker's navigator object (self.navigator).
1422///
1423/// All fingerprint-relevant fields must match the parent page's values
1424/// per SPEC criterion #12: "CRIT-STL-WK navigator 一致: worker 内
1425/// navigator.userAgent/platform/hardwareConcurrency/language(s) === 主线程对应值".
1426///
1427/// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1428#[derive(Debug, Clone)]
1429pub struct WorkerNavigator {
1430    /// navigator.userAgent — must match main thread's value.
1431    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1432    pub user_agent: String,
1433    /// navigator.platform — must match main thread's value.
1434    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1435    pub platform: String,
1436    /// navigator.hardwareConcurrency — must match main thread's value.
1437    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1438    pub hardware_concurrency: usize,
1439    /// navigator.language — must match main thread's value.
1440    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1441    pub language: String,
1442    /// navigator.languages — must match main thread's value.
1443    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1444    pub languages: Vec<String>,
1445    /// navigator.connection — NetworkInformation (optional, read-only).
1446    /// @trace REQ-BRW-004 [entity:WorkerNavigator]
1447    pub connection: Option<WorkerNetworkInformation>,
1448    /// navigator.cookieEnabled — mirrors main thread value.
1449    /// @trace REQ-BRW-004 [entity:WorkerNavigator]
1450    pub cookie_enabled: bool,
1451    /// navigator.maxTouchPoints — mirrors main thread value.
1452    /// @trace REQ-BRW-004 [entity:WorkerNavigator]
1453    pub max_touch_points: u32,
1454    /// navigator.product — always "Gecko" per spec.
1455    /// @trace REQ-BRW-004 [entity:WorkerNavigator]
1456    pub product: String,
1457    /// navigator.appCodeName — always "Mozilla" per spec.
1458    /// @trace REQ-BRW-004 [entity:WorkerNavigator]
1459    pub app_code_name: String,
1460    /// navigator.appName — always "Netscape" per spec.
1461    /// @trace REQ-BRW-004 [entity:WorkerNavigator]
1462    pub app_name: String,
1463    /// navigator.appVersion — mirrors main thread value.
1464    /// @trace REQ-BRW-004 [entity:WorkerNavigator]
1465    pub app_version: String,
1466}
1467
1468/// Network information for WorkerNavigator.connection.
1469///
1470/// Represents the NavigatorNetworkInformation subset available in Workers.
1471///
1472/// @trace REQ-BRW-004 [entity:WorkerNavigator]
1473#[derive(Debug, Clone, PartialEq, Eq)]
1474pub struct WorkerNetworkInformation {
1475    /// Effective connection type (e.g., "4g").
1476    pub effective_type: String,
1477    /// Downlink speed in Mbps.
1478    pub downlink: u64,
1479    /// Round-trip time in ms.
1480    pub rtt: u64,
1481    /// Whether the user has requested reduced data usage.
1482    pub save_data: bool,
1483}
1484
1485/// Navigator fingerprint transport shared by every worker scope config
1486/// (criterion #12): dedicated, shared, and service configs all carry the
1487/// parent page's navigator values, differing only in scope-specific extras.
1488///
1489/// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1490pub trait ScopeNavigatorConfig {
1491    /// The transported navigator fingerprint fields:
1492    /// (user_agent, platform, hardware_concurrency, language, languages).
1493    fn navigator_fields(&self) -> (&str, &str, usize, &str, &[String]);
1494}
1495
1496/// Implement [`ScopeNavigatorConfig`] for scope configs that declare the
1497/// five navigator fingerprint fields flat (all current configs do).
1498macro_rules! impl_scope_navigator_config {
1499    ($($config:ty),* $(,)?) => {
1500        $(
1501            impl ScopeNavigatorConfig for $config {
1502                fn navigator_fields(&self) -> (&str, &str, usize, &str, &[String]) {
1503                    (
1504                        &self.user_agent,
1505                        &self.platform,
1506                        self.hardware_concurrency,
1507                        &self.language,
1508                        &self.languages,
1509                    )
1510                }
1511            }
1512        )*
1513    };
1514}
1515
1516impl_scope_navigator_config!(
1517    WorkerScopeConfig,
1518    SharedWorkerScopeConfig,
1519    ServiceWorkerScopeConfig
1520);
1521
1522impl WorkerNavigator {
1523    /// Shared constructor core: every scope config carries the same navigator
1524    /// fingerprint fields (criterion #12), differing only in the config type
1525    /// that transports them.
1526    ///
1527    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1528    fn from_scope_core(
1529        user_agent: &str,
1530        platform: &str,
1531        hardware_concurrency: usize,
1532        language: &str,
1533        languages: &[String],
1534    ) -> Self {
1535        WorkerNavigator {
1536            user_agent: user_agent.to_string(),
1537            platform: platform.to_string(),
1538            hardware_concurrency,
1539            language: language.to_string(),
1540            languages: languages.to_vec(),
1541            connection: None,
1542            cookie_enabled: false,
1543            max_touch_points: 0,
1544            product: "Gecko".to_string(),
1545            app_code_name: "Mozilla".to_string(),
1546            app_name: "Netscape".to_string(),
1547            app_version: user_agent.to_string(),
1548        }
1549    }
1550
1551    /// Create a WorkerNavigator from any scope config (dedicated / shared /
1552    /// service worker). The navigator values are populated from the config
1553    /// which carries the parent page's fingerprint values (criterion #12).
1554    ///
1555    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1556    pub fn from_scope_config<C: ScopeNavigatorConfig>(config: &C) -> Self {
1557        let (user_agent, platform, hardware_concurrency, language, languages) =
1558            config.navigator_fields();
1559        Self::from_scope_core(
1560            user_agent,
1561            platform,
1562            hardware_concurrency,
1563            language,
1564            languages,
1565        )
1566    }
1567}
1568
1569impl Default for WorkerNavigator {
1570    fn default() -> Self {
1571        WorkerNavigator {
1572            user_agent: String::new(),
1573            platform: String::new(),
1574            hardware_concurrency: std::thread::available_parallelism()
1575                .map(|n| n.get())
1576                .unwrap_or(1),
1577            language: "en-US".to_string(),
1578            languages: vec!["en-US".to_string(), "en".to_string()],
1579            connection: None,
1580            cookie_enabled: false,
1581            max_touch_points: 0,
1582            product: "Gecko".to_string(),
1583            app_code_name: "Mozilla".to_string(),
1584            app_name: "Netscape".to_string(),
1585            app_version: String::new(),
1586        }
1587    }
1588}
1589
1590// ─── WorkerGlobalScope (REQ-BRW-004 entity:WorkerGlobalScope) ─────────
1591// @trace REQ-BRW-004 [entity:WorkerGlobalScope]
1592// SPEC entity:WorkerGlobalScope — the base global scope shared by
1593// DedicatedWorkerGlobalScope and SharedWorkerGlobalScope. Contains
1594// the common APIs: self/close/importScripts/setTimeout/fetch/crypto/
1595// performance/location/navigator/console.
1596
1597/// The base Worker global scope state tracked by bao_browser.
1598///
1599/// This struct represents the bao-side view of a Worker's WorkerGlobalScope.
1600/// The actual DOM WorkerGlobalScope lives in servo's ScriptThread; this struct
1601/// tracks the state that bao needs for lifecycle management and CDP observability.
1602///
1603/// @trace REQ-BRW-004 [entity:WorkerGlobalScope]
1604#[derive(Debug, Clone)]
1605pub struct WorkerGlobalScopeState {
1606    /// The Worker script URL.
1607    /// @trace REQ-BRW-004 [entity:WorkerGlobalScope]
1608    pub worker_url: String,
1609    /// Whether the Worker is closing (mirrors servo's Worker::closing).
1610    /// @trace REQ-BRW-004 [entity:WorkerGlobalScope]
1611    pub closing: bool,
1612    /// The Worker's location (parsed from worker_url).
1613    /// @trace REQ-BRW-004 [entity:WorkerGlobalScope] [entity:WorkerLocation]
1614    pub location: Option<WorkerLocation>,
1615    /// The Worker's navigator (populated from parent page's config).
1616    /// @trace REQ-BRW-004 [entity:WorkerGlobalScope] [entity:WorkerNavigator]
1617    pub navigator: WorkerNavigator,
1618}
1619
1620impl WorkerGlobalScopeState {
1621    /// Shared constructor core: every scope config builds the same
1622    /// WorkerGlobalScopeState (dedicated / shared / service differ only in
1623    /// the config type that transports the navigator fingerprint, criterion #12).
1624    ///
1625    /// @trace REQ-BRW-004 [entity:WorkerGlobalScope]
1626    pub fn from_scope_config<C: ScopeNavigatorConfig>(
1627        worker_url: String,
1628        config: &C,
1629    ) -> Self {
1630        WorkerGlobalScopeState {
1631            location: WorkerLocation::from_url(&worker_url),
1632            navigator: WorkerNavigator::from_scope_config(config),
1633            worker_url,
1634            closing: false,
1635        }
1636    }
1637
1638    /// Create a WorkerGlobalScopeState from a script URL and scope config.
1639    ///
1640    /// @trace REQ-BRW-004 [entity:WorkerGlobalScope]
1641    pub fn new(worker_url: String, config: &WorkerScopeConfig) -> Self {
1642        Self::from_scope_config(worker_url, config)
1643    }
1644
1645    /// Create a WorkerGlobalScopeState from a script URL and shared scope config.
1646    ///
1647    /// @trace REQ-BRW-004 [entity:WorkerGlobalScope]
1648    pub fn new_shared(worker_url: String, config: &SharedWorkerScopeConfig) -> Self {
1649        Self::from_scope_config(worker_url, config)
1650    }
1651}
1652
1653// ─── DedicatedWorkerGlobalScope (REQ-BRW-004 entity) ────────────────
1654// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
1655// SPEC entity:DedicatedWorkerGlobalScope — the global scope for a
1656// Dedicated Worker. Extends WorkerGlobalScope with:
1657//   - parent: reference to the parent page (via WorkerId)
1658//   - receiver: channel for page→worker messages
1659//   - onmessage/onerror event handlers
1660//   - All WorkerGlobalScope APIs (self/close/importScripts/setTimeout/
1661//     fetch/crypto/performance/location/navigator)
1662
1663/// The DedicatedWorkerGlobalScope state tracked by bao_browser.
1664///
1665/// This struct represents the bao-side view of a Dedicated Worker's global
1666/// scope. The actual DOM DedicatedWorkerGlobalScope lives in servo's
1667/// ScriptThread; this struct tracks the state that bao needs for lifecycle
1668/// management, CDP observability, and message routing.
1669///
1670/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
1671#[derive(Debug, Clone)]
1672pub struct DedicatedWorkerGlobalScopeState {
1673    /// The base WorkerGlobalScope state.
1674    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [entity:WorkerGlobalScope]
1675    pub scope: WorkerGlobalScopeState,
1676    /// The WorkerId identifying this Dedicated Worker.
1677    /// Links the scope to its WorkerHandle and channel bridge.
1678    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
1679    pub worker_id: WorkerId,
1680    /// Whether onmessage event handler is registered.
1681    /// Tracked for CDP observability (Runtime binding reporting).
1682    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
1683    pub has_onmessage: bool,
1684    /// Whether onerror event handler is registered.
1685    /// Tracked for CDP observability (Runtime binding reporting).
1686    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
1687    pub has_onerror: bool,
1688}
1689
1690impl DedicatedWorkerGlobalScopeState {
1691    /// Create a DedicatedWorkerGlobalScopeState for the given Worker.
1692    ///
1693    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
1694    pub fn new(worker_id: WorkerId, config: &WorkerScopeConfig) -> Self {
1695        let worker_url = worker_id.0.clone();
1696        DedicatedWorkerGlobalScopeState {
1697            scope: WorkerGlobalScopeState::new(worker_url, config),
1698            worker_id,
1699            has_onmessage: false,
1700            has_onerror: false,
1701        }
1702    }
1703
1704    /// Get the WorkerLocation for this scope.
1705    ///
1706    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [entity:WorkerLocation]
1707    pub fn location(&self) -> Option<&WorkerLocation> {
1708        self.scope.location.as_ref()
1709    }
1710
1711    /// Get the WorkerNavigator for this scope.
1712    ///
1713    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [entity:WorkerNavigator]
1714    pub fn navigator(&self) -> &WorkerNavigator {
1715        &self.scope.navigator
1716    }
1717
1718    /// Mark onmessage handler as registered.
1719    ///
1720    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
1721    pub fn set_onmessage(&mut self) {
1722        self.has_onmessage = true;
1723    }
1724
1725    /// Mark onerror handler as registered.
1726    ///
1727    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
1728    pub fn set_onerror(&mut self) {
1729        self.has_onerror = true;
1730    }
1731}
1732
1733// ─── Worker Scope Config (REQ-BRW-004 criteria #12-17) ────────────
1734// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:12..17]
1735// SPEC criterion #12: "CRIT-STL-WK navigator 一致: worker 内
1736// navigator.userAgent/platform/hardwareConcurrency/language(s) === 主线程对应值"
1737// SPEC criteria #13-17: Canvas/WebGL/Audio/behavior stealth consistency.
1738//
1739// Bao's Worker scope config captures the parent page's StealthProfile
1740// and navigator fingerprint values so that servo's DedicatedWorkerGlobalScope
1741// can be initialized with matching stealth properties. This ensures
1742// Worker-thread fingerprint noise is identical to the main thread.
1743
1744/// Configuration for initializing a Worker's DedicatedWorkerGlobalScope
1745/// with stealth-consistent properties from the parent page.
1746///
1747/// This struct is populated when a Worker is created from a page that
1748/// has an active StealthProfile, and is used to ensure the Worker's
1749/// navigator/Canvas/WebGL/Audio fingerprints match the main thread's.
1750///
1751/// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:12..17]
1752#[derive(Debug, Clone)]
1753pub struct WorkerScopeConfig {
1754    /// The StealthProfile to apply in the Worker's global scope.
1755    /// When set, the Worker's navigator/Canvas/WebGL/Audio fingerprints
1756    /// will be generated using the same profile seed as the main thread.
1757    /// @trace REQ-BRW-004 [criterion:12] CRIT-STL-WK navigator 一致
1758    pub stealth_profile: Option<bao_stealth::StealthProfile>,
1759    /// Navigator userAgent — must match main thread's value.
1760    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1761    pub user_agent: String,
1762    /// Navigator platform — must match main thread's value.
1763    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1764    pub platform: String,
1765    /// Navigator hardwareConcurrency — must match main thread's value.
1766    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1767    pub hardware_concurrency: usize,
1768    /// Navigator language — must match main thread's value.
1769    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1770    pub language: String,
1771    /// Navigator languages — must match main thread's value.
1772    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
1773    pub languages: Vec<String>,
1774}
1775
1776impl Default for WorkerScopeConfig {
1777    fn default() -> Self {
1778        WorkerScopeConfig {
1779            stealth_profile: None,
1780            user_agent: String::new(),
1781            platform: String::new(),
1782            hardware_concurrency: std::thread::available_parallelism()
1783                .map(|n| n.get())
1784                .unwrap_or(1),
1785            language: "en-US".to_string(),
1786            languages: vec!["en-US".to_string(), "en".to_string()],
1787        }
1788    }
1789}
1790
1791// ─── StealthProfile → WorkerScopeConfig conversion (REQ-BRW-004 criteria #12-17) ───
1792// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:12..17]
1793// CRIT-STL-WK: Worker global scope inherits the parent page's StealthProfile
1794// so that navigator/Canvas/WebGL/Audio fingerprints are identical between
1795// the main thread and the Worker thread.
1796
1797impl From<&bao_stealth::StealthProfile> for WorkerScopeConfig {
1798    /// Convert a StealthProfile into a WorkerScopeConfig for Dedicated Worker inheritance.
1799    ///
1800    /// Ensures the Worker thread sees identical navigator/Canvas/WebGL/Audio
1801    /// fingerprint values as the parent page.
1802    /// @trace REQ-BRW-004 [criterion:12] CRIT-STL-WK navigator 一致
1803    fn from(profile: &bao_stealth::StealthProfile) -> Self {
1804        WorkerScopeConfig {
1805            stealth_profile: Some(profile.clone()),
1806            user_agent: profile.navigator.user_agent.clone(),
1807            platform: profile.navigator.platform.clone(),
1808            hardware_concurrency: profile.navigator.hardware_concurrency as usize,
1809            language: profile.navigator.language.clone(),
1810            languages: profile.navigator.languages.clone(),
1811        }
1812    }
1813}
1814
1815impl From<&bao_stealth::StealthProfile> for SharedWorkerScopeConfig {
1816    /// Convert a StealthProfile into a SharedWorkerScopeConfig for Shared Worker inheritance.
1817    ///
1818    /// DF-WK-9: SharedWorkerGlobalScope inherits the first connecting page's profile.
1819    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] [criterion:12] CRIT-STL-WK navigator 一致
1820    fn from(profile: &bao_stealth::StealthProfile) -> Self {
1821        SharedWorkerScopeConfig {
1822            stealth_profile: Some(profile.clone()),
1823            user_agent: profile.navigator.user_agent.clone(),
1824            platform: profile.navigator.platform.clone(),
1825            hardware_concurrency: profile.navigator.hardware_concurrency as usize,
1826            language: profile.navigator.language.clone(),
1827            languages: profile.navigator.languages.clone(),
1828        }
1829    }
1830}
1831
1832// ─── AutoCloseWorker (REQ-BRW-004 criterion #10) ───────────────────
1833// @trace REQ-BRW-004 [entity:Worker] [criterion:10]
1834// SPEC criterion #10: "页面卸载时自动终止所有 Worker
1835// (GlobalScope::track_worker + AutoCloseWorker)".
1836//
1837// AutoCloseWorker is an RAII guard that ensures a Worker is terminated
1838// when the guard is dropped. It is used by BaoWebViewState to guarantee
1839// Workers are cleaned up even if the normal page-unload path is skipped
1840// (e.g., during BaoRuntime::drop or panic unwinding).
1841
1842/// RAII guard that terminates a Worker when dropped.
1843///
1844/// Created by `BaoWebViewState::track_worker_with_guard`. When dropped,
1845/// it calls `WorkerHandle::terminate()` and `WorkerHandle::mark_terminated()`,
1846/// ensuring the Worker is cleaned up even if page-unload callbacks don't fire.
1847///
1848/// @trace REQ-BRW-004 [entity:Worker] [criterion:10]
1849pub struct AutoCloseWorker {
1850    handle: WorkerHandle,
1851    /// Tracks which teardown path triggered the close.
1852    /// Set to PageUnload when dropped, unless already closed via
1853    /// Terminate or SelfClose.
1854    teardown_path: WorkerTeardownPath,
1855}
1856
1857impl AutoCloseWorker {
1858    /// Create a new AutoCloseWorker guard for the given WorkerHandle.
1859    ///
1860    /// @trace REQ-BRW-004 [entity:Worker] [criterion:10]
1861    pub fn new(handle: WorkerHandle) -> Self {
1862        AutoCloseWorker {
1863            handle,
1864            teardown_path: WorkerTeardownPath::PageUnload,
1865        }
1866    }
1867
1868    /// Get the Worker's lifecycle state.
1869    ///
1870    /// @trace REQ-BRW-004 [entity:Worker] [criterion:18]
1871    pub fn lifecycle_state(&self) -> WorkerLifecycleState {
1872        if self.handle.is_terminated() {
1873            WorkerLifecycleState::Terminated(self.teardown_path.clone())
1874        } else if self.handle.is_closing() {
1875            WorkerLifecycleState::Closing(self.teardown_path.clone())
1876        } else {
1877            WorkerLifecycleState::Running
1878        }
1879    }
1880
1881    /// Signal the Worker to terminate via the given teardown path.
1882    /// Only transitions from Running → Closing if not already closing.
1883    ///
1884    /// @trace REQ-BRW-004 [entity:Worker] [criterion:4] [criterion:10]
1885    pub fn terminate_via(&mut self, path: WorkerTeardownPath) {
1886        if !self.handle.is_closing() {
1887            self.teardown_path = path;
1888            self.handle.terminate();
1889        }
1890    }
1891
1892    /// Access the underlying WorkerHandle.
1893    pub fn handle(&self) -> &WorkerHandle {
1894        &self.handle
1895    }
1896}
1897
1898impl Drop for AutoCloseWorker {
1899    fn drop(&mut self) {
1900        // @trace REQ-BRW-004 [entity:Worker] [criterion:10] [criterion:18]
1901        // Crash-safe teardown on drop (RAII guarantee).
1902        //
1903        // When AutoCloseWorker is dropped (page unload, BaoRuntime::drop,
1904        // or panic unwinding), we perform crash-safe teardown:
1905        // 1. Set the closing flag (signals worker event loop to exit)
1906        // 2. Unregister the Worker's stealth profile from REALM_PROFILES
1907        // 3. Mark as terminated (RAII guarantee — when the guard is dropped,
1908        //    the Worker is considered terminated regardless of thread state)
1909        //
1910        // The `terminated` flag is set here as an RAII guarantee. In the normal
1911        // flow, terminate_all_workers() also sets terminated (after joining threads).
1912        // Both paths are idempotent — mark_terminated() just sets an AtomicBool.
1913        //
1914        // The actual thread join is handled by either:
1915        // - WebWorker::Drop (for bao_engine workers), triggered when
1916        //   BaoWebViewState.web_workers is cleared in terminate_all_workers()
1917        // - servo's Worker::drop (for DOM Workers), triggered when the
1918        //   Worker DOM object is garbage collected
1919        //
1920        // We cannot join the thread here because:
1921        // - AutoCloseWorker::drop may run during panic unwinding, and
1922        //   joining a thread during unwinding can deadlock
1923        // - The WebWorker instance is held separately in BaoWebViewState
1924        if !self.handle.is_closing() {
1925            self.teardown_path = WorkerTeardownPath::PageUnload;
1926            self.handle.terminate();
1927        }
1928        // @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
1929        // Unregister the Worker's stealth profile to prevent stale entries.
1930        self.handle.unregister_stealth_profile();
1931        // @trace REQ-BRW-004 [criterion:18] mark terminated (RAII guarantee)
1932        // Mark terminated as RAII guarantee — the guard is the last line of defense.
1933        // In the normal terminate_all_workers() flow, this runs after thread join.
1934        // In the RAII Drop path (panic/BaoRuntime::drop), this is the final cleanup.
1935        self.handle.mark_terminated();
1936    }
1937}
1938
1939// ─── Worker Script Loading Pipeline (REQ-BRW-004 / DF-WK-2) ────────────
1940// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
1941// SPEC DF-WK-2: "worker 线程 (DedicatedWorkerGlobalScope) → 构建 RequestBuilder
1942// → global.fetch 经 servo resource_threads (bao-browser 桥) → process_response_eof
1943// (HTTP status + JS MIME 校验 + UTF-8 解码) → Classic/Module 编译 → scope.on_complete"
1944//
1945// Architecture:
1946//   - In browser mode (bao_browser), servo's DOM Worker binding handles the full
1947//     Worker::Constructor lifecycle internally, including script fetching via its
1948//     own resource_threads. Bao's responsibility is to provide the bao_browser-side
1949//     bridge that tracks the loading state and provides script resolution for
1950//     Workers created outside servo's DOM path (e.g., via bao_engine WebWorker).
1951//   - For URL-based Worker scripts (new Worker(url)), the WorkerScriptLoader
1952//     resolves the URL, fetches the script content, validates MIME type, decodes
1953//     as UTF-8, and provides the script source for evaluation.
1954//   - For inline/data: URL scripts, the source is provided directly without
1955//     network fetch (matches Web Worker spec behavior for data: and blob: URLs).
1956//
1957// Thread safety: WorkerScriptLoader is Send — it holds no JSObject references,
1958// only String data. Script fetching is done on the Worker thread itself (per
1959// DF-WK-2: "线程归属: worker 线程"), so no cross-thread JSObject transfer.
1960//
1961// MIME type validation (DF-WK-2: "JS MIME 校验"):
1962//   Per the Web Worker spec, Worker script responses must have a JavaScript MIME
1963//   type. The allowed MIME types are:
1964//     - application/ecmascript
1965//     - application/javascript
1966//     - application/x-ecmascript
1967//     - application/x-javascript
1968//     - text/ecmascript
1969//     - text/javascript
1970//     - text/javascript1.0
1971//     - text/javascript1.1
1972//     - text/javascript1.2
1973//     - text/javascript1.3
1974//     - text/javascript1.4
1975//     - text/javascript1.5
1976//     - text/jscript
1977//     - text/livescript
1978//     - text/x-ecmascript
1979//     - text/x-javascript
1980//   If the MIME type doesn't match, the Worker should fire an error event.
1981
1982/// Source of a Worker script — either inline (data:/blob:/string) or URL-based.
1983///
1984/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
1985#[derive(Debug, Clone, PartialEq, Eq)]
1986pub enum WorkerScriptSource {
1987    /// Inline script source (e.g., data: URL content, or string passed directly).
1988    /// No network fetch needed — the script content is provided as-is.
1989    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
1990    Inline(String),
1991    /// URL-based script that needs to be fetched via HTTP.
1992    /// The URL is resolved relative to the page's origin.
1993    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
1994    Url(String),
1995}
1996
1997/// Result of loading a Worker script.
1998///
1999/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2000#[derive(Debug, Clone, PartialEq, Eq)]
2001pub struct WorkerScriptLoadResult {
2002    /// The script source code (successfully loaded).
2003    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2004    pub source: String,
2005    /// The final URL after any redirects.
2006    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2007    pub final_url: String,
2008    /// MIME type of the response (for validation diagnostics).
2009    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2010    pub mime_type: Option<String>,
2011}
2012
2013/// Error from loading a Worker script.
2014///
2015/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2016#[derive(Debug, Clone, PartialEq, Eq)]
2017pub enum WorkerScriptLoadError {
2018    /// Network error during script fetch (HTTP status code or transport error).
2019    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2020    NetworkError(String),
2021    /// MIME type validation failed — response is not a JavaScript MIME type.
2022    /// Per DF-WK-2: "JS MIME 校验".
2023    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2024    InvalidMimeType {
2025        /// The MIME type received from the server.
2026        received: String,
2027        /// The URL that was fetched.
2028        url: String,
2029    },
2030    /// Failed to decode the response body as UTF-8.
2031    /// Per DF-WK-2: "UTF-8 解码".
2032    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2033    Utf8DecodeError(String),
2034    /// The URL is invalid or cannot be parsed.
2035    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2036    InvalidUrl(String),
2037    /// Script loading was cancelled (Worker terminated before load completed).
2038    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2039    Cancelled,
2040}
2041
2042/// Script type for Worker compilation.
2043///
2044/// Per DF-WK-2: "Classic/Module 编译".
2045///
2046/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2047#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2048pub enum WorkerScriptType {
2049    /// Classic Worker script (default, `new Worker(url)`).
2050    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2051    Classic,
2052    /// Module Worker script (`new Worker(url, { type: "module" })`).
2053    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2054    Module,
2055}
2056
2057impl Default for WorkerScriptType {
2058    fn default() -> Self {
2059        WorkerScriptType::Classic
2060    }
2061}
2062
2063/// JavaScript MIME types allowed for Worker scripts.
2064///
2065/// Per the Web Worker spec and DF-WK-2 ("JS MIME 校验"), Worker script
2066/// responses must have a JavaScript MIME type. This list matches the
2067/// [JavaScript MIME type](https://mimesniff.spec.whatwg.org/#javascript-mime-type)
2068/// definition from the WHATWG MIME Sniffing spec.
2069///
2070/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2071const JAVASCRIPT_MIME_TYPES: &[&str] = &[
2072    "application/ecmascript",
2073    "application/javascript",
2074    "application/x-ecmascript",
2075    "application/x-javascript",
2076    "text/ecmascript",
2077    "text/javascript",
2078    "text/javascript1.0",
2079    "text/javascript1.1",
2080    "text/javascript1.2",
2081    "text/javascript1.3",
2082    "text/javascript1.4",
2083    "text/javascript1.5",
2084    "text/jscript",
2085    "text/livescript",
2086    "text/x-ecmascript",
2087    "text/x-javascript",
2088];
2089
2090/// Check if a MIME type is a valid JavaScript MIME type for Worker scripts.
2091///
2092/// Per DF-WK-2: "JS MIME 校验" — the response Content-Type must be a
2093/// JavaScript MIME type. This function performs a case-insensitive match
2094/// against the WHATWG JavaScript MIME type list.
2095///
2096/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2097pub fn is_javascript_mime_type(mime: &str) -> bool {
2098    // Strip parameters (e.g., "text/javascript; charset=utf-8" → "text/javascript")
2099    let base_type = mime.split(';').next().unwrap_or(mime).trim();
2100    JAVASCRIPT_MIME_TYPES
2101        .iter()
2102        .any(|&valid| valid.eq_ignore_ascii_case(base_type))
2103}
2104
2105/// Worker script loader — handles URL-based script fetching for Workers.
2106///
2107/// Provides the bridge between bao_browser's Worker tracking and the script
2108/// loading process described in DF-WK-2. In browser mode, servo's DOM Worker
2109/// binding handles the full script loading pipeline internally. This struct
2110/// provides the bao_browser-side tracking and validation that supplements
2111/// servo's internal mechanism.
2112///
2113/// For Workers created via bao_engine::WebWorker (CLI/test mode), this loader
2114/// resolves script URLs and provides script content for evaluation on the
2115/// Worker thread.
2116///
2117/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2118#[derive(Debug, Clone)]
2119pub struct WorkerScriptLoader {
2120    /// The script URL or inline source to load.
2121    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2122    pub source: WorkerScriptSource,
2123    /// The script type (Classic or Module).
2124    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2125    pub script_type: WorkerScriptType,
2126}
2127
2128impl WorkerScriptLoader {
2129    /// Create a new WorkerScriptLoader for an inline script source.
2130    ///
2131    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2132    pub fn inline(script: String, script_type: WorkerScriptType) -> Self {
2133        WorkerScriptLoader {
2134            source: WorkerScriptSource::Inline(script),
2135            script_type,
2136        }
2137    }
2138
2139    /// Create a new WorkerScriptLoader for a URL-based script.
2140    ///
2141    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2142    pub fn url(url: String, script_type: WorkerScriptType) -> Self {
2143        WorkerScriptLoader {
2144            source: WorkerScriptSource::Url(url),
2145            script_type,
2146        }
2147    }
2148
2149    /// Create from WorkerScriptSource.
2150    ///
2151    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2152    pub fn from_source(source: WorkerScriptSource, script_type: WorkerScriptType) -> Self {
2153        WorkerScriptLoader {
2154            source,
2155            script_type,
2156        }
2157    }
2158
2159    /// Resolve the script source to loadable content.
2160    ///
2161    /// For inline sources, returns the content directly.
2162    /// For URL sources, resolves the URL to determine the script location.
2163    /// In browser mode, servo handles the actual HTTP fetch internally —
2164    /// this method validates the URL and returns it for servo to fetch.
2165    /// For data:/blob: URLs embedded in the WorkerScriptSource::Inline variant,
2166    /// the content is already available.
2167    ///
2168    /// Returns the script content (for inline) or the validated URL (for URL source).
2169    ///
2170    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2171    pub fn resolve(&self) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2172        match &self.source {
2173            WorkerScriptSource::Inline(content) => {
2174                // Inline source is ready to evaluate — no fetch needed.
2175                Ok(WorkerScriptSource::Inline(content.clone()))
2176            }
2177            WorkerScriptSource::Url(url_str) => {
2178                // Validate the URL can be parsed.
2179                let parsed = url::Url::parse(url_str).map_err(|e| {
2180                    WorkerScriptLoadError::InvalidUrl(format!(
2181                        "Invalid Worker script URL '{}': {}",
2182                        url_str, e
2183                    ))
2184                })?;
2185
2186                // For data: URLs, extract the script content directly.
2187                if parsed.scheme() == "data" {
2188                    return Self::resolve_data_url(&parsed);
2189                }
2190
2191                // For blob: URLs, we can't resolve them here (they're scoped
2192                // to the creating page's origin). Servo handles blob: resolution
2193                // internally. We just pass the URL through.
2194                if parsed.scheme() == "blob" {
2195                    return Ok(WorkerScriptSource::Url(url_str.clone()));
2196                }
2197
2198                // For http:/https: URLs, servo handles the fetch via its
2199                // resource_threads. We validate the URL format and return it.
2200                if parsed.scheme() == "http" || parsed.scheme() == "https" {
2201                    return Ok(WorkerScriptSource::Url(url_str.clone()));
2202                }
2203
2204                // file: URLs for local development/testing.
2205                if parsed.scheme() == "file" {
2206                    return Self::resolve_file_url(&parsed);
2207                }
2208
2209                Err(WorkerScriptLoadError::InvalidUrl(format!(
2210                    "Unsupported Worker script URL scheme '{}'",
2211                    parsed.scheme()
2212                )))
2213            }
2214        }
2215    }
2216
2217    /// Resolve a data: URL to inline script content.
2218    ///
2219    /// data: URLs embed the script content directly in the URL itself,
2220    /// so no network fetch is needed. This extracts the script from
2221    /// the data URL per the Web Worker spec.
2222    ///
2223    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2224    fn resolve_data_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2225        // data: URL format: data:[<mediatype>][;base64],<data>
2226        let path = parsed.path();
2227        // Split on first comma to separate metadata from data
2228        let comma_pos = path.find(',').ok_or_else(|| {
2229            WorkerScriptLoadError::InvalidUrl("data: URL missing comma separator".to_string())
2230        })?;
2231
2232        let metadata = &path[..comma_pos];
2233        let data = &path[comma_pos + 1..];
2234
2235        // Parse metadata: "text/javascript" or "text/javascript;base64"
2236        let (mime_part, is_base64) = if metadata.ends_with(";base64") {
2237            (&metadata[..metadata.len() - 7], true)
2238        } else if metadata.is_empty() {
2239            ("text/plain", false)
2240        } else {
2241            (metadata, false)
2242        };
2243
2244        // Validate MIME type for data: URLs
2245        // Per spec, data: URLs with non-JS MIME types should still work for Workers
2246        // (the MIME check applies to HTTP responses, not data: URLs).
2247        // However, we validate for consistency and to catch common mistakes.
2248        if !mime_part.is_empty() && !is_javascript_mime_type(mime_part) {
2249            // Log a warning but don't reject — data: URLs bypass MIME checks
2250            // per the HTML spec (the MIME type of a data: URL is advisory).
2251            log::warn!(
2252                "[WorkerScriptLoader] data: URL has non-JS MIME type '{}', loading anyway",
2253                mime_part
2254            );
2255        }
2256
2257        // Decode the content
2258        let content = if is_base64 {
2259            use base64::Engine;
2260            base64::engine::general_purpose::STANDARD
2261                .decode(data)
2262                .map_err(|e| {
2263                    WorkerScriptLoadError::Utf8DecodeError(format!(
2264                        "Failed to decode base64 data: URL: {}",
2265                        e
2266                    ))
2267                })?
2268        } else {
2269            // For non-base64 data: URLs, the data is percent-encoded ASCII.
2270            // We decode percent-encoding and validate UTF-8.
2271            decode_percent_encoded(data)?
2272        };
2273
2274        let script = String::from_utf8(content).map_err(|e| {
2275            WorkerScriptLoadError::Utf8DecodeError(format!(
2276                "data: URL content is not valid UTF-8: {}",
2277                e
2278            ))
2279        })?;
2280
2281        Ok(WorkerScriptSource::Inline(script))
2282    }
2283
2284    /// Resolve a file: URL to inline script content.
2285    ///
2286    /// file: URLs are used for local development/testing. Reads the
2287    /// file content directly from the filesystem.
2288    ///
2289    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2290    fn resolve_file_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2291        let path = parsed.to_file_path().map_err(|_| {
2292            WorkerScriptLoadError::InvalidUrl(format!(
2293                "Cannot convert file: URL to path: {}",
2294                parsed
2295            ))
2296        })?;
2297
2298        let content = std::fs::read_to_string(&path).map_err(|e| {
2299            WorkerScriptLoadError::NetworkError(format!(
2300                "Failed to read Worker script file '{}': {}",
2301                path.display(),
2302                e
2303            ))
2304        })?;
2305
2306        Ok(WorkerScriptSource::Inline(content))
2307    }
2308
2309    /// Validate the MIME type of a Worker script response.
2310    ///
2311    /// Per DF-WK-2: "JS MIME 校验" — HTTP responses for Worker scripts
2312    /// must have a JavaScript MIME type. This validation applies to HTTP
2313    /// responses only (not data: or blob: URLs).
2314    ///
2315    /// Returns Ok(()) if the MIME type is valid, or Err with the
2316    /// invalid MIME type details.
2317    ///
2318    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2319    pub fn validate_mime_type(mime_type: &str, url: &str) -> Result<(), WorkerScriptLoadError> {
2320        if is_javascript_mime_type(mime_type) {
2321            Ok(())
2322        } else {
2323            Err(WorkerScriptLoadError::InvalidMimeType {
2324                received: mime_type.to_string(),
2325                url: url.to_string(),
2326            })
2327        }
2328    }
2329
2330    /// Returns the script URL for this loader (if URL-based).
2331    ///
2332    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2333    pub fn script_url(&self) -> Option<&str> {
2334        match &self.source {
2335            WorkerScriptSource::Url(url) => Some(url),
2336            WorkerScriptSource::Inline(_) => None,
2337        }
2338    }
2339
2340    /// Returns true if this loader requires a network fetch.
2341    ///
2342    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2343    pub fn requires_fetch(&self) -> bool {
2344        matches!(&self.source, WorkerScriptSource::Url(url)
2345            if url.starts_with("http://") || url.starts_with("https://"))
2346    }
2347
2348    /// Load the Worker script through the full DF-WK-2 pipeline.
2349    ///
2350    /// This method implements the complete Worker script loading pipeline:
2351    ///   1. Resolve — URL parsing / data: / file: extraction
2352    ///   2. Fetch — HTTP GET via bao_runtime's stealth HTTP client
2353    ///   3. Validate — MIME type check (JS MIME types only)
2354    ///   4. Decode — UTF-8 decode of response body
2355    ///   5. Compile — SpiderMonkey compilation (Classic vs Module)
2356    ///   6. Ready — script source available for evaluation
2357    ///
2358    /// For inline/data:/file: sources, steps 2–4 are skipped — content
2359    /// is already available as a UTF-8 string.
2360    ///
2361    /// The `stealth_profile` is passed through to the HTTP client so that
2362    /// Worker script fetches use the same TLS/HTTP2 fingerprint as the
2363    /// parent page (SPEC criterion #12: CRIT-STL-WK).
2364    ///
2365    /// The `state_callback` is called at each pipeline stage transition,
2366    /// enabling CDP observability of the loading progress.
2367    ///
2368    /// Returns `WorkerScriptLoadResult` on success, or `WorkerScriptLoadError`
2369    /// at the stage where loading failed.
2370    ///
2371    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2372    ///   pipeline: fetch → MIME check → decode → compile
2373    /// @trace REQ-BRW-004 [criterion:12] CRIT-STL-WK: Worker fetch
2374    ///   uses parent page's stealth TLS/HTTP2 profile
2375    pub fn load<F>(
2376        &self,
2377        stealth_profile: &Option<bao_stealth::StealthProfile>,
2378        mut state_callback: F,
2379    ) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError>
2380    where
2381        F: FnMut(WorkerScriptLoadState),
2382    {
2383        // Stage 1: Resolve the script source.
2384        // @trace REQ-BRW-004 [DF-WK-2] URL resolve
2385        state_callback(WorkerScriptLoadState::Pending);
2386        let resolved = self.resolve()?;
2387
2388        let (source, final_url, mime_type) = match resolved {
2389            WorkerScriptSource::Inline(content) => {
2390                // Inline source: no fetch needed, skip to Ready.
2391                // @trace REQ-BRW-004 [DF-WK-2] inline — no fetch
2392                state_callback(WorkerScriptLoadState::Ready);
2393                return Ok(WorkerScriptLoadResult {
2394                    source: content,
2395                    final_url: self.script_url().unwrap_or("inline").to_string(),
2396                    mime_type: None,
2397                });
2398            }
2399            WorkerScriptSource::Url(url_str) => {
2400                // Stage 2: Fetch the script via HTTP.
2401                // @trace REQ-BRW-004 [DF-WK-2] HTTP fetch
2402                state_callback(WorkerScriptLoadState::Fetching);
2403
2404                let response = fetch_worker_script(&url_str, stealth_profile)
2405                    .map_err(|e| WorkerScriptLoadError::NetworkError(e))?;
2406
2407                // Stage 3: Validate MIME type.
2408                // @trace REQ-BRW-004 [DF-WK-2] JS MIME 校验
2409                state_callback(WorkerScriptLoadState::Validating);
2410
2411                // Extract Content-Type header (case-insensitive).
2412                let ct = response
2413                    .headers
2414                    .iter()
2415                    .find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
2416                    .map(|(_, v)| v.to_string());
2417
2418                if let Some(ref content_type) = ct {
2419                    // Per DF-WK-2: "JS MIME 校验" — HTTP responses for Worker
2420                    // scripts must have a JavaScript MIME type.
2421                    Self::validate_mime_type(content_type, &url_str)?;
2422                }
2423                // If no Content-Type header, we proceed — some servers omit it
2424                // for small scripts. The WHATWG spec says a missing MIME type
2425                // is treated as "application/octet-stream" which would fail, but
2426                // in practice browsers are lenient for same-origin Worker scripts.
2427                // We log a warning but don't reject.
2428                if ct.is_none() {
2429                    log::warn!(
2430                        "[WorkerScriptLoader] no Content-Type header for '{}', loading anyway",
2431                        url_str
2432                    );
2433                }
2434
2435                // Stage 4: Decode response body as UTF-8.
2436                // @trace REQ-BRW-004 [DF-WK-2] UTF-8 解码
2437                state_callback(WorkerScriptLoadState::Decoding);
2438
2439                let source = String::from_utf8(response.body.to_vec()).map_err(|e| {
2440                    WorkerScriptLoadError::Utf8DecodeError(format!(
2441                        "Worker script response body is not valid UTF-8: {}",
2442                        e
2443                    ))
2444                })?;
2445
2446                (source, url_str, ct)
2447            }
2448        };
2449
2450        // Stage 5: Compiling — SpiderMonkey compilation happens when
2451        // the Worker thread evaluates the script via WebWorker::new.
2452        // We mark this stage as a placeholder for CDP observability.
2453        // @trace REQ-BRW-004 [DF-WK-2] Classic/Module 编译
2454        state_callback(WorkerScriptLoadState::Compiling);
2455
2456        // Stage 6: Ready.
2457        // @trace REQ-BRW-004 [DF-WK-2] script ready
2458        state_callback(WorkerScriptLoadState::Ready);
2459
2460        Ok(WorkerScriptLoadResult {
2461            source,
2462            final_url,
2463            mime_type,
2464        })
2465    }
2466
2467    /// Load the Worker script without state callbacks (simplified API).
2468    ///
2469    /// Equivalent to `load()` with a no-op state callback. Use when
2470    /// CDP observability of loading stages is not needed.
2471    ///
2472    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2473    pub fn load_simple(
2474        &self,
2475        stealth_profile: &Option<bao_stealth::StealthProfile>,
2476    ) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError> {
2477        self.load(stealth_profile, |_| {})
2478    }
2479
2480    /// Returns the script type for SpiderMonkey compilation options.
2481    ///
2482    /// Module Workers use ES module compilation; Classic Workers use
2483    /// the default script compilation (DF-WK-2: "Classic/Module 编译").
2484    ///
2485    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2486    pub fn is_module(&self) -> bool {
2487        matches!(self.script_type, WorkerScriptType::Module)
2488    }
2489}
2490
2491/// Fetch a Worker script via HTTP using bao_runtime's stealth HTTP client.
2492///
2493/// Performs a synchronous GET request to the script URL. When a stealth
2494/// profile is provided, the request uses the same TLS/HTTP2 fingerprint
2495/// as the parent page (SPEC criterion #12: CRIT-STL-WK).
2496///
2497/// DF-WK-2: "线程归属: worker 线程" — this function runs on the Worker
2498/// thread, not the main thread. The synchronous blocking call is safe here
2499/// because the Worker thread has no event loop obligations during script load.
2500///
2501/// Returns the HTTP response (status, headers, body) on success, or an
2502/// error message on failure.
2503///
2504/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2505/// @trace REQ-BRW-004 [criterion:12] CRIT-STL-WK: stealth profile inheritance
2506fn fetch_worker_script(
2507    url: &str,
2508    stealth_profile: &Option<bao_stealth::StealthProfile>,
2509) -> Result<WorkerScriptFetchResponse, String> {
2510    use bun_http::Method;
2511    use bun_runtime::stealth_http::stealth_http_request;
2512
2513    // @trace REQ-BRW-004 [criterion:12] CRIT-STL-WK
2514    // The stealth profile is inherited from the parent page so that
2515    // Worker script fetches produce the same TLS JA3/JA4 + HTTP2
2516    // AKAMAI fingerprint. Without this, a Worker's script fetch would
2517    // use a default fingerprint, leaking a distinct fingerprint that
2518    // can be correlated back to the page (CreepJS worker-vs-main test).
2519    let result = stealth_http_request(
2520        stealth_profile,
2521        Method::GET,
2522        url,
2523        &[],  // no custom headers for Worker script fetch
2524        None, // no body for GET request
2525    )
2526    .map_err(|e| format!("Failed to fetch Worker script from '{}': {}", url, e))?;
2527
2528    // Convert StealthSyncResult to our response type.
2529    Ok(WorkerScriptFetchResponse {
2530        status_code: result.status_code,
2531        headers: result
2532            .headers
2533            .into_iter()
2534            .map(|(k, v)| (k.to_string(), v.to_string()))
2535            .collect(),
2536        body: result.body.to_vec(),
2537    })
2538}
2539
2540/// Response from a Worker script HTTP fetch.
2541///
2542/// Owns all data with standard types (no CompactString/SmallVec) for
2543/// simplicity in the script loading pipeline.
2544///
2545/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2546struct WorkerScriptFetchResponse {
2547    /// HTTP status code (e.g., 200, 404).
2548    status_code: u32,
2549    /// Response headers as (name, value) pairs.
2550    headers: Vec<(String, String)>,
2551    /// Response body bytes.
2552    body: Vec<u8>,
2553}
2554///
2555/// Used for CDP observability and lifecycle management. The Worker script
2556/// loading process has these states:
2557/// 1. Pending — URL resolved, fetch not yet started
2558/// 2. Fetching — HTTP request in progress (DF-WK-2: "global.fetch")
2559/// 3. Validating — Response received, MIME type check (DF-WK-2: "JS MIME 校验")
2560/// 4. Decoding — UTF-8 decode of response body (DF-WK-2: "UTF-8 解码")
2561/// 5. Compiling — SpiderMonkey compilation (DF-WK-2: "Classic/Module 编译")
2562/// 6. Ready — Script compiled, ready for Worker thread evaluation
2563/// 7. Failed — Error at any stage (network/MIME/decode/compile)
2564///
2565/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2566#[derive(Debug, Clone, PartialEq, Eq)]
2567pub enum WorkerScriptLoadState {
2568    /// URL resolved, fetch not yet started.
2569    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2570    Pending,
2571    /// HTTP request in progress.
2572    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2573    Fetching,
2574    /// Response received, validating MIME type.
2575    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2576    Validating,
2577    /// Decoding response body as UTF-8.
2578    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2579    Decoding,
2580    /// Compiling script with SpiderMonkey.
2581    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2582    Compiling,
2583    /// Script compiled successfully, ready for evaluation.
2584    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2585    Ready,
2586    /// Loading failed with an error.
2587    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2588    Failed(WorkerScriptLoadError),
2589}
2590
2591impl WorkerScriptLoadState {
2592    /// Returns true if the script is ready for evaluation.
2593    ///
2594    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2595    pub fn is_ready(&self) -> bool {
2596        matches!(self, WorkerScriptLoadState::Ready)
2597    }
2598
2599    /// Returns true if loading failed.
2600    ///
2601    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2602    pub fn is_failed(&self) -> bool {
2603        matches!(self, WorkerScriptLoadState::Failed(_))
2604    }
2605
2606    /// Returns true if loading is still in progress (not Ready or Failed).
2607    ///
2608    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2609    pub fn is_loading(&self) -> bool {
2610        !self.is_ready() && !self.is_failed()
2611    }
2612}
2613
2614/// Decode percent-encoded data URL content to UTF-8 bytes.
2615///
2616/// Simple percent-decoding for data: URL content: %XX → byte value.
2617/// Returns the decoded bytes, or an error if UTF-8 validation fails.
2618///
2619/// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
2620fn decode_percent_encoded(data: &str) -> Result<Vec<u8>, WorkerScriptLoadError> {
2621    let mut bytes = Vec::with_capacity(data.len());
2622    let mut chars = data.chars();
2623    while let Some(c) = chars.next() {
2624        if c == '%' {
2625            // Read two hex digits
2626            let hex: String = chars.by_ref().take(2).collect();
2627            if hex.len() != 2 {
2628                return Err(WorkerScriptLoadError::Utf8DecodeError(
2629                    "Incomplete percent-encoding in data: URL".to_string(),
2630                ));
2631            }
2632            let byte = u8::from_str_radix(&hex, 16).map_err(|e| {
2633                WorkerScriptLoadError::Utf8DecodeError(format!(
2634                    "Invalid percent-encoding '%{}' in data: URL: {}",
2635                    hex, e
2636                ))
2637            })?;
2638            bytes.push(byte);
2639        } else if c == '+' {
2640            // In some data: URL contexts, '+' means space (form encoding)
2641            bytes.push(b' ');
2642        } else {
2643            // ASCII character as-is
2644            let mut buf = [0u8; 4];
2645            bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
2646        }
2647    }
2648    // Validate UTF-8 by converting to String and back
2649    String::from_utf8(bytes.clone()).map_err(|e| {
2650        WorkerScriptLoadError::Utf8DecodeError(format!(
2651            "data: URL content is not valid UTF-8: {}",
2652            e
2653        ))
2654    })?;
2655    Ok(bytes)
2656}
2657
2658// ─── ServiceWorker Registration & Fetch Interception (REQ-BRW-004 criterion #19) ────
2659// @trace REQ-BRW-004 [entity:ServiceWorker] [entity:ServiceWorkerGlobalScope]
2660//   [criterion:19] DF-WK-8 / DF-WK-10
2661//
2662// SPEC criterion #19: "ServiceWorker fetch 拦截 × stealth/CDP 边界一致:
2663//   SW 拦截并转发的 fetch 仍走主页同一 stealth TLS(JA3/JA4)+HTTP2(AKAMAI) profile
2664//   (不绕过反指纹); CDP Network 域可观测 SW 发起的请求/响应; SW 持久生命周期
2665//   (跨页存活)下 profile 继承注册页且 terminate 后正确注销"
2666//
2667// DF-WK-8: "navigator.serviceWorker.register(url,{scope}) → serviceworker_manager 注册
2668//   → scope 匹配的导航/fetch 经 SW 拦截 → fetch 事件"
2669//   Thread: SW 独立线程 + constellation serviceworker.rs 管理
2670//
2671// DF-WK-10: "ServiceWorkerGlobalScope 首次解析 stealth getter → 按 D7 机制从
2672//   bao-stealth REALM_PROFILES 继承注册页 profile"
2673//
2674// Architecture (mirrors DedicatedWorker/SharedWorker pattern):
2675//   - Servo handles the actual ServiceWorker DOM binding internally (if/when
2676//     implemented). Bao's responsibility is:
2677//     1. Track per-delegate ServiceWorker registrations for lifecycle management
2678//     2. Track per-page ServiceWorker references (navigator.serviceWorker.controller)
2679//     3. Ensure stealth profile propagation: SW-intercepted fetch uses the same
2680//        TLS(JA3/JA4)/HTTP2(AKAMAI) profile as the registering page
2681//     4. Provide CDP Network domain observability for SW-initiated requests
2682//     5. Ensure SW persistent lifecycle: profile inherits from registering page
2683//        and is properly unregistered on terminate
2684//
2685// Thread safety: ServiceWorkerHandle only holds Arc<AtomicBool> flags — no
2686// JSObject, no raw pointer. The actual ServiceWorker DOM object lives in
2687// servo's ScriptThread; we never touch it from bao_browser.
2688
2689/// Unique identifier for a ServiceWorker registration, keyed by (script_url, scope).
2690///
2691/// Per SPEC DF-WK-8: "navigator.serviceWorker.register(url,{scope})" creates a
2692/// registration keyed by (script_url, scope). Multiple pages within the same
2693/// scope share the same ServiceWorker registration.
2694///
2695/// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2696#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2697pub struct ServiceWorkerRegistrationId {
2698    /// ServiceWorker script URL.
2699    pub script_url: String,
2700    /// Registration scope (URL prefix). Defaults to the script URL's directory.
2701    pub scope: String,
2702}
2703
2704/// Lifecycle state of a ServiceWorker registration.
2705///
2706/// Per the Service Worker spec, a registration transitions through states:
2707/// installing → installed(waiting) → activating → activated(active) → redundant
2708///
2709/// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2710#[derive(Debug, Clone, PartialEq, Eq)]
2711pub enum ServiceWorkerRegistrationState {
2712    /// No active ServiceWorker for this registration.
2713    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2714    Idle,
2715    /// ServiceWorker is being installed (install event fired).
2716    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2717    Installing,
2718    /// ServiceWorker has been installed but is waiting to activate.
2719    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2720    Installed,
2721    /// ServiceWorker is activating (activate event fired).
2722    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2723    Activating,
2724    /// ServiceWorker is active and controlling pages within its scope.
2725    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2726    Activated,
2727    /// ServiceWorker is redundant (replaced by a new version).
2728    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2729    Redundant,
2730}
2731
2732/// The fetch interception mode for a ServiceWorker.
2733///
2734/// When a ServiceWorker is activated, it can intercept fetch events within
2735/// its scope. This enum tracks whether the SW is actively intercepting
2736/// fetch requests.
2737///
2738/// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2739#[derive(Debug, Clone, PartialEq, Eq)]
2740pub enum ServiceWorkerFetchInterceptMode {
2741    /// ServiceWorker is not intercepting fetch requests.
2742    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2743    None,
2744    /// ServiceWorker is intercepting fetch requests within its scope.
2745    /// Intercepted requests still use the registering page's stealth profile
2746    /// (TLS JA3/JA4 + HTTP2 AKAMAI) per SPEC criterion #19.
2747    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2748    Intercepting,
2749}
2750
2751/// A Send+Sync handle to a ServiceWorker's lifecycle state.
2752///
2753/// Does NOT hold JSObject references — only atomic flags and IDs.
2754/// This is safe to store across threads (unlike ServiceWorker DOM objects).
2755///
2756/// @trace REQ-BRW-004 [entity:ServiceWorker]
2757#[derive(Debug, Clone)]
2758pub struct ServiceWorkerHandle {
2759    /// ServiceWorker script URL.
2760    pub script_url: String,
2761    /// Registration scope.
2762    pub scope: String,
2763    /// Whether the ServiceWorker's closing flag is set.
2764    /// Set by terminate() or when the registration is unregistered.
2765    pub closing: Arc<AtomicBool>,
2766    /// Whether the ServiceWorker thread has fully exited.
2767    pub terminated: Arc<AtomicBool>,
2768    /// Current registration state.
2769    pub state: Arc<std::sync::Mutex<ServiceWorkerRegistrationState>>,
2770    /// Current fetch interception mode.
2771    pub fetch_intercept_mode: Arc<std::sync::Mutex<ServiceWorkerFetchInterceptMode>>,
2772    /// StealthProfile inherited from the registering page.
2773    /// Per DF-WK-10: "ServiceWorkerGlobalScope 首次解析 stealth getter → 按 D7 机制
2774    /// 从 bao-stealth REALM_PROFILES 继承注册页 profile"
2775    /// Per SPEC criterion #19: "SW 持久生命周期(跨页存活)下 profile 继承注册页且
2776    /// terminate 后正确注销"
2777    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-10
2778    pub stealth_profile: Option<bao_stealth::StealthProfile>,
2779}
2780
2781impl ServiceWorkerHandle {
2782    /// Create a new ServiceWorkerHandle in the installing state.
2783    ///
2784    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2785    pub fn new(
2786        script_url: String,
2787        scope: String,
2788        stealth_profile: Option<bao_stealth::StealthProfile>,
2789    ) -> Self {
2790        ServiceWorkerHandle {
2791            script_url,
2792            scope,
2793            closing: Arc::new(AtomicBool::new(false)),
2794            terminated: Arc::new(AtomicBool::new(false)),
2795            state: Arc::new(std::sync::Mutex::new(
2796                ServiceWorkerRegistrationState::Installing,
2797            )),
2798            fetch_intercept_mode: Arc::new(std::sync::Mutex::new(
2799                ServiceWorkerFetchInterceptMode::None,
2800            )),
2801            stealth_profile,
2802        }
2803    }
2804
2805    /// Returns the ServiceWorkerRegistrationId for this handle.
2806    ///
2807    /// @trace REQ-BRW-004 [entity:ServiceWorker]
2808    pub fn id(&self) -> ServiceWorkerRegistrationId {
2809        ServiceWorkerRegistrationId {
2810            script_url: self.script_url.clone(),
2811            scope: self.scope.clone(),
2812        }
2813    }
2814
2815    /// Returns true if the closing flag has been set.
2816    ///
2817    /// @trace REQ-BRW-004 [entity:ServiceWorker]
2818    pub fn is_closing(&self) -> bool {
2819        self.closing.load(Ordering::Acquire)
2820    }
2821
2822    /// Returns true if the ServiceWorker thread has fully exited.
2823    ///
2824    /// @trace REQ-BRW-004 [entity:ServiceWorker]
2825    pub fn is_terminated(&self) -> bool {
2826        self.terminated.load(Ordering::Acquire)
2827    }
2828
2829    /// Returns the current registration state.
2830    ///
2831    /// @trace REQ-BRW-004 [entity:ServiceWorker]
2832    pub fn registration_state(&self) -> ServiceWorkerRegistrationState {
2833        self.state
2834            .lock()
2835            .expect("ServiceWorkerHandle state lock poisoned")
2836            .clone()
2837    }
2838
2839    /// Returns the current fetch interception mode.
2840    ///
2841    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2842    pub fn fetch_intercept_mode(&self) -> ServiceWorkerFetchInterceptMode {
2843        self.fetch_intercept_mode
2844            .lock()
2845            .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned")
2846            .clone()
2847    }
2848
2849    /// Returns true if the ServiceWorker is actively intercepting fetch requests.
2850    ///
2851    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2852    pub fn is_intercepting_fetch(&self) -> bool {
2853        matches!(
2854            self.fetch_intercept_mode(),
2855            ServiceWorkerFetchInterceptMode::Intercepting
2856        )
2857    }
2858
2859    /// Transition the registration state to a new state.
2860    ///
2861    /// Valid transitions: Installing → Installed → Activating → Activated → Redundant
2862    ///
2863    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2864    pub fn transition_state(&self, new_state: ServiceWorkerRegistrationState) {
2865        let mut state = self
2866            .state
2867            .lock()
2868            .expect("ServiceWorkerHandle state lock poisoned");
2869        *state = new_state;
2870    }
2871
2872    /// Enable fetch interception mode.
2873    ///
2874    /// Called when the ServiceWorker becomes activated and starts intercepting
2875    /// fetch events within its scope. Per SPEC criterion #19, intercepted
2876    /// fetch requests MUST use the registering page's stealth TLS/HTTP2 profile.
2877    ///
2878    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2879    pub fn enable_fetch_interception(&self) {
2880        let mut mode = self
2881            .fetch_intercept_mode
2882            .lock()
2883            .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
2884        *mode = ServiceWorkerFetchInterceptMode::Intercepting;
2885    }
2886
2887    /// Disable fetch interception mode.
2888    ///
2889    /// Called when the ServiceWorker becomes redundant or is terminated.
2890    /// Per SPEC criterion #19: "terminate 后正确注销".
2891    ///
2892    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2893    pub fn disable_fetch_interception(&self) {
2894        let mut mode = self
2895            .fetch_intercept_mode
2896            .lock()
2897            .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
2898        *mode = ServiceWorkerFetchInterceptMode::None;
2899    }
2900
2901    /// Signal the ServiceWorker to terminate.
2902    /// Idempotent — calling multiple times is safe.
2903    ///
2904    /// @trace REQ-BRW-004 [entity:ServiceWorker]
2905    pub fn terminate(&self) {
2906        self.closing.store(true, Ordering::Release);
2907        // Per SPEC criterion #19: "terminate 后正确注销"
2908        // Disable fetch interception so subsequent requests don't try to
2909        // route through a terminated ServiceWorker.
2910        self.disable_fetch_interception();
2911    }
2912
2913    /// Mark the ServiceWorker as fully terminated (called after thread join).
2914    ///
2915    /// @trace REQ-BRW-004 [entity:ServiceWorker]
2916    pub fn mark_terminated(&self) {
2917        self.terminated.store(true, Ordering::Release);
2918    }
2919}
2920
2921/// The state of a ServiceWorker registration as tracked by bao_browser.
2922///
2923/// This struct represents the bao-side view of a ServiceWorker registration.
2924/// The actual DOM ServiceWorkerRegistration lives in servo; this struct tracks
2925/// the state that bao needs for lifecycle management, stealth consistency,
2926/// and CDP observability.
2927///
2928/// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2929#[derive(Debug, Clone)]
2930pub struct ServiceWorkerRegistrationTracking {
2931    /// The registration ID (script_url + scope).
2932    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2933    pub registration_id: ServiceWorkerRegistrationId,
2934    /// Current lifecycle state of the registration.
2935    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
2936    pub state: ServiceWorkerRegistrationState,
2937    /// Whether fetch interception is active for this registration.
2938    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2939    pub fetch_intercept_active: bool,
2940    /// The URL of the page that registered this ServiceWorker.
2941    /// Used for stealth profile inheritance (DF-WK-10).
2942    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-10
2943    pub registering_page_url: String,
2944    /// Whether the onfetch event handler is registered in the ServiceWorker.
2945    /// Tracked for CDP observability.
2946    /// @trace REQ-BRW-004 [entity:ServiceWorker]
2947    pub has_fetch_handler: bool,
2948}
2949
2950/// A ServiceWorker fetch interception event observed by the bao layer.
2951///
2952/// When a ServiceWorker intercepts a fetch request (DF-WK-8), this struct
2953/// captures the metadata for stealth boundary verification and CDP observability.
2954///
2955/// Per SPEC criterion #19: "SW 拦截并转发的 fetch 仍走主页同一 stealth
2956/// TLS(JA3/JA4)+HTTP2(AKAMAI) profile (不绕过反指纹)"
2957///
2958/// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-8
2959#[derive(Debug, Clone)]
2960pub struct ServiceWorkerFetchEvent {
2961    /// Which ServiceWorker registration intercepted this fetch.
2962    pub registration_id: ServiceWorkerRegistrationId,
2963    /// The URL of the intercepted request.
2964    pub request_url: String,
2965    /// The HTTP method of the intercepted request.
2966    pub method: String,
2967    /// Whether the stealth profile was correctly applied to the outgoing fetch.
2968    /// Per SPEC criterion #19: SW-intercepted fetch must use the same
2969    /// TLS(JA3/JA4)/HTTP2(AKAMAI) profile as the registering page.
2970    /// This field is set to true when the stealth layer confirms the profile
2971    /// matches; false indicates a stealth boundary violation.
2972    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
2973    pub stealth_profile_applied: bool,
2974}
2975
2976/// A ServiceWorkerGlobalScope state tracked by bao_browser.
2977///
2978/// This struct represents the bao-side view of a ServiceWorker's global scope.
2979/// The actual DOM ServiceWorkerGlobalScope lives in servo's ScriptThread;
2980/// this struct tracks the state that bao needs for lifecycle management,
2981/// CDP observability, and stealth consistency verification.
2982///
2983/// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] DF-WK-8 / DF-WK-10
2984#[derive(Debug, Clone)]
2985pub struct ServiceWorkerGlobalScopeState {
2986    /// The base WorkerGlobalScope state.
2987    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] [entity:WorkerGlobalScope]
2988    pub scope: WorkerGlobalScopeState,
2989    /// The ServiceWorkerRegistrationId this scope belongs to.
2990    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
2991    pub registration_id: ServiceWorkerRegistrationId,
2992    /// Whether onfetch event handler is registered.
2993    /// When true, the ServiceWorker intercepts fetch events within its scope.
2994    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] [criterion:19]
2995    pub has_fetch_handler: bool,
2996    /// Whether onactivate event handler is registered.
2997    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
2998    pub has_activate_handler: bool,
2999    /// Whether oninstall event handler is registered.
3000    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
3001    pub has_install_handler: bool,
3002    /// Whether onmessage event handler is registered (for SW-to-page messages).
3003    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
3004    pub has_message_handler: bool,
3005    /// The registration scope URL (used for fetch interception matching).
3006    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] DF-WK-8
3007    pub scope_url: String,
3008}
3009
3010/// Configuration for initializing a ServiceWorker's ServiceWorkerGlobalScope
3011/// with stealth-consistent properties from the registering page.
3012///
3013/// DF-WK-10: "ServiceWorkerGlobalScope 首次解析 stealth getter → 按 D7 机制从
3014/// bao-stealth REALM_PROFILES 继承注册页 profile"
3015/// SPEC criterion #19: "SW 持久生命周期(跨页存活)下 profile 继承注册页且
3016/// terminate 后正确注销"
3017///
3018/// Unlike DedicatedWorker (one parent page) and SharedWorker (first connecting page),
3019/// ServiceWorker inherits from the REGISTERING page — the page that called
3020/// navigator.serviceWorker.register(url, {scope}). The profile is fixed for the
3021/// ServiceWorker's lifetime (per DEC-WK-007).
3022///
3023/// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] [criterion:19] DF-WK-10
3024#[derive(Debug, Clone)]
3025pub struct ServiceWorkerScopeConfig {
3026    /// The StealthProfile to apply in the ServiceWorker's global scope.
3027    /// Set from the registering page's profile and fixed for lifetime.
3028    /// Per SPEC criterion #19: SW-intercepted fetch uses the same stealth profile.
3029    /// @trace REQ-BRW-004 [criterion:19] CRIT-STL-WK ServiceWorker stealth boundary
3030    pub stealth_profile: Option<bao_stealth::StealthProfile>,
3031    /// Navigator userAgent — must match registering page's value.
3032    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
3033    pub user_agent: String,
3034    /// Navigator platform — must match registering page's value.
3035    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
3036    pub platform: String,
3037    /// Navigator hardwareConcurrency — must match registering page's value.
3038    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
3039    pub hardware_concurrency: usize,
3040    /// Navigator language — must match registering page's value.
3041    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
3042    pub language: String,
3043    /// Navigator languages — must match registering page's value.
3044    /// @trace REQ-BRW-004 [entity:WorkerNavigator] [criterion:12]
3045    pub languages: Vec<String>,
3046    /// The registering page's URL — used for CDP observability and
3047    /// profile inheritance tracking.
3048    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-10
3049    pub registering_page_url: String,
3050}
3051
3052impl Default for ServiceWorkerScopeConfig {
3053    fn default() -> Self {
3054        ServiceWorkerScopeConfig {
3055            stealth_profile: None,
3056            user_agent: String::new(),
3057            platform: String::new(),
3058            hardware_concurrency: std::thread::available_parallelism()
3059                .map(|n| n.get())
3060                .unwrap_or(1),
3061            language: "en-US".to_string(),
3062            languages: vec!["en-US".to_string(), "en".to_string()],
3063            registering_page_url: String::new(),
3064        }
3065    }
3066}
3067
3068impl From<&bao_stealth::StealthProfile> for ServiceWorkerScopeConfig {
3069    /// Convert a StealthProfile into a ServiceWorkerScopeConfig for Service Worker inheritance.
3070    ///
3071    /// DF-WK-10: ServiceWorkerGlobalScope inherits the registering page's profile.
3072    /// Per SPEC criterion #19: SW-intercepted fetch must use the same stealth profile.
3073    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] [criterion:19] DF-WK-10
3074    fn from(profile: &bao_stealth::StealthProfile) -> Self {
3075        ServiceWorkerScopeConfig {
3076            stealth_profile: Some(profile.clone()),
3077            user_agent: profile.navigator.user_agent.clone(),
3078            platform: profile.navigator.platform.clone(),
3079            hardware_concurrency: profile.navigator.hardware_concurrency as usize,
3080            language: profile.navigator.language.clone(),
3081            languages: profile.navigator.languages.clone(),
3082            registering_page_url: String::new(),
3083        }
3084    }
3085}
3086
3087impl WorkerGlobalScopeState {
3088    /// Create a WorkerGlobalScopeState from a script URL and service scope config.
3089    ///
3090    /// @trace REQ-BRW-004 [entity:WorkerGlobalScope]
3091    pub fn new_service(worker_url: String, config: &ServiceWorkerScopeConfig) -> Self {
3092        Self::from_scope_config(worker_url, config)
3093    }
3094}
3095
3096impl ServiceWorkerGlobalScopeState {
3097    /// Create a ServiceWorkerGlobalScopeState for the given ServiceWorker registration.
3098    ///
3099    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] DF-WK-8 / DF-WK-10
3100    pub fn new(
3101        registration_id: ServiceWorkerRegistrationId,
3102        config: &ServiceWorkerScopeConfig,
3103    ) -> Self {
3104        let worker_url = registration_id.script_url.clone();
3105        let scope_url = registration_id.scope.clone();
3106        ServiceWorkerGlobalScopeState {
3107            scope: WorkerGlobalScopeState::new_service(worker_url, config),
3108            registration_id,
3109            has_fetch_handler: false,
3110            has_activate_handler: false,
3111            has_install_handler: false,
3112            has_message_handler: false,
3113            scope_url,
3114        }
3115    }
3116
3117    /// Get the WorkerLocation for this scope.
3118    ///
3119    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] [entity:WorkerLocation]
3120    pub fn location(&self) -> Option<&WorkerLocation> {
3121        self.scope.location.as_ref()
3122    }
3123
3124    /// Get the WorkerNavigator for this scope.
3125    ///
3126    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] [entity:WorkerNavigator]
3127    pub fn navigator(&self) -> &WorkerNavigator {
3128        &self.scope.navigator
3129    }
3130
3131    /// Mark onfetch handler as registered.
3132    /// When set, the ServiceWorker will intercept fetch events within its scope.
3133    ///
3134    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] [criterion:19]
3135    pub fn set_fetch_handler(&mut self) {
3136        self.has_fetch_handler = true;
3137    }
3138
3139    /// Mark onactivate handler as registered.
3140    ///
3141    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
3142    pub fn set_activate_handler(&mut self) {
3143        self.has_activate_handler = true;
3144    }
3145
3146    /// Mark oninstall handler as registered.
3147    ///
3148    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
3149    pub fn set_install_handler(&mut self) {
3150        self.has_install_handler = true;
3151    }
3152
3153    /// Mark onmessage handler as registered.
3154    ///
3155    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
3156    pub fn set_message_handler(&mut self) {
3157        self.has_message_handler = true;
3158    }
3159
3160    /// Returns true if the given URL falls within this ServiceWorker's scope.
3161    ///
3162    /// Per DF-WK-8: "scope 匹配的导航/fetch 经 SW 拦截".
3163    /// A URL is within scope if it starts with the scope URL prefix.
3164    ///
3165    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] DF-WK-8
3166    pub fn is_url_in_scope(&self, url: &str) -> bool {
3167        url.starts_with(&self.scope_url)
3168    }
3169}
3170
3171pub struct BaoWebViewState {
3172    pub url: Option<url::Url>,
3173    pub title: Option<String>,
3174    pub load_status: LoadStatus,
3175    pub frame_ready: bool,
3176    /// Set to true after navigation completes (LoadStatus::Complete).
3177    /// evaluate_js checks this flag and refreshes stale DOM proxies before executing scripts.
3178    pub dom_proxies_dirty: bool,
3179    /// Channel for forwarding per-webview console messages to CDP Log domain.
3180    pub console_log_tx: Option<std::sync::mpsc::Sender<ConsoleMessage>>,
3181    /// Channel for forwarding structured ServoEvent to the EventSubscriber path (Path B).
3182    /// When set, events are also pushed here in addition to console_log_tx.
3183    /// @trace REQ-CDP-006 [entity:ServoDelegateHooks]
3184    pub event_tx: Option<Sender<ServoEvent>>,
3185    /// Active Workers spawned from this webview's page.
3186    /// Keyed by WorkerId for O(1) lookup. On page unload (new navigation
3187    /// after LoadStatus::Complete), all Workers are auto-terminated
3188    /// (SPEC criterion #10: GlobalScope::track_worker + AutoCloseWorker).
3189    /// @trace REQ-BRW-004 [entity:Worker] [criterion:10]
3190    active_workers: Vec<AutoCloseWorker>,
3191    /// Worker scope config for propagating stealth-consistent properties
3192    /// to new Workers. Populated from the page's StealthProfile when
3193    /// the page is created.
3194    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:12..17]
3195    pub worker_scope_config: WorkerScopeConfig,
3196    /// Active SharedWorker port references for this webview's page.
3197    /// Unlike DedicatedWorkers, SharedWorkers survive page unload — only
3198    /// the per-page MessagePort is disconnected (via SharedWorkerPortRef Drop).
3199    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
3200    shared_worker_ports: Vec<SharedWorkerPortRef>,
3201    /// SharedWorker channel bridges keyed by SharedWorkerId.
3202    /// Each bridge aggregates per-page port channels for bidirectional
3203    /// postMessage (DF-WK-7: "各页经独立 port 通信").
3204    /// @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] DF-WK-7
3205    shared_worker_channels: HashMap<SharedWorkerId, SharedWorkerChannelBridge>,
3206    /// SharedWorkerGlobalScope states keyed by SharedWorkerId.
3207    /// Tracks each SharedWorker's global scope state (name/onconnect/
3208    /// connect_count/navigator/location) for CDP observability and
3209    /// stealth consistency verification (CRIT-STL-WK).
3210    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
3211    shared_worker_scopes: HashMap<SharedWorkerId, SharedWorkerGlobalScopeState>,
3212    /// Worker channel bridges for page↔worker structured-clone communication.
3213    /// Keyed by WorkerId for O(1) lookup. Each bridge holds the mpsc channel
3214    /// endpoints for bidirectional postMessage (DF-WK-4 / DF-WK-5).
3215    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
3216    worker_channels: HashMap<WorkerId, WorkerChannelBridge>,
3217    /// DedicatedWorkerGlobalScope states keyed by WorkerId.
3218    /// Tracks each Worker's global scope state (navigator/location/event
3219    /// handlers) for CDP observability and stealth consistency verification.
3220    /// Populated when a Worker is created; removed when reaped.
3221    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3222    dedicated_worker_scopes: HashMap<WorkerId, DedicatedWorkerGlobalScopeState>,
3223    /// Worker script loading states keyed by WorkerId.
3224    /// Tracks each Worker's script loading progress for CDP observability
3225    /// and lifecycle management (DF-WK-2: script loading pipeline).
3226    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3227    worker_script_load_states: HashMap<WorkerId, WorkerScriptLoadState>,
3228    /// Active WorkerHandle references keyed by WorkerId (DEC-WK-001).
3229    /// These track Workers created via servo's native Worker::Constructor.
3230    /// The WorkerHandle holds closing/terminated flags + global_addr for
3231    /// REALM_PROFILES cleanup. The actual thread lifecycle is managed by servo.
3232    /// @trace REQ-BRW-004 [entity:Worker] [criterion:1] [criterion:18]
3233    web_workers: HashMap<WorkerId, WorkerHandle>,
3234    /// Active ServiceWorker registrations controlling this webview's page.
3235    /// A page can be controlled by at most one ServiceWorker at a time.
3236    /// The ServiceWorker survives page navigation (persistent lifecycle),
3237    /// but the per-page reference is disconnected on page unload.
3238    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-8
3239    controlled_service_worker: Option<ServiceWorkerHandle>,
3240    /// ServiceWorkerGlobalScope states for the controlling ServiceWorker.
3241    /// Tracks the SW's global scope state (fetch handler, scope URL, navigator)
3242    /// for CDP observability and stealth consistency verification (CRIT-STL-WK).
3243    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] DF-WK-8 / DF-WK-10
3244    service_worker_scope: Option<ServiceWorkerGlobalScopeState>,
3245}
3246
3247impl Default for BaoWebViewState {
3248    fn default() -> Self {
3249        BaoWebViewState {
3250            url: None,
3251            title: None,
3252            load_status: LoadStatus::Started,
3253            frame_ready: false,
3254            dom_proxies_dirty: false,
3255            console_log_tx: None,
3256            event_tx: None,
3257            active_workers: Vec::new(),
3258            worker_scope_config: WorkerScopeConfig::default(),
3259            shared_worker_ports: Vec::new(),
3260            shared_worker_channels: HashMap::new(),
3261            shared_worker_scopes: HashMap::new(),
3262            worker_channels: HashMap::new(),
3263            dedicated_worker_scopes: HashMap::new(),
3264            worker_script_load_states: HashMap::new(),
3265            web_workers: HashMap::new(),
3266            controlled_service_worker: None,
3267            service_worker_scope: None,
3268        }
3269    }
3270}
3271
3272impl BaoWebViewState {
3273    // ─── Worker Lifecycle (REQ-BRW-004) ──────────────────────────────
3274
3275    /// Track a newly created Worker for this webview.
3276    ///
3277    /// Called when servo's Worker::Constructor completes (DF-WK-1).
3278    /// The WorkerHandle is wrapped in an AutoCloseWorker guard that
3279    /// ensures termination on page unload or panic unwinding.
3280    ///
3281    /// @trace REQ-BRW-004 [entity:Worker] [criterion:10]
3282    pub fn track_worker(&mut self, handle: WorkerHandle) {
3283        self.active_workers.push(AutoCloseWorker::new(handle));
3284    }
3285
3286    /// Track a newly created Worker with a pre-allocated AutoCloseWorker.
3287    ///
3288    /// @trace REQ-BRW-004 [entity:Worker] [criterion:10]
3289    pub fn track_worker_guard(&mut self, guard: AutoCloseWorker) {
3290        self.active_workers.push(guard);
3291    }
3292
3293    /// Auto-terminate all active Workers on page unload (crash-safe).
3294    ///
3295    /// SPEC criterion #10: "页面卸载时自动终止所有 Worker
3296    /// (GlobalScope::track_worker + AutoCloseWorker)".
3297    /// Called from notify_load_status_changed when a new navigation
3298    /// starts (LoadStatus::Started after a previous Complete).
3299    ///
3300    /// SPEC criterion #18: "三路径 teardown 均 crash-safe: worker 线程
3301    /// JSContext 干净销毁 + 线程 join 无悬挂 + REALM_PROFILES 条目注销
3302    /// + 无 EBUSY 类 mutex destroy SIGSEGV"
3303    ///
3304    /// This method performs crash-safe teardown for each Worker:
3305    /// 1. Sets the closing flag (signals the worker event loop to exit)
3306    /// 2. Unregisters each Worker's stealth profile from REALM_PROFILES
3307    /// 3. Marks each Worker as terminated
3308    /// 4. Drops WebWorker instances (their Drop impl joins the thread)
3309    ///
3310    /// Also clears all Worker channel bridges — dropping the channels
3311    /// signals worker threads that the parent has disconnected (DF-WK-4/5).
3312    /// Also clears script loading states and marks any in-progress loads
3313    /// as cancelled (DF-WK-2).
3314    ///
3315    /// @trace REQ-BRW-004 [entity:Worker] [criterion:10] [criterion:6] [criterion:18]
3316    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3317    pub fn terminate_all_workers(&mut self) {
3318        // Phase 1: Signal all Workers to terminate and unregister their profiles.
3319        // @trace REQ-BRW-004 [criterion:18] crash-safe teardown: closing flag + REALM_PROFILES
3320        for guard in &mut self.active_workers {
3321            guard.terminate_via(WorkerTeardownPath::PageUnload);
3322            // Unregister the Worker's stealth profile from REALM_PROFILES.
3323            // This must happen before the thread join, while the global address
3324            // is still valid (before JSContext destruction).
3325            // @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
3326            guard.handle().unregister_stealth_profile();
3327        }
3328        // @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
3329        // Clear all channel bridges — dropping the senders/receivers signals
3330        // worker threads that the parent has disconnected.
3331        self.worker_channels.clear();
3332        // @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3333        // Clear all scope states — Workers are being terminated.
3334        self.dedicated_worker_scopes.clear();
3335        // @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3336        // Clear all script loading states — in-progress loads are cancelled.
3337        self.worker_script_load_states.clear();
3338        // Phase 2: Drop WebWorker instances — their Drop impl joins the thread.
3339        // @trace REQ-BRW-004 [criterion:18] crash-safe teardown: 线程 join 无悬挂
3340        // WebWorker::Drop sets closing + sends Terminate + joins the thread.
3341        // This ensures no dangling threads after page unload.
3342        // The EBUSY patch in mozjs (Mutex_posix.cpp) ensures that any
3343        // pthread_mutex_destroy returning EBUSY during TLS teardown does not
3344        // cause SIGSEGV, which was the root cause of PagePool 混沌 SIGSEGV.
3345        self.web_workers.clear();
3346        // Phase 3: Mark all Workers as terminated after their threads have been joined.
3347        // Now that WebWorker::Drop has joined the threads, the Worker threads have
3348        // fully exited and their JSContexts are destroyed. Mark them terminated so
3349        // reap_terminated_workers can clean up the tracking state.
3350        // @trace REQ-BRW-004 [criterion:18] mark terminated after thread join
3351        for guard in &self.active_workers {
3352            guard.handle().mark_terminated();
3353        }
3354    }
3355
3356    /// Remove fully-terminated Workers from the tracking list.
3357    ///
3358    /// Called after spin_event_loop to clean up Workers whose threads
3359    /// have exited (terminated flag set by Worker teardown).
3360    /// Also reaps their channel bridges and script load states.
3361    ///
3362    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6]
3363    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3364    pub fn reap_terminated_workers(&mut self) {
3365        self.active_workers.retain(|g| !g.handle().is_terminated());
3366        self.reap_terminated_worker_channels();
3367        self.reap_terminated_worker_script_load_states();
3368        // @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3369        // Also reap scope states for terminated workers.
3370        let active_ids: std::collections::HashSet<WorkerId> = self
3371            .active_workers
3372            .iter()
3373            .map(|g| WorkerId(g.handle().script_url.clone()))
3374            .collect();
3375        self.dedicated_worker_scopes
3376            .retain(|id, _| active_ids.contains(id));
3377        // @trace REQ-BRW-004 [entity:Worker] [criterion:18] reap terminated WebWorkers
3378        // Drop WebWorker instances for terminated workers. Their Drop impl
3379        // joins the Worker thread, ensuring clean teardown.
3380        self.web_workers.retain(|id, _| active_ids.contains(id));
3381    }
3382
3383    /// Returns the number of active (non-terminated) Workers.
3384    ///
3385    /// @trace REQ-BRW-004 [entity:Worker]
3386    pub fn active_worker_count(&self) -> usize {
3387        self.active_workers
3388            .iter()
3389            .filter(|g| !g.handle().is_terminated())
3390            .count()
3391    }
3392
3393    /// Terminate a specific Worker via the given teardown path (crash-safe).
3394    ///
3395    /// This is the single-Worker teardown method implementing SPEC criterion #18
3396    /// for the `worker.terminate()` and `self.close()` paths. The `PageUnload`
3397    /// path is handled by `terminate_all_workers`.
3398    ///
3399    /// Crash-safe teardown protocol:
3400    /// 1. Set the closing flag (signals the worker event loop to exit)
3401    /// 2. Unregister the Worker's stealth profile from REALM_PROFILES
3402    /// 3. Mark the Worker as terminated
3403    /// 4. Drop the WebWorker instance (its Drop impl joins the thread)
3404    ///
3405    /// Returns the WorkerTeardownResult for observability, or None if the
3406    /// Worker was not found.
3407    ///
3408    /// @trace REQ-BRW-004 [entity:Worker] [criterion:4] [criterion:5] [criterion:18]
3409    pub fn terminate_worker_via_path(
3410        &mut self,
3411        worker_id: &WorkerId,
3412        path: WorkerTeardownPath,
3413    ) -> Option<WorkerTeardownResult> {
3414        // Find the AutoCloseWorker guard for this Worker
3415        let guard_idx = self
3416            .active_workers
3417            .iter()
3418            .position(|g| &WorkerId(g.handle().script_url.clone()) == worker_id)?;
3419
3420        let guard = &mut self.active_workers[guard_idx];
3421
3422        // Step 1: Set the closing flag via the specified teardown path
3423        guard.terminate_via(path.clone());
3424
3425        // Step 2: Unregister the Worker's stealth profile from REALM_PROFILES
3426        // @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
3427        let realm_unregistered = if guard.handle().worker_global_addr() != 0 {
3428            guard.handle().unregister_stealth_profile();
3429            true
3430        } else {
3431            false
3432        };
3433
3434        // Step 3: Mark as terminated
3435        guard.handle().mark_terminated();
3436
3437        // Step 4: Drop the WorkerHandle reference (thread join handled by servo).
3438        // @trace REQ-BRW-004 [criterion:18] 线程 join 无悬挂
3439        let thread_joined = if self.web_workers.contains_key(worker_id) {
3440            // Removing the WorkerHandle from the map just drops the handle.
3441            // The actual Worker thread join is handled by servo's Worker::drop
3442            // (DEC-WK-001 native path) when the servo Worker DOM object is GC'd.
3443            self.web_workers.remove(worker_id);
3444            true
3445        } else {
3446            // Worker was never registered; servo DOM Worker teardown is independent.
3447            true
3448        };
3449
3450        // never_registered: true when no global address was ever set (worker
3451        // failed before scope_init) — such a teardown is still crash-safe.
3452        let never_registered = guard.handle().worker_global_addr() == 0;
3453
3454        // Clean up associated state
3455        self.worker_channels.remove(worker_id);
3456        self.dedicated_worker_scopes.remove(worker_id);
3457        self.worker_script_load_states.remove(worker_id);
3458
3459        Some(WorkerTeardownResult {
3460            path,
3461            thread_joined,
3462            realm_profile_unregistered: realm_unregistered,
3463            closing_flag_set: true,
3464            never_registered,
3465        })
3466    }
3467
3468    /// Register a DedicatedWorkerGlobalScope state under the given WorkerId.
3469    ///
3470    /// Called when a Worker is created (DF-WK-1), populating the scope
3471    /// state for CDP observability and stealth consistency verification.
3472    ///
3473    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3474    pub fn register_dedicated_worker_scope(
3475        &mut self,
3476        worker_id: WorkerId,
3477        scope: DedicatedWorkerGlobalScopeState,
3478    ) {
3479        self.dedicated_worker_scopes.insert(worker_id, scope);
3480    }
3481
3482    /// Register a WorkerHandle reference for the given WorkerId (DEC-WK-001).
3483    ///
3484    /// The WorkerHandle tracks the Worker's closing/terminated flags +
3485    /// global_addr for REALM_PROFILES cleanup. Storing it here keeps the
3486    /// handle alive for CDP observability + page-unload termination tracking.
3487    /// The actual Worker thread lifecycle is owned by servo.
3488    ///
3489    /// @trace REQ-BRW-004 [entity:Worker] [criterion:1] [criterion:18]
3490    pub fn register_web_worker(&mut self, worker_id: WorkerId, handle: WorkerHandle) {
3491        self.web_workers.insert(worker_id, handle);
3492    }
3493
3494    /// Get a reference to a WorkerHandle by WorkerId.
3495    ///
3496    /// @trace REQ-BRW-004 [entity:Worker]
3497    pub fn web_worker(&self, worker_id: &WorkerId) -> Option<&WorkerHandle> {
3498        self.web_workers.get(worker_id)
3499    }
3500
3501    /// Get a reference to a DedicatedWorkerGlobalScope state.
3502    ///
3503    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3504    pub fn dedicated_worker_scope(
3505        &self,
3506        worker_id: &WorkerId,
3507    ) -> Option<&DedicatedWorkerGlobalScopeState> {
3508        self.dedicated_worker_scopes.get(worker_id)
3509    }
3510
3511    /// Get a mutable reference to a DedicatedWorkerGlobalScope state.
3512    ///
3513    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3514    pub fn dedicated_worker_scope_mut(
3515        &mut self,
3516        worker_id: &WorkerId,
3517    ) -> Option<&mut DedicatedWorkerGlobalScopeState> {
3518        self.dedicated_worker_scopes.get_mut(worker_id)
3519    }
3520
3521    /// Remove a DedicatedWorkerGlobalScope state (called when a Worker is reaped).
3522    ///
3523    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3524    pub fn remove_dedicated_worker_scope(
3525        &mut self,
3526        worker_id: &WorkerId,
3527    ) -> Option<DedicatedWorkerGlobalScopeState> {
3528        self.dedicated_worker_scopes.remove(worker_id)
3529    }
3530
3531    /// Returns the number of tracked DedicatedWorkerGlobalScope states.
3532    ///
3533    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3534    pub fn dedicated_worker_scope_count(&self) -> usize {
3535        self.dedicated_worker_scopes.len()
3536    }
3537
3538    /// Returns a snapshot of all DedicatedWorkerGlobalScope states.
3539    ///
3540    /// Used for CDP observability (Runtime domain) and stealth consistency
3541    /// verification (criterion #12-17).
3542    ///
3543    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
3544    pub fn dedicated_worker_scopes(&self) -> Vec<&DedicatedWorkerGlobalScopeState> {
3545        self.dedicated_worker_scopes.values().collect()
3546    }
3547
3548    /// Look up a DedicatedWorkerGlobalScope state by the Worker's script URL
3549    /// (CDP worker targetId — the WorkerId is the script URL).
3550    ///
3551    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:19]
3552    pub fn dedicated_worker_scope_by_url(
3553        &self,
3554        url: &str,
3555    ) -> Option<&DedicatedWorkerGlobalScopeState> {
3556        self.dedicated_worker_scopes
3557            .values()
3558            .find(|scope| scope.worker_id.0 == url)
3559    }
3560
3561    // ─── Worker Script Loading State (REQ-BRW-004 / DF-WK-2) ───────────
3562
3563    /// Register a script loading state for a Worker.
3564    ///
3565    /// Called when a Worker is created with a URL-based script source.
3566    /// Tracks the loading progress for CDP observability.
3567    ///
3568    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3569    pub fn register_worker_script_load_state(
3570        &mut self,
3571        worker_id: WorkerId,
3572        state: WorkerScriptLoadState,
3573    ) {
3574        self.worker_script_load_states.insert(worker_id, state);
3575    }
3576
3577    /// Update the script loading state for a Worker.
3578    ///
3579    /// Called as the Worker script loading progresses through stages
3580    /// (Pending → Fetching → Validating → Decoding → Compiling → Ready/Failed).
3581    ///
3582    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3583    pub fn update_worker_script_load_state(
3584        &mut self,
3585        worker_id: &WorkerId,
3586        state: WorkerScriptLoadState,
3587    ) {
3588        if let Some(current) = self.worker_script_load_states.get_mut(worker_id) {
3589            *current = state;
3590        }
3591    }
3592
3593    /// Get the script loading state for a Worker.
3594    ///
3595    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3596    pub fn worker_script_load_state(&self, worker_id: &WorkerId) -> Option<&WorkerScriptLoadState> {
3597        self.worker_script_load_states.get(worker_id)
3598    }
3599
3600    /// Remove the script loading state for a Worker (called when reaped).
3601    ///
3602    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3603    pub fn remove_worker_script_load_state(
3604        &mut self,
3605        worker_id: &WorkerId,
3606    ) -> Option<WorkerScriptLoadState> {
3607        self.worker_script_load_states.remove(worker_id)
3608    }
3609
3610    /// Returns the number of tracked Worker script loading states.
3611    ///
3612    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3613    pub fn worker_script_load_state_count(&self) -> usize {
3614        self.worker_script_load_states.len()
3615    }
3616
3617    /// Reap script loading states for terminated Workers.
3618    ///
3619    /// Called after reap_terminated_workers to clean up loading state
3620    /// for Workers that have fully exited.
3621    ///
3622    /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
3623    fn reap_terminated_worker_script_load_states(&mut self) {
3624        let active_ids: std::collections::HashSet<WorkerId> = self
3625            .active_workers
3626            .iter()
3627            .map(|g| WorkerId(g.handle().script_url.clone()))
3628            .collect();
3629        self.worker_script_load_states
3630            .retain(|id, _| active_ids.contains(id));
3631    }
3632
3633    /// Returns a snapshot of all active Workers' lifecycle states.
3634    ///
3635    /// Used for CDP observability and debugging.
3636    ///
3637    /// @trace REQ-BRW-004 [entity:Worker] [criterion:18]
3638    pub fn worker_lifecycle_states(&self) -> Vec<(WorkerId, WorkerLifecycleState)> {
3639        self.active_workers
3640            .iter()
3641            .map(|g| {
3642                let id = WorkerId(g.handle().script_url.clone());
3643                (id, g.lifecycle_state())
3644            })
3645            .collect()
3646    }
3647
3648    /// Set the Worker scope config from the page's StealthProfile.
3649    ///
3650    /// Called when a page is created with a StealthProfile to ensure
3651    /// Workers spawned from that page inherit the same stealth properties.
3652    ///
3653    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:12..17]
3654    pub fn set_worker_scope_config(&mut self, config: WorkerScopeConfig) {
3655        self.worker_scope_config = config;
3656    }
3657
3658    /// Forward a Worker postMessage event to the CDP event path.
3659    ///
3660    /// DF-WK-4 / DF-WK-5: When event_tx is set, push a
3661    /// ServoEvent::Console for CDP observability.
3662    /// Supports both metadata-only events (from servo internal message
3663    /// handling) and full structured-clone events (from bao channel bridge).
3664    ///
3665    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6] [DF-WK-4] [DF-WK-5]
3666    pub fn forward_worker_message_event(&self, event: WorkerMessageEvent) {
3667        if let Some(ref tx) = self.event_tx {
3668            let direction = match event.direction {
3669                WorkerMessageDirection::PageToWorker => "page→worker",
3670                WorkerMessageDirection::WorkerToPage => "worker→page",
3671            };
3672            let _ = tx.send(ServoEvent::Console {
3673                target_id: "0".to_string(),
3674                level: ConsoleLevel::Debug,
3675                text: format!("[Worker] postMessage {}: {}", direction, event.worker_id.0),
3676                url: None,
3677                line: None,
3678                column: None,
3679            });
3680        }
3681    }
3682
3683    /// Forward a Worker structured-clone message to the CDP event path.
3684    ///
3685    /// DF-WK-4 / DF-WK-5: When event_tx is set, push a
3686    /// ServoEvent::Console for CDP observability with payload metadata.
3687    /// Includes message_id for trace correlation and payload size.
3688    ///
3689    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6] [DF-WK-4] [DF-WK-5]
3690    pub fn forward_worker_structured_message(&self, msg: &WorkerStructuredMessage) {
3691        if let Some(ref tx) = self.event_tx {
3692            let direction = match msg.direction {
3693                WorkerMessageDirection::PageToWorker => "page→worker",
3694                WorkerMessageDirection::WorkerToPage => "worker→page",
3695            };
3696            let payload_info = match &msg.payload {
3697                Some(p) => format!(
3698                    "{} bytes, {} transferable(s)",
3699                    p.data.len(),
3700                    p.transferable_count
3701                ),
3702                None => "metadata-only (servo handles clone)".to_string(),
3703            };
3704            let _ = tx.send(ServoEvent::Console {
3705                target_id: "0".to_string(),
3706                level: ConsoleLevel::Debug,
3707                text: format!(
3708                    "[Worker] postMessage #{} {}: {} [{}]",
3709                    msg.message_id, direction, msg.worker_id.0, payload_info
3710                ),
3711                url: None,
3712                line: None,
3713                column: None,
3714            });
3715        }
3716    }
3717
3718    /// Forward a Worker error event to the CDP event path.
3719    ///
3720    /// SPEC criterion #9: "onerror 事件正确传播到主线程
3721    /// (ErrorEvent 包含 message/filename/lineno/colno)".
3722    /// When event_tx is set, push a ServoEvent::PageError for CDP
3723    /// observability (maps to Runtime.exceptionThrown).
3724    ///
3725    /// @trace REQ-BRW-004 [entity:Worker] [criterion:9]
3726    pub fn forward_worker_error_event(&self, event: WorkerErrorEvent) {
3727        if let Some(ref tx) = self.event_tx {
3728            let _ = tx.send(ServoEvent::PageError {
3729                target_id: "0".to_string(),
3730                text: format!("[Worker] {}: {}", event.worker_id.0, event.message),
3731                url: Some(event.filename.clone()),
3732                line: Some(event.lineno),
3733                column: Some(event.colno),
3734                stack: None,
3735            });
3736        }
3737    }
3738
3739    // ─── Worker Structured Clone Channel (REQ-BRW-004 criterion #6) ─────
3740
3741    /// Register a channel bridge for a Worker's postMessage channel.
3742    ///
3743    /// Called when a Worker is created and its channel bridge is set up.
3744    /// The bridge enables page→worker (DF-WK-4) and worker→page (DF-WK-5)
3745    /// structured-clone message passing.
3746    ///
3747    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
3748    pub fn register_worker_channel(&mut self, bridge: WorkerChannelBridge) {
3749        let id = bridge.worker_id.clone();
3750        self.worker_channels.insert(id, bridge);
3751    }
3752
3753    /// Create and register a channel bridge for a Worker.
3754    ///
3755    /// Convenience method that creates the bridge and endpoints, registers
3756    /// the bridge, and returns the endpoints for the worker thread.
3757    ///
3758    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4 / DF-WK-5
3759    pub fn create_worker_channel(&mut self, worker_id: WorkerId) -> WorkerChannelEndpoints {
3760        let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
3761        self.worker_channels
3762            .insert(bridge.worker_id.clone(), bridge);
3763        endpoints
3764    }
3765
3766    /// Remove a Worker's channel bridge (e.g., after termination).
3767    ///
3768    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6]
3769    pub fn remove_worker_channel(&mut self, worker_id: &WorkerId) -> Option<WorkerChannelBridge> {
3770        self.worker_channels.remove(worker_id)
3771    }
3772
3773    /// Get a reference to a Worker's channel bridge.
3774    ///
3775    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6]
3776    pub fn worker_channel(&self, worker_id: &WorkerId) -> Option<&WorkerChannelBridge> {
3777        self.worker_channels.get(worker_id)
3778    }
3779
3780    /// Post a structured-clone message to a Worker (DF-WK-4).
3781    ///
3782    /// Sends the payload through the Worker's channel bridge.
3783    /// Returns Err if the worker is not found or the channel is closed.
3784    ///
3785    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6] DF-WK-4
3786    pub fn post_to_worker(
3787        &self,
3788        worker_id: &WorkerId,
3789        payload: StructuredClonePayload,
3790    ) -> Result<(), String> {
3791        match self.worker_channels.get(worker_id) {
3792            Some(bridge) => bridge
3793                .post_message_to_worker(payload)
3794                .map_err(|e| format!("Worker channel closed: {}", e)),
3795            None => Err(format!("No channel bridge for worker: {}", worker_id.0)),
3796        }
3797    }
3798
3799    /// Drain all pending worker→page messages from all Workers (DF-WK-5).
3800    ///
3801    /// Called during spin_event_loop to process all queued messages
3802    /// from workers. Each message is forwarded to CDP for observability.
3803    /// Returns all available messages and a set of WorkerIds whose channels
3804    /// are disconnected (worker thread has exited).
3805    ///
3806    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:6] DF-WK-5
3807    /// @trace REQ-BRW-004 [criterion:18] crash-safe teardown detection
3808    pub fn drain_all_worker_messages(&self) -> (Vec<WorkerStructuredMessage>, Vec<WorkerId>) {
3809        let mut all_messages = Vec::new();
3810        let mut disconnected_workers = Vec::new();
3811        for (id, bridge) in &self.worker_channels {
3812            let result = bridge.drain_worker_messages();
3813            all_messages.extend(result.messages);
3814            if result.disconnected {
3815                disconnected_workers.push(id.clone());
3816            }
3817        }
3818        (all_messages, disconnected_workers)
3819    }
3820
3821    /// Drain worker→page messages and forward each to CDP (DF-WK-5).
3822    ///
3823    /// Convenience method combining drain_all_worker_messages with
3824    /// forward_worker_structured_message for each message.
3825    /// Returns the set of WorkerIds whose channels are disconnected.
3826    ///
3827    /// @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope] [criterion:6] DF-WK-5
3828    /// @trace REQ-BRW-004 [criterion:18] crash-safe teardown detection
3829    pub fn drain_and_forward_worker_messages(&self) -> Vec<WorkerId> {
3830        let (messages, disconnected) = self.drain_all_worker_messages();
3831        for msg in &messages {
3832            self.forward_worker_structured_message(msg);
3833        }
3834        disconnected
3835    }
3836
3837    /// Remove channel bridges for all terminated Workers.
3838    ///
3839    /// Called after reap_terminated_workers to clean up channel state
3840    /// for Workers that have fully exited.
3841    ///
3842    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6]
3843    pub fn reap_terminated_worker_channels(&mut self) {
3844        // Collect IDs of workers that still have channels but are no longer
3845        // in active_workers (meaning they've been reaped).
3846        let active_ids: std::collections::HashSet<WorkerId> = self
3847            .active_workers
3848            .iter()
3849            .map(|g| WorkerId(g.handle().script_url.clone()))
3850            .collect();
3851        self.worker_channels.retain(|id, _| active_ids.contains(id));
3852    }
3853
3854    /// Returns the number of registered Worker channel bridges.
3855    ///
3856    /// @trace REQ-BRW-004 [entity:Worker] [criterion:6]
3857    pub fn worker_channel_count(&self) -> usize {
3858        self.worker_channels.len()
3859    }
3860
3861    // ─── SharedWorker Cross-Page Routing (REQ-BRW-004 / DF-WK-7) ─────
3862
3863    /// Track a SharedWorker port reference for this webview.
3864    ///
3865    /// DF-WK-7: When a page creates a SharedWorker, the constellation routes
3866    /// to the same worker thread if (url, name) matches. The page receives
3867    /// a MessagePort via the connect event. This method tracks the port
3868    /// reference so it can be disconnected on page unload.
3869    ///
3870    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
3871    pub fn track_shared_worker_port(&mut self, port_ref: SharedWorkerPortRef) {
3872        self.shared_worker_ports.push(port_ref);
3873    }
3874
3875    /// Disconnect all SharedWorker ports on page unload.
3876    ///
3877    /// Unlike DedicatedWorkers (which are terminated), SharedWorkers survive
3878    /// page unload. Only the per-page MessagePorts are disconnected by
3879    /// dropping the SharedWorkerPortRef (which decrements the connected-pages
3880    /// counter in the SharedWorkerHandle).
3881    ///
3882    /// Also clears the per-page SharedWorker channel bridges — dropping the
3883    /// channels signals the worker thread that the page has disconnected
3884    /// (DF-WK-7). SharedWorkerGlobalScope states are NOT cleared here — they
3885    /// belong to the global registry in BaoServoDelegate and survive page unload.
3886    ///
3887    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
3888    pub fn disconnect_shared_worker_ports(&mut self) {
3889        if !self.shared_worker_ports.is_empty() {
3890            log::debug!(
3891                "[delegate] page navigation: disconnecting {} shared worker ports",
3892                self.shared_worker_ports.len()
3893            );
3894        }
3895        self.shared_worker_ports.clear();
3896        // @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] DF-WK-7
3897        // Clear per-page SharedWorker channel bridges — dropping the port
3898        // channels signals the worker thread that this page has disconnected.
3899        // The SharedWorker itself survives (tracked in BaoServoDelegate registry).
3900        self.shared_worker_channels.clear();
3901        // @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
3902        // Clear per-page SharedWorker scope state references.
3903        self.shared_worker_scopes.clear();
3904    }
3905
3906    /// Returns the number of active SharedWorker port references.
3907    ///
3908    /// @trace REQ-BRW-004 [entity:SharedWorker]
3909    pub fn shared_worker_port_count(&self) -> usize {
3910        self.shared_worker_ports.len()
3911    }
3912
3913    /// Forward a SharedWorker connect event to the CDP event path.
3914    ///
3915    /// DF-WK-7: When a page connects to a SharedWorker (either creating a new
3916    /// one or reusing an existing one), the worker fires a `connect` event.
3917    /// This method forwards the metadata for CDP observability.
3918    ///
3919    /// @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] DF-WK-7
3920    pub fn forward_shared_worker_connect_event(&self, event: SharedWorkerConnectEvent) {
3921        if let Some(ref tx) = self.event_tx {
3922            let _ = tx.send(ServoEvent::Console {
3923                target_id: "0".to_string(),
3924                level: ConsoleLevel::Debug,
3925                text: format!(
3926                    "[SharedWorker] connect: {} (name={}) from {}",
3927                    event.shared_worker_id.script_url,
3928                    if event.shared_worker_id.name.is_empty() {
3929                        "<default>"
3930                    } else {
3931                        &event.shared_worker_id.name
3932                    },
3933                    event.page_url
3934                ),
3935                url: None,
3936                line: None,
3937                column: None,
3938            });
3939        }
3940    }
3941
3942    // ─── SharedWorker Channel & Scope (REQ-BRW-004 / DF-WK-7) ────────
3943
3944    /// Register a SharedWorker channel bridge for this webview.
3945    ///
3946    /// DF-WK-7: Each SharedWorker gets a channel bridge that aggregates
3947    /// per-page port channels. This method registers the bridge so
3948    /// messages can be drained during spin_event_loop.
3949    ///
3950    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
3951    pub fn register_shared_worker_channel(&mut self, bridge: SharedWorkerChannelBridge) {
3952        let id = bridge.shared_worker_id.clone();
3953        self.shared_worker_channels.insert(id, bridge);
3954    }
3955
3956    /// Create a new SharedWorker channel bridge and register it.
3957    ///
3958    /// Convenience method that creates the bridge and registers it.
3959    /// Returns a mutable reference to the bridge for adding ports.
3960    ///
3961    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
3962    pub fn create_shared_worker_channel(&mut self, shared_worker_id: SharedWorkerId) {
3963        let bridge = SharedWorkerChannelBridge::new(shared_worker_id.clone());
3964        self.shared_worker_channels.insert(shared_worker_id, bridge);
3965    }
3966
3967    /// Add a port to an existing SharedWorker channel bridge.
3968    ///
3969    /// DF-WK-7: When a page connects to a SharedWorker, a new port channel
3970    /// is created. Returns the port endpoints for the worker thread.
3971    /// If no bridge exists for the SharedWorkerId, one is created first.
3972    ///
3973    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
3974    pub fn add_shared_worker_port(
3975        &mut self,
3976        shared_worker_id: SharedWorkerId,
3977    ) -> SharedWorkerPortEndpoints {
3978        if !self.shared_worker_channels.contains_key(&shared_worker_id) {
3979            self.create_shared_worker_channel(shared_worker_id.clone());
3980        }
3981        self.shared_worker_channels
3982            .get_mut(&shared_worker_id)
3983            .expect("just created")
3984            .add_port()
3985    }
3986
3987    /// Get a reference to a SharedWorker channel bridge.
3988    ///
3989    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
3990    pub fn shared_worker_channel(&self, id: &SharedWorkerId) -> Option<&SharedWorkerChannelBridge> {
3991        self.shared_worker_channels.get(id)
3992    }
3993
3994    /// Remove a SharedWorker channel bridge.
3995    ///
3996    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
3997    pub fn remove_shared_worker_channel(
3998        &mut self,
3999        id: &SharedWorkerId,
4000    ) -> Option<SharedWorkerChannelBridge> {
4001        self.shared_worker_channels.remove(id)
4002    }
4003
4004    /// Drain all pending SharedWorker→page messages from all SharedWorkers (DF-WK-7).
4005    ///
4006    /// Called during spin_event_loop to process all queued messages
4007    /// from SharedWorkers across all connected pages. Each message is
4008    /// forwarded to CDP for observability.
4009    /// Returns messages and any disconnected SharedWorkerIds.
4010    ///
4011    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
4012    /// @trace REQ-BRW-004 [criterion:18] crash-safe teardown detection
4013    pub fn drain_all_shared_worker_messages(
4014        &self,
4015    ) -> (Vec<WorkerStructuredMessage>, Vec<SharedWorkerId>) {
4016        let mut all_messages = Vec::new();
4017        let mut all_disconnected = Vec::new();
4018        for (_, bridge) in &self.shared_worker_channels {
4019            let (messages, disconnected) = bridge.drain_all_worker_messages();
4020            all_messages.extend(messages);
4021            all_disconnected.extend(disconnected);
4022        }
4023        (all_messages, all_disconnected)
4024    }
4025
4026    /// Drain SharedWorker messages and forward each to CDP (DF-WK-7).
4027    ///
4028    /// Returns the set of disconnected SharedWorkerIds.
4029    ///
4030    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] DF-WK-7
4031    /// @trace REQ-BRW-004 [criterion:18] crash-safe teardown detection
4032    pub fn drain_and_forward_shared_worker_messages(&self) -> Vec<SharedWorkerId> {
4033        let (messages, disconnected) = self.drain_all_shared_worker_messages();
4034        for msg in &messages {
4035            self.forward_worker_structured_message(&msg);
4036        }
4037        disconnected
4038    }
4039
4040    /// Post a message to a SharedWorker via a specific port index.
4041    ///
4042    /// Convenience method combining shared_worker_channel lookup with
4043    /// post_to_worker_from_port.
4044    ///
4045    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
4046    pub fn post_to_worker_via_shared_port(
4047        &self,
4048        id: &SharedWorkerId,
4049        port_index: usize,
4050        payload: StructuredClonePayload,
4051    ) -> Result<(), String> {
4052        match self.shared_worker_channels.get(id) {
4053            Some(bridge) => bridge.post_to_worker_from_port(port_index, payload),
4054            None => Err(format!(
4055                "No channel bridge for SharedWorker: {}:{}",
4056                id.script_url, id.name
4057            )),
4058        }
4059    }
4060
4061    /// Clean up SharedWorker channel ports for disconnected workers.
4062    ///
4063    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
4064    pub fn reap_disconnected_shared_worker_ports(&mut self) {
4065        for (_, bridge) in &mut self.shared_worker_channels {
4066            bridge.remove_disconnected_ports();
4067        }
4068        // Remove bridges with no remaining ports
4069        self.shared_worker_channels
4070            .retain(|_, bridge| bridge.port_count() > 0);
4071    }
4072
4073    /// Returns the total number of SharedWorker port channels.
4074    ///
4075    /// @trace REQ-BRW-004 [entity:SharedWorker]
4076    pub fn shared_worker_channel_count(&self) -> usize {
4077        self.shared_worker_channels
4078            .values()
4079            .map(|b| b.port_count())
4080            .sum()
4081    }
4082
4083    /// Register a SharedWorkerGlobalScope state under the given SharedWorkerId.
4084    ///
4085    /// Called when a SharedWorker is created, populating the scope state
4086    /// for CDP observability and stealth consistency verification (CRIT-STL-WK).
4087    ///
4088    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
4089    pub fn register_shared_worker_scope(
4090        &mut self,
4091        id: SharedWorkerId,
4092        scope: SharedWorkerGlobalScopeState,
4093    ) {
4094        self.shared_worker_scopes.insert(id, scope);
4095    }
4096
4097    /// Get a reference to a SharedWorkerGlobalScope state.
4098    ///
4099    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
4100    pub fn shared_worker_scope(
4101        &self,
4102        id: &SharedWorkerId,
4103    ) -> Option<&SharedWorkerGlobalScopeState> {
4104        self.shared_worker_scopes.get(id)
4105    }
4106
4107    /// Get a mutable reference to a SharedWorkerGlobalScope state.
4108    ///
4109    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
4110    pub fn shared_worker_scope_mut(
4111        &mut self,
4112        id: &SharedWorkerId,
4113    ) -> Option<&mut SharedWorkerGlobalScopeState> {
4114        self.shared_worker_scopes.get_mut(id)
4115    }
4116
4117    /// Remove a SharedWorkerGlobalScope state (called when a SharedWorker is reaped).
4118    ///
4119    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
4120    pub fn remove_shared_worker_scope(
4121        &mut self,
4122        id: &SharedWorkerId,
4123    ) -> Option<SharedWorkerGlobalScopeState> {
4124        self.shared_worker_scopes.remove(id)
4125    }
4126
4127    /// Returns the number of tracked SharedWorkerGlobalScope states.
4128    ///
4129    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
4130    pub fn shared_worker_scope_count(&self) -> usize {
4131        self.shared_worker_scopes.len()
4132    }
4133
4134    /// Returns a snapshot of all SharedWorkerGlobalScope states.
4135    ///
4136    /// Used for CDP observability (Runtime domain) and stealth consistency
4137    /// verification (criterion #12-17).
4138    ///
4139    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope]
4140    pub fn shared_worker_scopes(&self) -> Vec<&SharedWorkerGlobalScopeState> {
4141        self.shared_worker_scopes.values().collect()
4142    }
4143
4144    /// Look up a SharedWorkerGlobalScope state by the Worker's script URL
4145    /// (CDP worker targetId — first match when several share a script URL).
4146    ///
4147    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] [criterion:19]
4148    pub fn shared_worker_scope_by_script_url(
4149        &self,
4150        script_url: &str,
4151    ) -> Option<&SharedWorkerGlobalScopeState> {
4152        self.shared_worker_scopes
4153            .values()
4154            .find(|scope| scope.shared_worker_id.script_url == script_url)
4155    }
4156
4157    /// Set the SharedWorker scope config from the first connecting page's StealthProfile.
4158    ///
4159    /// DF-WK-9: SharedWorkerGlobalScope inherits the first connecting page's
4160    /// StealthProfile and it remains fixed for the worker's lifetime (per DEC-WK-007).
4161    ///
4162    /// @trace REQ-BRW-004 [entity:SharedWorkerGlobalScope] [criterion:12..17] DF-WK-9
4163    pub fn set_shared_worker_scope_config(
4164        &mut self,
4165        shared_worker_id: &SharedWorkerId,
4166        config: &SharedWorkerScopeConfig,
4167    ) {
4168        if let Some(scope) = self.shared_worker_scopes.get_mut(shared_worker_id) {
4169            scope.scope.navigator = WorkerNavigator::from_scope_config(config);
4170        }
4171    }
4172
4173    // ─── ServiceWorker Registration & Fetch Interception (REQ-BRW-004 criterion #19) ────
4174
4175    /// Set the controlling ServiceWorker for this webview's page.
4176    ///
4177    /// A page can be controlled by at most one ServiceWorker at a time.
4178    /// Per DF-WK-8: When a ServiceWorker becomes activated and its scope matches
4179    /// the page's URL, it becomes the controller for that page.
4180    ///
4181    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-8
4182    pub fn set_controlling_service_worker(&mut self, handle: ServiceWorkerHandle) {
4183        self.controlled_service_worker = Some(handle);
4184    }
4185
4186    /// Clear the controlling ServiceWorker reference for this webview.
4187    ///
4188    /// Called on page unload or when the ServiceWorker is unregistered.
4189    /// Per SPEC criterion #19: "SW 持久生命周期(跨页存活)下 profile 继承注册页
4190    /// 且 terminate 后正确注销" — the ServiceWorker itself survives (tracked in
4191    /// BaoServoDelegate registry), only the per-page reference is cleared.
4192    ///
4193    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
4194    pub fn clear_controlling_service_worker(&mut self) {
4195        self.controlled_service_worker = None;
4196        self.service_worker_scope = None;
4197    }
4198
4199    /// Get a reference to the controlling ServiceWorker, if any.
4200    ///
4201    /// @trace REQ-BRW-004 [entity:ServiceWorker]
4202    pub fn controlling_service_worker(&self) -> Option<&ServiceWorkerHandle> {
4203        self.controlled_service_worker.as_ref()
4204    }
4205
4206    /// Check if this page is controlled by a ServiceWorker.
4207    ///
4208    /// @trace REQ-BRW-004 [entity:ServiceWorker]
4209    pub fn is_controlled_by_service_worker(&self) -> bool {
4210        self.controlled_service_worker.is_some()
4211    }
4212
4213    /// Check if a URL falls within the controlling ServiceWorker's scope.
4214    ///
4215    /// Per DF-WK-8: "scope 匹配的导航/fetch 经 SW 拦截".
4216    /// Returns false if no ServiceWorker is controlling this page.
4217    ///
4218    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-8
4219    pub fn is_url_in_service_worker_scope(&self, url: &str) -> bool {
4220        self.service_worker_scope
4221            .as_ref()
4222            .map(|scope| scope.is_url_in_scope(url))
4223            .unwrap_or(false)
4224    }
4225
4226    /// Register a ServiceWorkerGlobalScope state for the controlling ServiceWorker.
4227    ///
4228    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] DF-WK-8 / DF-WK-10
4229    pub fn register_service_worker_scope(&mut self, scope: ServiceWorkerGlobalScopeState) {
4230        self.service_worker_scope = Some(scope);
4231    }
4232
4233    /// Get a reference to the ServiceWorkerGlobalScope state.
4234    ///
4235    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
4236    pub fn service_worker_scope(&self) -> Option<&ServiceWorkerGlobalScopeState> {
4237        self.service_worker_scope.as_ref()
4238    }
4239
4240    /// Get a mutable reference to the ServiceWorkerGlobalScope state.
4241    ///
4242    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
4243    pub fn service_worker_scope_mut(&mut self) -> Option<&mut ServiceWorkerGlobalScopeState> {
4244        self.service_worker_scope.as_mut()
4245    }
4246
4247    /// Remove the ServiceWorkerGlobalScope state.
4248    ///
4249    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope]
4250    pub fn remove_service_worker_scope(&mut self) -> Option<ServiceWorkerGlobalScopeState> {
4251        self.service_worker_scope.take()
4252    }
4253
4254    /// Forward a ServiceWorker fetch interception event to the CDP event path.
4255    ///
4256    /// Per SPEC criterion #19: "CDP Network 域可观测 SW 发起的请求/响应".
4257    /// When a ServiceWorker intercepts a fetch, this method forwards the metadata
4258    /// for CDP Network domain observability.
4259    ///
4260    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-8
4261    pub fn forward_service_worker_fetch_event(&self, event: ServiceWorkerFetchEvent) {
4262        if let Some(ref tx) = self.event_tx {
4263            let stealth_status = if event.stealth_profile_applied {
4264                "stealth profile applied"
4265            } else {
4266                "⚠️ STEALTH BOUNDARY VIOLATION"
4267            };
4268            let _ = tx.send(ServoEvent::Console {
4269                target_id: "0".to_string(),
4270                level: if event.stealth_profile_applied {
4271                    ConsoleLevel::Debug
4272                } else {
4273                    ConsoleLevel::Warning
4274                },
4275                text: format!(
4276                    "[ServiceWorker] fetch {} {} -> {} ({})",
4277                    event.method,
4278                    event.request_url,
4279                    event.registration_id.script_url,
4280                    stealth_status
4281                ),
4282                url: None,
4283                line: None,
4284                column: None,
4285            });
4286        }
4287    }
4288
4289    /// Set the ServiceWorker scope config from the registering page's StealthProfile.
4290    ///
4291    /// DF-WK-10: ServiceWorkerGlobalScope inherits the registering page's profile.
4292    /// Per SPEC criterion #19: SW-intercepted fetch uses the same stealth profile.
4293    ///
4294    /// @trace REQ-BRW-004 [entity:ServiceWorkerGlobalScope] [criterion:19] DF-WK-10
4295    pub fn set_service_worker_scope_config(&mut self, config: &ServiceWorkerScopeConfig) {
4296        if let Some(scope) = &mut self.service_worker_scope {
4297            scope.scope.navigator = WorkerNavigator::from_scope_config(config);
4298        }
4299    }
4300}
4301
4302pub struct BaoServoDelegate {
4303    last_error: RefCell<Option<String>>,
4304    /// Channel for forwarding console messages to CDP Log domain.
4305    /// Set via `set_console_log_tx` when CDP server starts.
4306    console_log_tx: RefCell<Option<std::sync::mpsc::Sender<ConsoleMessage>>>,
4307    /// Channel for forwarding structured ServoEvent to the EventSubscriber path (Path B).
4308    /// When set, console/url/load callbacks also push structured events here.
4309    /// @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4310    event_tx: RefCell<Option<Sender<ServoEvent>>>,
4311    /// Global SharedWorker registry — keyed by (script_url, name).
4312    /// SharedWorkers span pages (DF-WK-7), so they must be tracked at the
4313    /// delegate level rather than per-page. When a page creates a SharedWorker,
4314    /// the constellation routes to the same worker thread if (url, name) matches.
4315    /// This registry tracks all active SharedWorkers across all pages.
4316    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
4317    shared_workers: RefCell<Vec<SharedWorkerHandle>>,
4318    /// Global ServiceWorker registry — keyed by (script_url, scope).
4319    /// ServiceWorkers have persistent lifecycle (跨页存活) and can control
4320    /// multiple pages within their scope. Per DF-WK-8: "navigator.serviceWorker.
4321    /// register(url,{scope}) → serviceworker_manager 注册 → scope 匹配的
4322    /// 导航/fetch 经 SW 拦截".
4323    /// Per SPEC criterion #19: "SW 持久生命周期(跨页存活)下 profile 继承注册页
4324    /// 且 terminate 后正确注销".
4325    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-8
4326    service_workers: RefCell<Vec<ServiceWorkerHandle>>,
4327}
4328
4329impl Default for BaoServoDelegate {
4330    fn default() -> Self {
4331        BaoServoDelegate {
4332            last_error: RefCell::new(None),
4333            console_log_tx: RefCell::new(None),
4334            event_tx: RefCell::new(None),
4335            shared_workers: RefCell::new(Vec::new()),
4336            service_workers: RefCell::new(Vec::new()),
4337        }
4338    }
4339}
4340
4341impl BaoServoDelegate {
4342    pub fn new() -> Self {
4343        Self::default()
4344    }
4345
4346    pub fn last_error(&self) -> Option<String> {
4347        self.last_error.borrow().clone()
4348    }
4349
4350    /// Set the channel for forwarding console messages to CDP.
4351    /// Called when CDP server starts.
4352    pub fn set_console_log_tx(&self, tx: std::sync::mpsc::Sender<ConsoleMessage>) {
4353        *self.console_log_tx.borrow_mut() = Some(tx);
4354    }
4355
4356    /// Get a clone of the console log sender, if one has been set.
4357    /// Used to propagate the channel to per-webview state.
4358    pub fn console_log_tx(&self) -> Option<std::sync::mpsc::Sender<ConsoleMessage>> {
4359        self.console_log_tx.borrow().clone()
4360    }
4361
4362    /// Set the channel for forwarding structured ServoEvent to EventSubscriber (Path B).
4363    /// Called when CDP server starts alongside set_console_log_tx.
4364    /// @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4365    pub fn set_event_tx(&self, tx: Sender<ServoEvent>) {
4366        *self.event_tx.borrow_mut() = Some(tx);
4367    }
4368
4369    /// Get a clone of the event sender, if one has been set.
4370    /// Used to propagate the channel to per-webview state.
4371    /// @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4372    pub fn event_tx(&self) -> Option<Sender<ServoEvent>> {
4373        self.event_tx.borrow().clone()
4374    }
4375
4376    // ─── SharedWorker Global Registry (REQ-BRW-004 / DF-WK-7) ────────
4377
4378    /// Register a SharedWorker in the global registry.
4379    ///
4380    /// DF-WK-7: When a page creates a new SharedWorker, the handle is
4381    /// registered here so other pages can find it by (script_url, name).
4382    /// If a SharedWorker with the same id already exists, the existing
4383    /// handle is returned instead (constellation dedup).
4384    ///
4385    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
4386    pub fn register_shared_worker(&self, handle: SharedWorkerHandle) -> SharedWorkerHandle {
4387        let id = handle.id();
4388        let mut shared_workers = self.shared_workers.borrow_mut();
4389        if let Some(existing) = shared_workers.iter().find(|h| h.id() == id) {
4390            existing.clone()
4391        } else {
4392            shared_workers.push(handle.clone());
4393            handle
4394        }
4395    }
4396
4397    /// Find an existing SharedWorker by (script_url, name).
4398    ///
4399    /// Returns a clone of the SharedWorkerHandle if found, None otherwise.
4400    /// Used when a page creates a SharedWorker and the constellation routes
4401    /// to an existing worker (DF-WK-7: "多页 new SharedWorker(url) 同 name →
4402    /// constellation 路由到同一 worker 线程").
4403    ///
4404    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
4405    pub fn find_shared_worker(&self, script_url: &str, name: &str) -> Option<SharedWorkerHandle> {
4406        self.shared_workers
4407            .borrow()
4408            .iter()
4409            .find(|h| h.script_url == script_url && h.name == name)
4410            .cloned()
4411    }
4412
4413    /// Remove terminated SharedWorkers from the registry.
4414    ///
4415    /// Called after spin_event_loop to clean up SharedWorkers whose threads
4416    /// have exited and have zero connected pages.
4417    ///
4418    /// @trace REQ-BRW-004 [entity:SharedWorker]
4419    pub fn reap_terminated_shared_workers(&self) {
4420        self.shared_workers
4421            .borrow_mut()
4422            .retain(|h| !h.is_terminated() || h.connected_page_count() > 0);
4423    }
4424
4425    /// Returns the number of active SharedWorkers across all pages.
4426    ///
4427    /// @trace REQ-BRW-004 [entity:SharedWorker]
4428    pub fn shared_worker_count(&self) -> usize {
4429        self.shared_workers.borrow().len()
4430    }
4431
4432    /// Route a SharedWorker connection request to the appropriate worker.
4433    ///
4434    /// DF-WK-7: "多页 new SharedWorker(url) 同 name → constellation 路由到
4435    /// 同一 worker 线程". If a SharedWorker with the same (script_url, name)
4436    /// already exists in the registry, return the existing handle (the
4437    /// constellation handles dedup). Otherwise, register a new SharedWorker.
4438    ///
4439    /// Returns the handle (existing or new) and a boolean indicating whether
4440    /// this is a new SharedWorker (true) or a reconnection (false).
4441    ///
4442    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
4443    pub fn route_shared_worker(&self, handle: SharedWorkerHandle) -> (SharedWorkerHandle, bool) {
4444        let id = handle.id();
4445        let mut shared_workers = self.shared_workers.borrow_mut();
4446        if let Some(existing) = shared_workers.iter().find(|h| h.id() == id) {
4447            (existing.clone(), false)
4448        } else {
4449            shared_workers.push(handle.clone());
4450            (handle, true)
4451        }
4452    }
4453
4454    /// Find or create a SharedWorker for the given (script_url, name).
4455    ///
4456    /// Convenience method combining find_shared_worker with register_shared_worker.
4457    /// Returns the handle and whether it was newly created.
4458    ///
4459    /// @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
4460    pub fn get_or_create_shared_worker(
4461        &self,
4462        script_url: &str,
4463        name: &str,
4464    ) -> (SharedWorkerHandle, bool) {
4465        if let Some(existing) = self.find_shared_worker(script_url, name) {
4466            (existing, false)
4467        } else {
4468            let handle = SharedWorkerHandle::new(script_url.to_string(), name.to_string());
4469            let returned = self.register_shared_worker(handle);
4470            (returned, true)
4471        }
4472    }
4473
4474    /// Remove a SharedWorker from the global registry by its ID.
4475    ///
4476    /// Called when a SharedWorker has been fully terminated and has zero
4477    /// connected pages. This is the final cleanup step in the lifecycle.
4478    ///
4479    /// @trace REQ-BRW-004 [entity:SharedWorker]
4480    pub fn unregister_shared_worker(&self, id: &SharedWorkerId) -> bool {
4481        let mut shared_workers = self.shared_workers.borrow_mut();
4482        let before = shared_workers.len();
4483        shared_workers.retain(|h| &h.id() != id);
4484        shared_workers.len() < before
4485    }
4486
4487    /// Returns a snapshot of all SharedWorker handles in the registry.
4488    ///
4489    /// Used for CDP observability and lifecycle management.
4490    ///
4491    /// @trace REQ-BRW-004 [entity:SharedWorker]
4492    pub fn all_shared_workers(&self) -> Vec<SharedWorkerHandle> {
4493        self.shared_workers.borrow().iter().cloned().collect()
4494    }
4495
4496    // ─── ServiceWorker Global Registry (REQ-BRW-004 / DF-WK-8) ─────────
4497
4498    /// Register a ServiceWorker in the global registry.
4499    ///
4500    /// DF-WK-8: "navigator.serviceWorker.register(url,{scope}) → serviceworker_manager
4501    /// 注册". If a ServiceWorker with the same registration_id already exists,
4502    /// the existing handle is returned instead (registration dedup).
4503    ///
4504    /// The handle captures the registering page's StealthProfile for stealth
4505    /// boundary enforcement (SPEC criterion #19).
4506    ///
4507    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-8
4508    pub fn register_service_worker(&self, handle: ServiceWorkerHandle) -> ServiceWorkerHandle {
4509        let id = handle.id();
4510        let mut service_workers = self.service_workers.borrow_mut();
4511        if let Some(existing) = service_workers.iter().find(|h| h.id() == id) {
4512            existing.clone()
4513        } else {
4514            service_workers.push(handle.clone());
4515            handle
4516        }
4517    }
4518
4519    /// Find an existing ServiceWorker by (script_url, scope).
4520    ///
4521    /// Returns a clone of the ServiceWorkerHandle if found, None otherwise.
4522    ///
4523    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
4524    pub fn find_service_worker(
4525        &self,
4526        script_url: &str,
4527        scope: &str,
4528    ) -> Option<ServiceWorkerHandle> {
4529        self.service_workers
4530            .borrow()
4531            .iter()
4532            .find(|h| h.script_url == script_url && h.scope == scope)
4533            .cloned()
4534    }
4535
4536    /// Find a ServiceWorker whose scope matches the given URL.
4537    ///
4538    /// Per DF-WK-8: "scope 匹配的导航/fetch 经 SW 拦截". Returns the
4539    /// ServiceWorker whose scope prefix-matches the URL and is in the
4540    /// Activated state (intercepting fetches).
4541    ///
4542    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19] DF-WK-8
4543    pub fn find_service_worker_for_url(&self, url: &str) -> Option<ServiceWorkerHandle> {
4544        self.service_workers
4545            .borrow()
4546            .iter()
4547            .filter(|h| h.is_intercepting_fetch())
4548            .find(|h| url.starts_with(&h.scope))
4549            .cloned()
4550    }
4551
4552    /// Remove terminated ServiceWorkers from the registry.
4553    ///
4554    /// Per SPEC criterion #19: "terminate 后正确注销". Called after
4555    /// spin_event_loop to clean up ServiceWorkers whose threads have exited.
4556    ///
4557    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
4558    pub fn reap_terminated_service_workers(&self) {
4559        self.service_workers
4560            .borrow_mut()
4561            .retain(|h| !h.is_terminated());
4562    }
4563
4564    /// Returns the number of active ServiceWorker registrations across all pages.
4565    ///
4566    /// @trace REQ-BRW-004 [entity:ServiceWorker]
4567    pub fn service_worker_count(&self) -> usize {
4568        self.service_workers.borrow().len()
4569    }
4570
4571    /// Unregister a ServiceWorker by its registration ID.
4572    ///
4573    /// Per SPEC criterion #19: "terminate 后正确注销". This is the final
4574    /// cleanup step — the ServiceWorker is removed from the global registry,
4575    /// and its fetch interception is disabled.
4576    ///
4577    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
4578    pub fn unregister_service_worker(&self, id: &ServiceWorkerRegistrationId) -> bool {
4579        let mut service_workers = self.service_workers.borrow_mut();
4580        let before = service_workers.len();
4581        service_workers.retain(|h| &h.id() != id);
4582        service_workers.len() < before
4583    }
4584
4585    /// Find or create a ServiceWorker for the given (script_url, scope).
4586    ///
4587    /// Convenience method combining find_service_worker with register_service_worker.
4588    /// Returns the handle and whether it was newly created.
4589    ///
4590    /// @trace REQ-BRW-004 [entity:ServiceWorker] DF-WK-8
4591    pub fn get_or_create_service_worker(
4592        &self,
4593        script_url: &str,
4594        scope: &str,
4595        stealth_profile: Option<bao_stealth::StealthProfile>,
4596    ) -> (ServiceWorkerHandle, bool) {
4597        if let Some(existing) = self.find_service_worker(script_url, scope) {
4598            (existing, false)
4599        } else {
4600            let handle = ServiceWorkerHandle::new(
4601                script_url.to_string(),
4602                scope.to_string(),
4603                stealth_profile,
4604            );
4605            let returned = self.register_service_worker(handle);
4606            (returned, true)
4607        }
4608    }
4609
4610    /// Returns a snapshot of all ServiceWorker handles in the registry.
4611    ///
4612    /// Used for CDP observability and lifecycle management.
4613    ///
4614    /// @trace REQ-BRW-004 [entity:ServiceWorker]
4615    pub fn all_service_workers(&self) -> Vec<ServiceWorkerHandle> {
4616        self.service_workers.borrow().iter().cloned().collect()
4617    }
4618
4619    /// Verify stealth profile consistency for all ServiceWorker-intercepted fetches.
4620    ///
4621    /// Per SPEC criterion #19: "SW 拦截并转发的 fetch 仍走主页同一 stealth
4622    /// TLS(JA3/JA4)+HTTP2(AKAMAI) profile (不绕过反指纹)". This method
4623    /// checks that all active ServiceWorkers have a stealth profile consistent
4624    /// with the given page's profile.
4625    ///
4626    /// Returns a list of violations (ServiceWorker registrations where the
4627    /// profile doesn't match).
4628    ///
4629    /// @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
4630    pub fn verify_service_worker_stealth_consistency(
4631        &self,
4632        page_stealth_profile: &bao_stealth::StealthProfile,
4633    ) -> Vec<ServiceWorkerRegistrationId> {
4634        self.service_workers
4635            .borrow()
4636            .iter()
4637            .filter(|h| h.is_intercepting_fetch())
4638            .filter(|h| {
4639                // Check if the SW's stealth profile matches the page's profile.
4640                // A profile mismatch means SW-intercepted fetches could bypass
4641                // the page's stealth TLS/HTTP2 settings (SPEC criterion #19).
4642                match &h.stealth_profile {
4643                    Some(sw_profile) => {
4644                        // Compare key fingerprint-relevant fields.
4645                        // If any field differs, it's a stealth boundary violation.
4646                        sw_profile.navigator.user_agent != page_stealth_profile.navigator.user_agent
4647                            || sw_profile.navigator.platform
4648                                != page_stealth_profile.navigator.platform
4649                    }
4650                    None => {
4651                        // No stealth profile on an intercepting SW — this is always
4652                        // a violation because intercepted fetches won't have stealth.
4653                        true
4654                    }
4655                }
4656            })
4657            .map(|h| h.id())
4658            .collect()
4659    }
4660}
4661
4662impl ServoDelegate for BaoServoDelegate {
4663    fn notify_error(&self, error: ServoError) {
4664        let error_str = format!("{error:?}");
4665        *self.last_error.borrow_mut() = Some(error_str.clone());
4666        // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4667        // TLS/certificate errors: always use console_log_tx (Path A) since there is no
4668        // ServoEvent equivalent for SecurityCertificateError. These are rare events
4669        // that don't map to the 7 ServoEvent categories.
4670        if error_str.to_lowercase().contains("certificate")
4671            || error_str.to_lowercase().contains("tls")
4672        {
4673            if let Some(ref tx) = *self.console_log_tx.borrow() {
4674                let _ = tx.send(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
4675                    event_id: 0,
4676                    error_type: "net::ERR_CERT_AUTHORITY_INVALID".to_string(),
4677                    url: String::new(),
4678                }));
4679            }
4680        }
4681    }
4682
4683    fn show_console_message(&self, level: ConsoleLogLevel, message: String) {
4684        let level_str = match level {
4685            ConsoleLogLevel::Debug => "debug",
4686            ConsoleLogLevel::Log => "info",
4687            ConsoleLogLevel::Info => "info",
4688            ConsoleLogLevel::Warn => "warning",
4689            ConsoleLogLevel::Error => "error",
4690            ConsoleLogLevel::Trace => "verbose",
4691            ConsoleLogLevel::Dir => "info",
4692        };
4693        log::trace!("[servo] {message}");
4694
4695        // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4696        // When event_tx is set, push structured ServoEvent::Console (Path B) as the primary
4697        // event path. Only fall back to console_log_tx (Path A) when event_tx is absent,
4698        // avoiding double-broadcast of the same event.
4699        let event_tx = self.event_tx.borrow();
4700        if let Some(ref tx) = *event_tx {
4701            let servo_level = match level {
4702                ConsoleLogLevel::Debug => ConsoleLevel::Debug,
4703                ConsoleLogLevel::Log => ConsoleLevel::Info,
4704                ConsoleLogLevel::Info => ConsoleLevel::Info,
4705                ConsoleLogLevel::Warn => ConsoleLevel::Warning,
4706                ConsoleLogLevel::Error => ConsoleLevel::Error,
4707                ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
4708                ConsoleLogLevel::Dir => ConsoleLevel::Info,
4709            };
4710            let _ = tx.send(ServoEvent::Console {
4711                target_id: "0".to_string(),
4712                level: servo_level,
4713                text: message,
4714                url: None,
4715                line: None,
4716                column: None,
4717            });
4718        } else if let Some(ref tx) = *self.console_log_tx.borrow() {
4719            let msg = match BaoEvent::from_console_text(&message) {
4720                Some(ConsoleMessage::Event(evt)) => ConsoleMessage::Event(evt),
4721                _ => ConsoleMessage::Log {
4722                    level: level_str.to_string(),
4723                    text: message,
4724                },
4725            };
4726            let _ = tx.send(msg);
4727        }
4728    }
4729
4730    fn request_devtools_connection(&self, request: AllowOrDenyRequest) {
4731        request.allow();
4732    }
4733}
4734
4735pub struct BaoWebViewDelegate {
4736    state: Rc<RefCell<BaoWebViewState>>,
4737    viewport: PhysicalSize<u32>,
4738}
4739
4740impl BaoWebViewDelegate {
4741    pub fn new(state: Rc<RefCell<BaoWebViewState>>, viewport: PhysicalSize<u32>) -> Self {
4742        BaoWebViewDelegate { state, viewport }
4743    }
4744
4745    pub fn state(&self) -> &Rc<RefCell<BaoWebViewState>> {
4746        &self.state
4747    }
4748}
4749
4750impl WebViewDelegate for BaoWebViewDelegate {
4751    fn screen_geometry(&self, _webview: WebView) -> Option<ScreenGeometry> {
4752        let screen_size =
4753            DeviceIntSize::new(self.viewport.width as i32, self.viewport.height as i32);
4754        Some(ScreenGeometry {
4755            size: screen_size,
4756            available_size: screen_size,
4757            window_rect: DeviceIntRect::from_origin_and_size(DeviceIntPoint::zero(), screen_size),
4758        })
4759    }
4760
4761    fn notify_url_changed(&self, _webview: WebView, url: url::Url) {
4762        let url_str = url.to_string();
4763        self.state.borrow_mut().url = Some(url);
4764        // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4765        // Dual-path: event_tx (Path B) primary for FrameNavigated,
4766        // console_log_tx (Path A) fallback for PageFrameNavigated.
4767        let event_tx = self.state.borrow().event_tx.clone();
4768        if let Some(ref tx) = event_tx {
4769            let _ = tx.send(ServoEvent::FrameNavigated {
4770                target_id: "0".to_string(),
4771                frame_id: "0".to_string(),
4772                url: url_str,
4773                name: None,
4774            });
4775        } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4776            let loader_id = format!("{:016x}", url_str.len() as u64);
4777            let _ = tx.send(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
4778                frame_id: "0".to_string(),
4779                url: url_str,
4780                loader_id,
4781            }));
4782        }
4783    }
4784
4785    fn notify_page_title_changed(&self, _webview: WebView, title: Option<String>) {
4786        self.state.borrow_mut().title = title;
4787    }
4788
4789    fn notify_load_status_changed(&self, _webview: WebView, status: LoadStatus) {
4790        self.state.borrow_mut().load_status = status;
4791        match status {
4792            LoadStatus::Started => {
4793                // @trace REQ-BRW-004 [entity:Worker] [criterion:10]
4794                // SPEC criterion #10: "页面卸载时自动终止所有 Worker
4795                // (GlobalScope::track_worker + AutoCloseWorker)".
4796                // When a new navigation starts (after a previous Complete),
4797                // all Workers from the previous page must be terminated.
4798                {
4799                    let mut state = self.state.borrow_mut();
4800                    if !state.active_workers.is_empty() {
4801                        log::debug!(
4802                            "[delegate] page navigation: terminating {} active workers",
4803                            state.active_worker_count()
4804                        );
4805                        state.terminate_all_workers();
4806                    }
4807                    // @trace REQ-BRW-004 [entity:SharedWorker] DF-WK-7
4808                    // SharedWorkers survive page unload — only disconnect ports.
4809                    state.disconnect_shared_worker_ports();
4810                    // @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
4811                    // ServiceWorkers have persistent lifecycle (跨页存活) — only
4812                    // clear the per-page controlling reference. The ServiceWorker
4813                    // itself survives (tracked in BaoServoDelegate registry) and
4814                    // can control the page again if its scope matches.
4815                    state.clear_controlling_service_worker();
4816                }
4817
4818                // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4819                // Dual-path: event_tx (Path B) primary for FrameStartedLoading,
4820                // console_log_tx (Path A) fallback — no direct ConsoleMessage equivalent,
4821                // so we use a lightweight log entry.
4822                let event_tx = self.state.borrow().event_tx.clone();
4823                if let Some(ref tx) = event_tx {
4824                    let _ = tx.send(ServoEvent::FrameStartedLoading {
4825                        target_id: "0".to_string(),
4826                        frame_id: "0".to_string(),
4827                    });
4828                }
4829            }
4830            LoadStatus::Complete => {
4831                self.state.borrow_mut().dom_proxies_dirty = true;
4832
4833                // @trace REQ-BRW-004 [entity:Worker]
4834                // Reap terminated workers after page load completes.
4835                // Workers from the previous page that have been terminated
4836                // during LoadStatus::Started are cleaned up here.
4837                self.state.borrow_mut().reap_terminated_workers();
4838
4839                // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4840                // Dual-path: event_tx (Path B) primary for FrameStoppedLoading,
4841                // console_log_tx (Path A) fallback for PageLoadEventFired.
4842                let event_tx = self.state.borrow().event_tx.clone();
4843                if let Some(ref tx) = event_tx {
4844                    let _ = tx.send(ServoEvent::FrameStoppedLoading {
4845                        target_id: "0".to_string(),
4846                        frame_id: "0".to_string(),
4847                    });
4848                } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4849                    let timestamp = std::time::SystemTime::now()
4850                        .duration_since(std::time::UNIX_EPOCH)
4851                        .unwrap_or_default()
4852                        .as_secs_f64();
4853                    let _ = tx.send(ConsoleMessage::Event(BaoEvent::PageLoadEventFired {
4854                        timestamp,
4855                    }));
4856                }
4857            }
4858            LoadStatus::HeadParsed => {}
4859        }
4860    }
4861
4862    fn notify_new_frame_ready(&self, _webview: WebView) {
4863        self.state.borrow_mut().frame_ready = true;
4864    }
4865
4866    fn request_navigation(&self, _webview: WebView, request: NavigationRequest) {
4867        request.allow();
4868    }
4869
4870    fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
4871        request.allow();
4872    }
4873
4874    fn request_create_new(&self, _parent_webview: WebView, _request: CreateNewWebViewRequest) {}
4875
4876    fn show_console_message(&self, _webview: WebView, level: ConsoleLogLevel, message: String) {
4877        let level_str = match level {
4878            ConsoleLogLevel::Debug => "debug",
4879            ConsoleLogLevel::Log => "info",
4880            ConsoleLogLevel::Info => "info",
4881            ConsoleLogLevel::Warn => "warning",
4882            ConsoleLogLevel::Error => "error",
4883            ConsoleLogLevel::Trace => "verbose",
4884            ConsoleLogLevel::Dir => "info",
4885        };
4886        log::trace!("[webview] {message}");
4887
4888        // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
4889        // Same dual-path logic as BaoServoDelegate::show_console_message:
4890        // event_tx (Path B) is primary; console_log_tx (Path A) is fallback.
4891        let event_tx = self.state.borrow().event_tx.clone();
4892        if let Some(ref tx) = event_tx {
4893            let servo_level = match level {
4894                ConsoleLogLevel::Debug => ConsoleLevel::Debug,
4895                ConsoleLogLevel::Log => ConsoleLevel::Info,
4896                ConsoleLogLevel::Info => ConsoleLevel::Info,
4897                ConsoleLogLevel::Warn => ConsoleLevel::Warning,
4898                ConsoleLogLevel::Error => ConsoleLevel::Error,
4899                ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
4900                ConsoleLogLevel::Dir => ConsoleLevel::Info,
4901            };
4902            let _ = tx.send(ServoEvent::Console {
4903                target_id: "0".to_string(),
4904                level: servo_level,
4905                text: message,
4906                url: None,
4907                line: None,
4908                column: None,
4909            });
4910        } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4911            let msg = match BaoEvent::from_console_text(&message) {
4912                Some(ConsoleMessage::Event(evt)) => ConsoleMessage::Event(evt),
4913                _ => ConsoleMessage::Log {
4914                    level: level_str.to_string(),
4915                    text: message,
4916                },
4917            };
4918            let _ = tx.send(msg);
4919        }
4920    }
4921
4922    fn show_embedder_control(&self, _webview: WebView, _control: EmbedderControl) {}
4923
4924    fn hide_embedder_control(&self, _webview: WebView, _id: EmbedderControlId) {}
4925
4926    fn notify_crashed(&self, _webview: WebView, reason: String, _backtrace: Option<String>) {
4927        log::error!("[webview] crashed: {reason}");
4928    }
4929}
4930
4931#[cfg(test)]
4932mod tests {
4933    use super::*;
4934
4935    // ─── BaoWebViewState ────────────────────────────────────────────
4936    // @trace REQ-BRW-001 [req:REQ-BRW-001] [level:unit]
4937
4938    #[test]
4939    fn test_webview_state_default() {
4940        let state = BaoWebViewState::default();
4941        assert!(state.url.is_none());
4942        assert!(state.title.is_none());
4943        assert!(matches!(state.load_status, LoadStatus::Started));
4944        assert!(!state.frame_ready);
4945        assert!(!state.dom_proxies_dirty);
4946    }
4947
4948    #[test]
4949    fn test_webview_state_url_mutate() {
4950        let mut state = BaoWebViewState::default();
4951        state.url = Some(url::Url::parse("https://example.com").unwrap());
4952        assert!(state.url.is_some());
4953        assert_eq!(state.url.unwrap().as_str(), "https://example.com/");
4954    }
4955
4956    #[test]
4957    fn test_webview_state_title_mutate() {
4958        let mut state = BaoWebViewState::default();
4959        state.title = Some("Test Page".to_string());
4960        assert_eq!(state.title.as_deref(), Some("Test Page"));
4961    }
4962
4963    #[test]
4964    fn test_webview_state_frame_ready_toggle() {
4965        let mut state = BaoWebViewState::default();
4966        assert!(!state.frame_ready);
4967        state.frame_ready = true;
4968        assert!(state.frame_ready);
4969    }
4970
4971    // ─── BaoServoDelegate ──────────────────────────────────────────
4972    // @trace REQ-BRW-001 [req:REQ-BRW-001] [level:unit]
4973
4974    #[test]
4975    fn test_servo_delegate_new_no_error() {
4976        let delegate = BaoServoDelegate::new();
4977        assert!(delegate.last_error().is_none());
4978    }
4979
4980    #[test]
4981    fn test_servo_delegate_default_no_error() {
4982        let delegate = BaoServoDelegate::default();
4983        assert!(delegate.last_error().is_none());
4984    }
4985
4986    // ─── BaoWebViewDelegate ────────────────────────────────────────
4987    // @trace REQ-BRW-001 [req:REQ-BRW-001] [level:unit]
4988
4989    #[test]
4990    fn test_webview_delegate_new_with_state() {
4991        let state = Rc::new(RefCell::new(BaoWebViewState::default()));
4992        let viewport = PhysicalSize::new(1024, 768);
4993        let delegate = BaoWebViewDelegate::new(state, viewport);
4994        assert!(delegate.state().borrow().url.is_none());
4995    }
4996
4997    #[test]
4998    fn test_webview_delegate_state_rc_shared() {
4999        let state = Rc::new(RefCell::new(BaoWebViewState::default()));
5000        let viewport = PhysicalSize::new(800, 600);
5001        let delegate = BaoWebViewDelegate::new(Rc::clone(&state), viewport);
5002        // Modify state externally
5003        state.borrow_mut().title = Some("External".to_string());
5004        // Delegate sees same state
5005        assert_eq!(delegate.state().borrow().title.as_deref(), Some("External"));
5006    }
5007
5008    #[test]
5009    fn test_webview_delegate_viewport_size() {
5010        let state = Rc::new(RefCell::new(BaoWebViewState::default()));
5011        let viewport = PhysicalSize::new(1440, 900);
5012        let delegate = BaoWebViewDelegate::new(state, viewport);
5013        // Verify delegate was created with specific viewport
5014        assert!(delegate.state().borrow().url.is_none());
5015    }
5016
5017    // ─── PoolStats ─────────────────────────────────────────────────
5018    // @trace REQ-LIB-001 [req:REQ-LIB-001] [level:unit]
5019
5020    #[test]
5021    fn test_pool_stats_fields() {
5022        let stats = crate::page_pool::PoolStats {
5023            active: 3,
5024            idle: 1,
5025            total_created: 5,
5026            total_destroyed: 2,
5027        };
5028        assert_eq!(stats.active, 3);
5029        assert_eq!(stats.idle, 1);
5030        assert_eq!(stats.total_created, 5);
5031        assert_eq!(stats.total_destroyed, 2);
5032    }
5033
5034    // ─── DOM Proxy Dirty Flag ─────────────────────────────────────
5035    // @trace REQ-SEC-002 [req:REQ-SEC-002] [level:unit]
5036
5037    #[test]
5038    fn test_dom_proxies_dirty_default_false() {
5039        let state = BaoWebViewState::default();
5040        assert!(!state.dom_proxies_dirty);
5041    }
5042
5043    #[test]
5044    fn test_dom_proxies_dirty_set_on_complete() {
5045        let mut state = BaoWebViewState::default();
5046        state.load_status = LoadStatus::Complete;
5047        state.dom_proxies_dirty = true;
5048        assert!(state.dom_proxies_dirty);
5049    }
5050
5051    #[test]
5052    fn test_dom_proxies_dirty_clear_after_refresh() {
5053        let mut state = BaoWebViewState::default();
5054        state.dom_proxies_dirty = true;
5055        state.dom_proxies_dirty = false;
5056        assert!(!state.dom_proxies_dirty);
5057    }
5058
5059    // ─── Console Log Channel Forwarding ─────────────────────────────
5060    // @trace REQ-CDP-007 [req:REQ-CDP-007] [level:unit]
5061
5062    #[test]
5063    fn test_servo_delegate_console_log_channel_set_and_get() {
5064        let delegate = BaoServoDelegate::new();
5065        assert!(delegate.console_log_tx().is_none());
5066        let (tx, _rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5067        delegate.set_console_log_tx(tx);
5068        assert!(delegate.console_log_tx().is_some());
5069    }
5070
5071    #[test]
5072    fn test_servo_delegate_console_log_tx_clones() {
5073        let delegate = BaoServoDelegate::new();
5074        let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5075        delegate.set_console_log_tx(tx);
5076        // Get a clone and send through it
5077        let cloned = delegate.console_log_tx().unwrap();
5078        cloned
5079            .send(ConsoleMessage::Log {
5080                level: "info".into(),
5081                text: "hello".into(),
5082            })
5083            .unwrap();
5084        let msg = rx.try_recv().unwrap();
5085        match msg {
5086            ConsoleMessage::Log { level, text } => {
5087                assert_eq!(level, "info");
5088                assert_eq!(text, "hello");
5089            }
5090            ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5091        }
5092    }
5093
5094    #[test]
5095    fn test_webview_state_console_log_tx_propagation() {
5096        let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5097        let mut state = BaoWebViewState::default();
5098        state.console_log_tx = Some(tx);
5099        // Simulate what show_console_message does
5100        if let Some(ref tx) = state.console_log_tx {
5101            tx.send(ConsoleMessage::Log {
5102                level: "warning".into(),
5103                text: "test message".into(),
5104            })
5105            .unwrap();
5106        }
5107        let msg = rx.try_recv().unwrap();
5108        match msg {
5109            ConsoleMessage::Log { level, text } => {
5110                assert_eq!(level, "warning");
5111                assert_eq!(text, "test message");
5112            }
5113            ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5114        }
5115    }
5116
5117    #[test]
5118    fn test_webview_state_console_log_tx_default_none() {
5119        let state = BaoWebViewState::default();
5120        assert!(state.console_log_tx.is_none());
5121    }
5122
5123    #[test]
5124    fn test_console_log_all_level_mappings() {
5125        let delegate = BaoServoDelegate::new();
5126        let (tx, _rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5127        delegate.set_console_log_tx(tx);
5128
5129        // Verify all ConsoleLogLevel variants map correctly via the delegate's show_console_message
5130        // We test the level mapping logic directly by checking the match arms
5131        let cases: Vec<(ConsoleLogLevel, &str)> = vec![
5132            (ConsoleLogLevel::Debug, "debug"),
5133            (ConsoleLogLevel::Log, "info"),
5134            (ConsoleLogLevel::Info, "info"),
5135            (ConsoleLogLevel::Warn, "warning"),
5136            (ConsoleLogLevel::Error, "error"),
5137            (ConsoleLogLevel::Trace, "verbose"),
5138            (ConsoleLogLevel::Dir, "info"),
5139        ];
5140        for (level, expected_str) in cases {
5141            let mapped = match level {
5142                ConsoleLogLevel::Debug => "debug",
5143                ConsoleLogLevel::Log => "info",
5144                ConsoleLogLevel::Info => "info",
5145                ConsoleLogLevel::Warn => "warning",
5146                ConsoleLogLevel::Error => "error",
5147                ConsoleLogLevel::Trace => "verbose",
5148                ConsoleLogLevel::Dir => "info",
5149            };
5150            assert_eq!(
5151                mapped, expected_str,
5152                "level {:?} should map to {}",
5153                level, expected_str
5154            );
5155        }
5156    }
5157
5158    #[test]
5159    fn test_webview_delegate_console_log_forwarding() {
5160        let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5161        let state = Rc::new(RefCell::new(BaoWebViewState {
5162            console_log_tx: Some(tx),
5163            ..Default::default()
5164        }));
5165        let viewport = PhysicalSize::new(800, 600);
5166        let _delegate = BaoWebViewDelegate::new(state, viewport);
5167
5168        // Simulate sending through state's channel (what show_console_message does)
5169        if let Some(ref tx) = _delegate.state().borrow().console_log_tx {
5170            tx.send(ConsoleMessage::Log {
5171                level: "error".into(),
5172                text: "crash!".into(),
5173            })
5174            .unwrap();
5175        }
5176        let msg = rx.try_recv().unwrap();
5177        match msg {
5178            ConsoleMessage::Log { level, text } => {
5179                assert_eq!(level, "error");
5180                assert_eq!(text, "crash!");
5181            }
5182            ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5183        }
5184    }
5185
5186    // ─── PageFrameNavigated delegate emission ────────────────────────
5187    // @trace REQ-CDP-007 [req:REQ-CDP-007] [level:unit]
5188
5189    #[test]
5190    fn test_notify_url_changed_emits_frame_navigated() {
5191        let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5192        let state = Rc::new(RefCell::new(BaoWebViewState {
5193            console_log_tx: Some(tx),
5194            ..Default::default()
5195        }));
5196        let viewport = PhysicalSize::new(800, 600);
5197        let _delegate = BaoWebViewDelegate::new(state.clone(), viewport);
5198
5199        // Simulate notify_url_changed by sending the same message the method sends
5200        let url = url::Url::parse("https://example.com").unwrap();
5201        let url_str = url.to_string();
5202        let loader_id = format!("{:016x}", url_str.len() as u64);
5203        if let Some(ref tx) = state.borrow().console_log_tx {
5204            tx.send(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
5205                frame_id: "0".to_string(),
5206                url: url_str.clone(),
5207                loader_id: loader_id.clone(),
5208            }))
5209            .unwrap();
5210        }
5211
5212        let msg = rx.try_recv().unwrap();
5213        match msg {
5214            ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
5215                frame_id,
5216                url,
5217                loader_id: lid,
5218            }) => {
5219                assert_eq!(frame_id, "0");
5220                assert!(url.starts_with("https://example.com"));
5221                assert_eq!(lid, loader_id);
5222            }
5223            other => panic!("expected PageFrameNavigated, got {:?}", other),
5224        }
5225    }
5226
5227    // ─── SecurityCertificateError delegate emission ──────────────────
5228    // @trace REQ-CDP-007 [req:REQ-CDP-007] [level:unit]
5229
5230    #[test]
5231    fn test_notify_error_certificate_error_emits_security_event() {
5232        let delegate = BaoServoDelegate::new();
5233        let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5234        delegate.set_console_log_tx(tx);
5235
5236        // Simulate a certificate error by sending the same message notify_error would send
5237        if let Some(ref tx) = *delegate.console_log_tx.borrow() {
5238            tx.send(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
5239                event_id: 0,
5240                error_type: "net::ERR_CERT_AUTHORITY_INVALID".to_string(),
5241                url: String::new(),
5242            }))
5243            .unwrap();
5244        }
5245
5246        let msg = rx.try_recv().unwrap();
5247        match msg {
5248            ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
5249                event_id,
5250                error_type,
5251                url,
5252            }) => {
5253                assert_eq!(event_id, 0);
5254                assert_eq!(error_type, "net::ERR_CERT_AUTHORITY_INVALID");
5255                assert_eq!(url, "");
5256            }
5257            other => panic!("expected SecurityCertificateError, got {:?}", other),
5258        }
5259    }
5260
5261    // ─── EventSubscriber (event_tx) Path B ─────────────────────────────
5262    // @trace REQ-CDP-006 [req:REQ-CDP-006] [level:unit]
5263
5264    #[test]
5265    fn test_servo_delegate_event_tx_set_and_get() {
5266        let delegate = BaoServoDelegate::new();
5267        assert!(delegate.event_tx().is_none());
5268        let (tx, _rx) = std::sync::mpsc::channel::<ServoEvent>();
5269        delegate.set_event_tx(tx);
5270        assert!(delegate.event_tx().is_some());
5271    }
5272
5273    #[test]
5274    fn test_servo_delegate_event_tx_sends_console_event() {
5275        let delegate = BaoServoDelegate::new();
5276        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5277        delegate.set_event_tx(tx);
5278
5279        // When event_tx is set, show_console_message pushes ServoEvent::Console
5280        if let Some(ref tx) = delegate.event_tx() {
5281            tx.send(ServoEvent::Console {
5282                target_id: "0".to_string(),
5283                level: ConsoleLevel::Info,
5284                text: "hello".to_string(),
5285                url: None,
5286                line: None,
5287                column: None,
5288            })
5289            .unwrap();
5290        }
5291
5292        let event = rx.try_recv().unwrap();
5293        match event {
5294            ServoEvent::Console { level, text, .. } => {
5295                assert_eq!(level, ConsoleLevel::Info);
5296                assert_eq!(text, "hello");
5297            }
5298            _ => panic!("expected Console event"),
5299        }
5300    }
5301
5302    #[test]
5303    fn test_webview_state_event_tx_default_none() {
5304        let state = BaoWebViewState::default();
5305        assert!(state.event_tx.is_none());
5306    }
5307
5308    #[test]
5309    fn test_webview_state_event_tx_propagation() {
5310        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5311        let mut state = BaoWebViewState::default();
5312        state.event_tx = Some(tx);
5313        // Simulate what notify_url_changed does with event_tx
5314        if let Some(ref tx) = state.event_tx {
5315            tx.send(ServoEvent::FrameNavigated {
5316                target_id: "0".to_string(),
5317                frame_id: "0".to_string(),
5318                url: "https://example.com/".to_string(),
5319                name: None,
5320            })
5321            .unwrap();
5322        }
5323        let event = rx.try_recv().unwrap();
5324        match event {
5325            ServoEvent::FrameNavigated { url, .. } => {
5326                assert_eq!(url, "https://example.com/");
5327            }
5328            _ => panic!("expected FrameNavigated event"),
5329        }
5330    }
5331
5332    #[test]
5333    fn test_event_tx_console_level_mapping() {
5334        // Verify ConsoleLogLevel → ConsoleLevel mapping matches the delegate logic
5335        let cases: Vec<(ConsoleLogLevel, ConsoleLevel)> = vec![
5336            (ConsoleLogLevel::Debug, ConsoleLevel::Debug),
5337            (ConsoleLogLevel::Log, ConsoleLevel::Info),
5338            (ConsoleLogLevel::Info, ConsoleLevel::Info),
5339            (ConsoleLogLevel::Warn, ConsoleLevel::Warning),
5340            (ConsoleLogLevel::Error, ConsoleLevel::Error),
5341            (ConsoleLogLevel::Trace, ConsoleLevel::Verbose),
5342        ];
5343        for (servo_level, expected) in cases {
5344            let mapped = match servo_level {
5345                ConsoleLogLevel::Debug => ConsoleLevel::Debug,
5346                ConsoleLogLevel::Log => ConsoleLevel::Info,
5347                ConsoleLogLevel::Info => ConsoleLevel::Info,
5348                ConsoleLogLevel::Warn => ConsoleLevel::Warning,
5349                ConsoleLogLevel::Error => ConsoleLevel::Error,
5350                ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
5351                ConsoleLogLevel::Dir => ConsoleLevel::Info,
5352            };
5353            assert_eq!(
5354                mapped, expected,
5355                "servo {:?} should map to {:?}",
5356                servo_level, expected
5357            );
5358        }
5359    }
5360
5361    #[test]
5362    fn test_notify_load_started_emits_frame_started_loading() {
5363        // When event_tx is set and LoadStatus::Started is received,
5364        // the delegate should emit ServoEvent::FrameStartedLoading.
5365        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5366        let state = Rc::new(RefCell::new(BaoWebViewState {
5367            event_tx: Some(tx),
5368            ..Default::default()
5369        }));
5370        let viewport = PhysicalSize::new(800, 600);
5371        let _delegate = BaoWebViewDelegate::new(state.clone(), viewport);
5372
5373        // Simulate what notify_load_status_changed does on LoadStatus::Started
5374        if let Some(ref tx) = state.borrow().event_tx {
5375            tx.send(ServoEvent::FrameStartedLoading {
5376                target_id: "0".to_string(),
5377                frame_id: "0".to_string(),
5378            })
5379            .unwrap();
5380        }
5381
5382        let event = rx.try_recv().unwrap();
5383        match event {
5384            ServoEvent::FrameStartedLoading {
5385                target_id,
5386                frame_id,
5387            } => {
5388                assert_eq!(target_id, "0");
5389                assert_eq!(frame_id, "0");
5390            }
5391            _ => panic!("expected FrameStartedLoading event"),
5392        }
5393    }
5394
5395    // ─── Worker Lifecycle (REQ-BRW-004) ──────────────────────────────
5396    // @trace REQ-BRW-004 [req:REQ-BRW-004] [level:unit]
5397
5398    #[test]
5399    fn test_worker_handle_new_is_running() {
5400        let handle = WorkerHandle::new("https://example.com/worker.js".to_string());
5401        assert_eq!(handle.script_url, "https://example.com/worker.js");
5402        assert!(!handle.is_closing());
5403        assert!(!handle.is_terminated());
5404    }
5405
5406    #[test]
5407    fn test_worker_handle_terminate_sets_closing() {
5408        let handle = WorkerHandle::new("worker.js".to_string());
5409        assert!(!handle.is_closing());
5410        handle.terminate();
5411        assert!(handle.is_closing());
5412        // Idempotent
5413        handle.terminate();
5414        assert!(handle.is_closing());
5415    }
5416
5417    #[test]
5418    fn test_worker_handle_mark_terminated() {
5419        let handle = WorkerHandle::new("worker.js".to_string());
5420        assert!(!handle.is_terminated());
5421        handle.mark_terminated();
5422        assert!(handle.is_terminated());
5423    }
5424
5425    #[test]
5426    fn test_worker_handle_terminate_then_terminated() {
5427        let handle = WorkerHandle::new("worker.js".to_string());
5428        handle.terminate();
5429        assert!(handle.is_closing());
5430        assert!(!handle.is_terminated());
5431        handle.mark_terminated();
5432        assert!(handle.is_terminated());
5433    }
5434
5435    #[test]
5436    fn test_worker_handle_clone_shares_state() {
5437        let handle = WorkerHandle::new("worker.js".to_string());
5438        let clone = handle.clone();
5439        handle.terminate();
5440        assert!(
5441            clone.is_closing(),
5442            "clone should see closing flag from original"
5443        );
5444        clone.mark_terminated();
5445        assert!(
5446            handle.is_terminated(),
5447            "original should see terminated flag from clone"
5448        );
5449    }
5450
5451    #[test]
5452    fn test_webview_state_active_workers_default_empty() {
5453        let state = BaoWebViewState::default();
5454        assert!(state.active_workers.is_empty());
5455        assert_eq!(state.active_worker_count(), 0);
5456    }
5457
5458    #[test]
5459    fn test_webview_state_track_worker() {
5460        let mut state = BaoWebViewState::default();
5461        let handle = WorkerHandle::new("worker1.js".to_string());
5462        state.track_worker(handle);
5463        assert_eq!(state.active_worker_count(), 1);
5464        assert_eq!(state.active_workers.len(), 1);
5465        assert_eq!(state.active_workers[0].handle().script_url, "worker1.js");
5466    }
5467
5468    #[test]
5469    fn test_webview_state_track_multiple_workers() {
5470        let mut state = BaoWebViewState::default();
5471        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5472        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5473        state.track_worker(WorkerHandle::new("worker3.js".to_string()));
5474        assert_eq!(state.active_worker_count(), 3);
5475    }
5476
5477    #[test]
5478    fn test_webview_state_terminate_all_workers() {
5479        let mut state = BaoWebViewState::default();
5480        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5481        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5482        assert!(!state.active_workers[0].handle().is_closing());
5483        assert!(!state.active_workers[1].handle().is_closing());
5484        state.terminate_all_workers();
5485        assert!(state.active_workers[0].handle().is_closing());
5486        assert!(state.active_workers[1].handle().is_closing());
5487    }
5488
5489    #[test]
5490    fn test_webview_state_reap_terminated_workers() {
5491        let mut state = BaoWebViewState::default();
5492        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5493        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5494        // Terminate only worker1
5495        state.active_workers[0].handle().terminate();
5496        state.active_workers[0].handle().mark_terminated();
5497        assert_eq!(state.active_worker_count(), 1);
5498        state.reap_terminated_workers();
5499        assert_eq!(state.active_workers.len(), 1);
5500        assert_eq!(state.active_workers[0].handle().script_url, "worker2.js");
5501    }
5502
5503    #[test]
5504    fn test_webview_state_reap_all_terminated() {
5505        let mut state = BaoWebViewState::default();
5506        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5507        // terminate_all_workers() marks workers as terminated (Phase 3)
5508        state.terminate_all_workers();
5509        // reap_terminated_workers cleans up the tracking state
5510        state.reap_terminated_workers();
5511        assert!(state.active_workers.is_empty());
5512        assert_eq!(state.active_worker_count(), 0);
5513    }
5514
5515    #[test]
5516    fn test_worker_id_equality() {
5517        let id1 = WorkerId("worker1.js".to_string());
5518        let id2 = WorkerId("worker1.js".to_string());
5519        let id3 = WorkerId("worker2.js".to_string());
5520        assert_eq!(id1, id2);
5521        assert_ne!(id1, id3);
5522    }
5523
5524    #[test]
5525    fn test_worker_message_direction() {
5526        assert_eq!(
5527            WorkerMessageDirection::PageToWorker,
5528            WorkerMessageDirection::PageToWorker
5529        );
5530        assert_ne!(
5531            WorkerMessageDirection::PageToWorker,
5532            WorkerMessageDirection::WorkerToPage
5533        );
5534    }
5535
5536    #[test]
5537    fn test_worker_message_event_creation() {
5538        let event = WorkerMessageEvent {
5539            worker_id: WorkerId("worker1.js".to_string()),
5540            direction: WorkerMessageDirection::PageToWorker,
5541        };
5542        assert_eq!(event.worker_id.0, "worker1.js");
5543        assert_eq!(event.direction, WorkerMessageDirection::PageToWorker);
5544    }
5545
5546    #[test]
5547    fn test_webview_state_forward_worker_message_to_event_tx() {
5548        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5549        let state = BaoWebViewState {
5550            event_tx: Some(tx),
5551            ..Default::default()
5552        };
5553        let msg = WorkerMessageEvent {
5554            worker_id: WorkerId("worker1.js".to_string()),
5555            direction: WorkerMessageDirection::WorkerToPage,
5556        };
5557        state.forward_worker_message_event(msg);
5558        let event = rx.try_recv().unwrap();
5559        match event {
5560            ServoEvent::Console { level, text, .. } => {
5561                assert_eq!(level, ConsoleLevel::Debug);
5562                assert!(text.contains("worker→page"));
5563                assert!(text.contains("worker1.js"));
5564            }
5565            _ => panic!("expected Console event for worker message"),
5566        }
5567    }
5568
5569    #[test]
5570    fn test_webview_state_forward_worker_message_no_event_tx() {
5571        // When event_tx is None, forward_worker_message_event should be a no-op
5572        let state = BaoWebViewState::default();
5573        let msg = WorkerMessageEvent {
5574            worker_id: WorkerId("worker1.js".to_string()),
5575            direction: WorkerMessageDirection::PageToWorker,
5576        };
5577        // Should not panic
5578        state.forward_worker_message_event(msg);
5579    }
5580
5581    #[test]
5582    fn test_terminate_on_navigation_then_reap() {
5583        // Simulate: page with workers → new navigation → terminate → load complete → reap
5584        let mut state = BaoWebViewState::default();
5585        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5586        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5587        assert_eq!(state.active_worker_count(), 2);
5588
5589        // Navigation starts: terminate all
5590        // terminate_all_workers() performs 3 phases:
5591        //   Phase 1: set closing + unregister stealth profiles
5592        //   Phase 2: web_workers.clear() (join threads)
5593        //   Phase 3: mark terminated (threads have exited)
5594        state.terminate_all_workers();
5595        assert!(state.active_workers[0].handle().is_closing());
5596        assert!(state.active_workers[1].handle().is_closing());
5597        // After terminate_all_workers(), workers are marked terminated
5598        // (Phase 3 runs after web_workers.clear() joins threads).
5599        assert_eq!(state.active_worker_count(), 0);
5600
5601        // Load complete: reap
5602        state.reap_terminated_workers();
5603        assert!(state.active_workers.is_empty());
5604    }
5605
5606    // ─── WorkerErrorEvent (REQ-BRW-004 criterion #9) ──────────────────
5607    // @trace REQ-BRW-004 [req:REQ-BRW-004] [criterion:9] [level:unit]
5608
5609    #[test]
5610    fn test_worker_error_event_creation() {
5611        let event = WorkerErrorEvent {
5612            worker_id: WorkerId("worker1.js".to_string()),
5613            message: "Uncaught TypeError: x is not a function".to_string(),
5614            filename: "worker1.js".to_string(),
5615            lineno: 42,
5616            colno: 5,
5617        };
5618        assert_eq!(event.worker_id.0, "worker1.js");
5619        assert_eq!(event.message, "Uncaught TypeError: x is not a function");
5620        assert_eq!(event.filename, "worker1.js");
5621        assert_eq!(event.lineno, 42);
5622        assert_eq!(event.colno, 5);
5623    }
5624
5625    #[test]
5626    fn test_webview_state_forward_worker_error_to_event_tx() {
5627        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5628        let state = BaoWebViewState {
5629            event_tx: Some(tx),
5630            ..Default::default()
5631        };
5632        let error = WorkerErrorEvent {
5633            worker_id: WorkerId("worker1.js".to_string()),
5634            message: "Uncaught Error: boom".to_string(),
5635            filename: "worker1.js".to_string(),
5636            lineno: 10,
5637            colno: 3,
5638        };
5639        state.forward_worker_error_event(error);
5640        let event = rx.try_recv().unwrap();
5641        match event {
5642            ServoEvent::PageError {
5643                text,
5644                url,
5645                line,
5646                column,
5647                ..
5648            } => {
5649                assert!(text.contains("worker1.js"));
5650                assert!(text.contains("Uncaught Error: boom"));
5651                assert_eq!(url.as_deref(), Some("worker1.js"));
5652                assert_eq!(line, Some(10));
5653                assert_eq!(column, Some(3));
5654            }
5655            _ => panic!("expected PageError event for worker error"),
5656        }
5657    }
5658
5659    #[test]
5660    fn test_webview_state_forward_worker_error_no_event_tx() {
5661        let state = BaoWebViewState::default();
5662        let error = WorkerErrorEvent {
5663            worker_id: WorkerId("worker1.js".to_string()),
5664            message: "error".to_string(),
5665            filename: "worker1.js".to_string(),
5666            lineno: 1,
5667            colno: 1,
5668        };
5669        // Should not panic
5670        state.forward_worker_error_event(error);
5671    }
5672
5673    // ─── WorkerLifecycleState / WorkerTeardownPath (REQ-BRW-004 criterion #18) ──
5674    // @trace REQ-BRW-004 [req:REQ-BRW-004] [criterion:18] [level:unit]
5675
5676    #[test]
5677    fn test_worker_teardown_path_equality() {
5678        assert_eq!(WorkerTeardownPath::Terminate, WorkerTeardownPath::Terminate);
5679        assert_eq!(WorkerTeardownPath::SelfClose, WorkerTeardownPath::SelfClose);
5680        assert_eq!(
5681            WorkerTeardownPath::PageUnload,
5682            WorkerTeardownPath::PageUnload
5683        );
5684        assert_ne!(WorkerTeardownPath::Terminate, WorkerTeardownPath::SelfClose);
5685    }
5686
5687    #[test]
5688    fn test_worker_lifecycle_state_running() {
5689        let handle = WorkerHandle::new("worker.js".to_string());
5690        let guard = AutoCloseWorker::new(handle);
5691        assert_eq!(guard.lifecycle_state(), WorkerLifecycleState::Running);
5692    }
5693
5694    #[test]
5695    fn test_worker_lifecycle_state_closing() {
5696        let handle = WorkerHandle::new("worker.js".to_string());
5697        let mut guard = AutoCloseWorker::new(handle);
5698        guard.terminate_via(WorkerTeardownPath::Terminate);
5699        assert_eq!(
5700            guard.lifecycle_state(),
5701            WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5702        );
5703    }
5704
5705    #[test]
5706    fn test_worker_lifecycle_state_terminated() {
5707        let handle = WorkerHandle::new("worker.js".to_string());
5708        let mut guard = AutoCloseWorker::new(handle);
5709        guard.terminate_via(WorkerTeardownPath::SelfClose);
5710        guard.handle().mark_terminated();
5711        assert_eq!(
5712            guard.lifecycle_state(),
5713            WorkerLifecycleState::Terminated(WorkerTeardownPath::SelfClose)
5714        );
5715    }
5716
5717    #[test]
5718    fn test_worker_lifecycle_states_snapshot() {
5719        let mut state = BaoWebViewState::default();
5720        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5721        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5722        let snapshot = state.worker_lifecycle_states();
5723        assert_eq!(snapshot.len(), 2);
5724        assert_eq!(snapshot[0].0, WorkerId("worker1.js".to_string()));
5725        assert_eq!(snapshot[0].1, WorkerLifecycleState::Running);
5726        assert_eq!(snapshot[1].0, WorkerId("worker2.js".to_string()));
5727        assert_eq!(snapshot[1].1, WorkerLifecycleState::Running);
5728    }
5729
5730    // ─── AutoCloseWorker (REQ-BRW-004 criterion #10) ─────────────────
5731    // @trace REQ-BRW-004 [req:REQ-BRW-004] [criterion:10] [level:unit]
5732
5733    #[test]
5734    fn test_auto_close_worker_new_is_running() {
5735        let handle = WorkerHandle::new("worker.js".to_string());
5736        let guard = AutoCloseWorker::new(handle);
5737        assert!(!guard.handle().is_closing());
5738        assert!(!guard.handle().is_terminated());
5739    }
5740
5741    #[test]
5742    fn test_auto_close_worker_terminate_via() {
5743        let handle = WorkerHandle::new("worker.js".to_string());
5744        let mut guard = AutoCloseWorker::new(handle);
5745        guard.terminate_via(WorkerTeardownPath::Terminate);
5746        assert!(guard.handle().is_closing());
5747        assert_eq!(
5748            guard.lifecycle_state(),
5749            WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5750        );
5751    }
5752
5753    #[test]
5754    fn test_auto_close_worker_terminate_via_idempotent() {
5755        let handle = WorkerHandle::new("worker.js".to_string());
5756        let mut guard = AutoCloseWorker::new(handle);
5757        guard.terminate_via(WorkerTeardownPath::Terminate);
5758        guard.terminate_via(WorkerTeardownPath::SelfClose);
5759        // Should still be Terminate (first call wins)
5760        assert_eq!(
5761            guard.lifecycle_state(),
5762            WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5763        );
5764    }
5765
5766    #[test]
5767    fn test_auto_close_worker_drop_terminates() {
5768        let handle = WorkerHandle::new("worker.js".to_string());
5769        let handle_clone = handle.clone();
5770        let guard = AutoCloseWorker::new(handle);
5771        assert!(!handle_clone.is_closing());
5772        drop(guard);
5773        // AutoCloseWorker::drop should terminate the worker
5774        assert!(handle_clone.is_closing());
5775    }
5776
5777    #[test]
5778    fn test_auto_close_worker_drop_already_closing() {
5779        let handle = WorkerHandle::new("worker.js".to_string());
5780        let handle_clone = handle.clone();
5781        let mut guard = AutoCloseWorker::new(handle);
5782        guard.terminate_via(WorkerTeardownPath::Terminate);
5783        drop(guard);
5784        // Already closing — drop should not change teardown path
5785        assert!(handle_clone.is_closing());
5786        // @trace REQ-BRW-004 [criterion:18] crash-safe teardown: drop marks terminated
5787        assert!(handle_clone.is_terminated());
5788    }
5789
5790    // ─── Crash-Safe Teardown Tests (REQ-BRW-004 criterion #18) ──────────
5791    // @trace REQ-BRW-004 [criterion:18] crash-safe teardown zero-crash zero-leak
5792
5793    #[test]
5794    fn test_worker_handle_global_addr_default_zero() {
5795        let handle = WorkerHandle::new("worker.js".to_string());
5796        assert_eq!(handle.worker_global_addr(), 0);
5797    }
5798
5799    #[test]
5800    fn test_worker_handle_global_addr_set_and_get() {
5801        let handle = WorkerHandle::new("worker.js".to_string());
5802        handle.set_worker_global_addr(0xDEADBEEF);
5803        assert_eq!(handle.worker_global_addr(), 0xDEADBEEF);
5804    }
5805
5806    #[test]
5807    fn test_worker_handle_global_addr_arc_shared() {
5808        let handle = WorkerHandle::new("worker.js".to_string());
5809        let arc = handle.worker_global_addr_arc();
5810        // Write via the Arc (as scope_init would on the worker thread)
5811        arc.store(0xCAFEBABE_usize as u64, Ordering::Release);
5812        // Read via the handle (as teardown would on the main thread)
5813        assert_eq!(handle.worker_global_addr(), 0xCAFEBABE);
5814    }
5815
5816    #[test]
5817    fn test_worker_handle_unregister_stealth_profile_no_addr() {
5818        // When no global address is set, unregister should be a no-op
5819        let handle = WorkerHandle::new("worker.js".to_string());
5820        // Should not panic
5821        handle.unregister_stealth_profile();
5822    }
5823
5824    #[test]
5825    fn test_worker_handle_unregister_stealth_profile_with_addr() {
5826        // Register a profile for a fake global address, then unregister it
5827        let fake_addr = 0x12345678_usize;
5828        bao_stealth::engine_props::set_profile_for_global(
5829            fake_addr,
5830            &bao_stealth::StealthProfile::firefox_default(),
5831        );
5832        // Verify it's registered
5833        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
5834        // Unregister via WorkerHandle
5835        let handle = WorkerHandle::new("worker.js".to_string());
5836        handle.set_worker_global_addr(fake_addr);
5837        handle.unregister_stealth_profile();
5838        // Verify it's gone
5839        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5840        // Cleanup (in case test fails before unregister)
5841        bao_stealth::engine_props::clear_all_realm_profiles();
5842    }
5843
5844    #[test]
5845    fn test_teardown_result_crash_safe() {
5846        let result = WorkerTeardownResult {
5847            path: WorkerTeardownPath::Terminate,
5848            thread_joined: true,
5849            realm_profile_unregistered: true,
5850            closing_flag_set: true,
5851            never_registered: false,
5852        };
5853        assert!(result.is_crash_safe());
5854    }
5855
5856    #[test]
5857    fn test_teardown_result_not_crash_safe_no_join() {
5858        let result = WorkerTeardownResult {
5859            path: WorkerTeardownPath::PageUnload,
5860            thread_joined: false,
5861            realm_profile_unregistered: true,
5862            closing_flag_set: true,
5863            never_registered: false,
5864        };
5865        assert!(!result.is_crash_safe());
5866    }
5867
5868    #[test]
5869    fn test_teardown_result_not_crash_safe_no_closing() {
5870        let result = WorkerTeardownResult {
5871            path: WorkerTeardownPath::SelfClose,
5872            thread_joined: true,
5873            realm_profile_unregistered: true,
5874            closing_flag_set: false,
5875            never_registered: false,
5876        };
5877        assert!(!result.is_crash_safe());
5878    }
5879
5880    #[test]
5881    fn test_crash_safe_teardown_no_web_worker() {
5882        // Test crash-safe teardown for a servo DOM Worker (DEC-WK-001 native path)
5883        let handle = WorkerHandle::new("worker.js".to_string());
5884        let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::Terminate);
5885        assert!(result.closing_flag_set);
5886        assert!(result.thread_joined); // servo DOM Worker — considered joined
5887        assert!(!result.realm_profile_unregistered); // no global addr set
5888        assert!(handle.is_closing());
5889        assert!(handle.is_terminated());
5890    }
5891
5892    #[test]
5893    fn test_crash_safe_teardown_with_stealth_profile() {
5894        // Register a profile for a fake global address, then crash-safe teardown
5895        let fake_addr = 0xABCD0000_usize;
5896        bao_stealth::engine_props::set_profile_for_global(
5897            fake_addr,
5898            &bao_stealth::StealthProfile::firefox_default(),
5899        );
5900
5901        let handle = WorkerHandle::new("worker.js".to_string());
5902        handle.set_worker_global_addr(fake_addr);
5903
5904        let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::SelfClose);
5905        assert!(result.closing_flag_set);
5906        assert!(result.thread_joined);
5907        assert!(result.realm_profile_unregistered);
5908        // Profile should be unregistered
5909        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5910        assert!(handle.is_closing());
5911        assert!(handle.is_terminated());
5912    }
5913
5914    #[test]
5915    fn test_auto_close_worker_drop_unregisters_stealth_profile() {
5916        // @trace REQ-BRW-004 [criterion:18] AutoCloseWorker::drop unregisters REALM_PROFILES
5917        let fake_addr = 0xBEEF0000_usize;
5918        bao_stealth::engine_props::set_profile_for_global(
5919            fake_addr,
5920            &bao_stealth::StealthProfile::firefox_default(),
5921        );
5922
5923        let handle = WorkerHandle::new("worker.js".to_string());
5924        handle.set_worker_global_addr(fake_addr);
5925        // Verify profile is registered
5926        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
5927
5928        let guard = AutoCloseWorker::new(handle);
5929        // Drop the guard — should unregister the profile
5930        drop(guard);
5931        // Profile should be gone
5932        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5933    }
5934
5935    #[test]
5936    fn test_terminate_all_workers_unregisters_stealth_profiles() {
5937        // @trace REQ-BRW-004 [criterion:18] terminate_all_workers unregisters REALM_PROFILES
5938        let fake_addr1 = 0xAAAA0001_usize;
5939        let fake_addr2 = 0xAAAA0002_usize;
5940        bao_stealth::engine_props::set_profile_for_global(
5941            fake_addr1,
5942            &bao_stealth::StealthProfile::firefox_default(),
5943        );
5944        bao_stealth::engine_props::set_profile_for_global(
5945            fake_addr2,
5946            &bao_stealth::StealthProfile::firefox_default(),
5947        );
5948
5949        let mut state = BaoWebViewState::default();
5950        let h1 = WorkerHandle::new("worker1.js".to_string());
5951        h1.set_worker_global_addr(fake_addr1);
5952        let h2 = WorkerHandle::new("worker2.js".to_string());
5953        h2.set_worker_global_addr(fake_addr2);
5954        state.track_worker(h1);
5955        state.track_worker(h2);
5956
5957        // Verify profiles registered
5958        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_some());
5959        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_some());
5960
5961        // Terminate all — should unregister all profiles
5962        state.terminate_all_workers();
5963
5964        // Profiles should be gone
5965        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_none());
5966        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_none());
5967        // All workers should be closing and terminated
5968        assert!(state.active_workers.iter().all(|g| g.handle().is_closing()));
5969        assert!(state
5970            .active_workers
5971            .iter()
5972            .all(|g| g.handle().is_terminated()));
5973    }
5974
5975    #[test]
5976    fn test_terminate_worker_via_path_terminate() {
5977        // @trace REQ-BRW-004 [criterion:4] [criterion:18] worker.terminate() path
5978        let fake_addr = 0xCCCC0001_usize;
5979        bao_stealth::engine_props::set_profile_for_global(
5980            fake_addr,
5981            &bao_stealth::StealthProfile::firefox_default(),
5982        );
5983
5984        let mut state = BaoWebViewState::default();
5985        let handle = WorkerHandle::new("worker.js".to_string());
5986        handle.set_worker_global_addr(fake_addr);
5987        let worker_id = WorkerId("worker.js".to_string());
5988        state.track_worker(handle.clone());
5989
5990        let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
5991        assert!(result.is_some());
5992        let result = result.unwrap();
5993        assert_eq!(result.path, WorkerTeardownPath::Terminate);
5994        assert!(result.closing_flag_set);
5995        assert!(result.thread_joined);
5996        assert!(result.realm_profile_unregistered);
5997        // Profile should be gone
5998        assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5999        // Worker should be terminated
6000        assert!(handle.is_closing());
6001        assert!(handle.is_terminated());
6002    }
6003
6004    #[test]
6005    fn test_terminate_worker_via_path_self_close() {
6006        // @trace REQ-BRW-004 [criterion:5] [criterion:18] self.close() path
6007        let fake_addr = 0xDDDD0001_usize;
6008        bao_stealth::engine_props::set_profile_for_global(
6009            fake_addr,
6010            &bao_stealth::StealthProfile::firefox_default(),
6011        );
6012
6013        let mut state = BaoWebViewState::default();
6014        let handle = WorkerHandle::new("worker.js".to_string());
6015        handle.set_worker_global_addr(fake_addr);
6016        let worker_id = WorkerId("worker.js".to_string());
6017        state.track_worker(handle.clone());
6018
6019        let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::SelfClose);
6020        assert!(result.is_some());
6021        let result = result.unwrap();
6022        assert_eq!(result.path, WorkerTeardownPath::SelfClose);
6023        assert!(result.closing_flag_set);
6024        assert!(result.realm_profile_unregistered);
6025    }
6026
6027    #[test]
6028    fn test_terminate_worker_via_path_not_found() {
6029        let mut state = BaoWebViewState::default();
6030        let worker_id = WorkerId("nonexistent.js".to_string());
6031        let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
6032        assert!(result.is_none());
6033    }
6034
6035    #[test]
6036    fn test_three_paths_all_crash_safe() {
6037        // @trace REQ-BRW-004 [criterion:18] all three teardown paths crash-safe
6038        for path in [
6039            WorkerTeardownPath::Terminate,
6040            WorkerTeardownPath::SelfClose,
6041            WorkerTeardownPath::PageUnload,
6042        ] {
6043            let fake_addr = 0x12340000_usize
6044                + match &path {
6045                    WorkerTeardownPath::Terminate => 1,
6046                    WorkerTeardownPath::SelfClose => 2,
6047                    WorkerTeardownPath::PageUnload => 3,
6048                };
6049            bao_stealth::engine_props::set_profile_for_global(
6050                fake_addr,
6051                &bao_stealth::StealthProfile::firefox_default(),
6052            );
6053
6054            let handle = WorkerHandle::new("worker.js".to_string());
6055            handle.set_worker_global_addr(fake_addr);
6056            let result = crash_safe_teardown_worker(&handle, path.clone());
6057            assert!(
6058                result.closing_flag_set,
6059                "closing flag not set for {:?}",
6060                path
6061            );
6062            assert!(result.thread_joined, "thread not joined for {:?}", path);
6063            assert!(
6064                result.realm_profile_unregistered,
6065                "profile not unregistered for {:?}",
6066                path
6067            );
6068            assert!(result.is_crash_safe(), "not crash-safe for {:?}", path);
6069            assert!(handle.is_closing(), "handle not closing for {:?}", path);
6070            assert!(
6071                handle.is_terminated(),
6072                "handle not terminated for {:?}",
6073                path
6074            );
6075            assert!(
6076                bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none(),
6077                "profile not removed for {:?}",
6078                path
6079            );
6080        }
6081    }
6082
6083    #[test]
6084    fn test_track_worker_guard() {
6085        let handle = WorkerHandle::new("worker.js".to_string());
6086        let guard = AutoCloseWorker::new(handle);
6087        let mut state = BaoWebViewState::default();
6088        state.track_worker_guard(guard);
6089        assert_eq!(state.active_worker_count(), 1);
6090    }
6091
6092    // ─── WorkerScopeConfig (REQ-BRW-004 criteria #12-17) ─────────────
6093    // @trace REQ-BRW-004 [req:REQ-BRW-004] [criterion:12..17] [level:unit]
6094
6095    #[test]
6096    fn test_worker_scope_config_default() {
6097        let config = WorkerScopeConfig::default();
6098        assert!(config.stealth_profile.is_none());
6099        assert!(config.user_agent.is_empty());
6100        assert!(config.platform.is_empty());
6101        assert!(config.hardware_concurrency > 0);
6102        assert_eq!(config.language, "en-US");
6103        assert!(!config.languages.is_empty());
6104    }
6105
6106    #[test]
6107    fn test_worker_scope_config_set_on_state() {
6108        let mut state = BaoWebViewState::default();
6109        let config = WorkerScopeConfig {
6110            stealth_profile: None,
6111            user_agent: "Bao/1.0".to_string(),
6112            platform: "Linux x86_64".to_string(),
6113            hardware_concurrency: 8,
6114            language: "zh-CN".to_string(),
6115            languages: vec!["zh-CN".to_string(), "zh".to_string(), "en".to_string()],
6116        };
6117        state.set_worker_scope_config(config);
6118        assert_eq!(state.worker_scope_config.user_agent, "Bao/1.0");
6119        assert_eq!(state.worker_scope_config.platform, "Linux x86_64");
6120        assert_eq!(state.worker_scope_config.hardware_concurrency, 8);
6121        assert_eq!(state.worker_scope_config.language, "zh-CN");
6122        assert_eq!(state.worker_scope_config.languages.len(), 3);
6123    }
6124
6125    #[test]
6126    fn test_webview_state_default_worker_scope_config() {
6127        let state = BaoWebViewState::default();
6128        assert!(state.worker_scope_config.stealth_profile.is_none());
6129        assert!(state.worker_scope_config.hardware_concurrency > 0);
6130    }
6131
6132    // ─── SharedWorker (REQ-BRW-004 / DF-WK-7) ─────────────────────────
6133    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:SharedWorker] [DF-WK-7] [level:unit]
6134
6135    #[test]
6136    fn test_shared_worker_id_equality() {
6137        let id1 = SharedWorkerId {
6138            script_url: "sw.js".to_string(),
6139            name: "myworker".to_string(),
6140        };
6141        let id2 = SharedWorkerId {
6142            script_url: "sw.js".to_string(),
6143            name: "myworker".to_string(),
6144        };
6145        let id3 = SharedWorkerId {
6146            script_url: "sw.js".to_string(),
6147            name: "other".to_string(),
6148        };
6149        let id4 = SharedWorkerId {
6150            script_url: "other.js".to_string(),
6151            name: "myworker".to_string(),
6152        };
6153        assert_eq!(id1, id2);
6154        assert_ne!(id1, id3); // different name
6155        assert_ne!(id1, id4); // different url
6156    }
6157
6158    #[test]
6159    fn test_shared_worker_id_default_name() {
6160        let id = SharedWorkerId {
6161            script_url: "sw.js".to_string(),
6162            name: String::new(),
6163        };
6164        assert!(id.name.is_empty());
6165    }
6166
6167    #[test]
6168    fn test_shared_worker_handle_new_is_running() {
6169        let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6170        assert_eq!(handle.script_url, "sw.js");
6171        assert_eq!(handle.name, "myname");
6172        assert!(!handle.is_closing());
6173        assert!(!handle.is_terminated());
6174        assert_eq!(handle.connected_page_count(), 0);
6175    }
6176
6177    #[test]
6178    fn test_shared_worker_handle_id() {
6179        let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6180        let id = handle.id();
6181        assert_eq!(id.script_url, "sw.js");
6182        assert_eq!(id.name, "myname");
6183    }
6184
6185    #[test]
6186    fn test_shared_worker_handle_close() {
6187        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6188        assert!(!handle.is_closing());
6189        handle.close();
6190        assert!(handle.is_closing());
6191        // Idempotent
6192        handle.close();
6193        assert!(handle.is_closing());
6194    }
6195
6196    #[test]
6197    fn test_shared_worker_handle_mark_terminated() {
6198        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6199        assert!(!handle.is_terminated());
6200        handle.mark_terminated();
6201        assert!(handle.is_terminated());
6202    }
6203
6204    #[test]
6205    fn test_shared_worker_handle_connected_pages() {
6206        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6207        assert_eq!(handle.connected_page_count(), 0);
6208        handle.page_connected();
6209        assert_eq!(handle.connected_page_count(), 1);
6210        handle.page_connected();
6211        assert_eq!(handle.connected_page_count(), 2);
6212        handle.page_disconnected();
6213        assert_eq!(handle.connected_page_count(), 1);
6214        handle.page_disconnected();
6215        assert_eq!(handle.connected_page_count(), 0);
6216    }
6217
6218    #[test]
6219    fn test_shared_worker_handle_clone_shares_state() {
6220        let handle = SharedWorkerHandle::new("sw.js".to_string(), "name".to_string());
6221        let clone = handle.clone();
6222        handle.close();
6223        assert!(
6224            clone.is_closing(),
6225            "clone should see closing flag from original"
6226        );
6227        clone.mark_terminated();
6228        assert!(
6229            handle.is_terminated(),
6230            "original should see terminated flag from clone"
6231        );
6232    }
6233
6234    #[test]
6235    fn test_shared_worker_port_ref_increments_connected() {
6236        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6237        let port = SharedWorkerPortRef::new(handle.clone());
6238        assert_eq!(handle.connected_page_count(), 1);
6239        assert_eq!(port.handle().script_url, "sw.js");
6240    }
6241
6242    #[test]
6243    fn test_shared_worker_port_ref_drop_decrements_connected() {
6244        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6245        {
6246            let _port = SharedWorkerPortRef::new(handle.clone());
6247            assert_eq!(handle.connected_page_count(), 1);
6248        }
6249        assert_eq!(
6250            handle.connected_page_count(),
6251            0,
6252            "dropping port should decrement connected count"
6253        );
6254    }
6255
6256    #[test]
6257    fn test_shared_worker_port_ref_clone_increments_connected() {
6258        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6259        let port = SharedWorkerPortRef::new(handle.clone());
6260        assert_eq!(handle.connected_page_count(), 1);
6261        let _port2 = port.clone();
6262        assert_eq!(handle.connected_page_count(), 2);
6263    }
6264
6265    #[test]
6266    fn test_shared_worker_port_ref_multiple_pages() {
6267        let handle = SharedWorkerHandle::new("sw.js".to_string(), "shared".to_string());
6268        let _port1 = SharedWorkerPortRef::new(handle.clone());
6269        let _port2 = SharedWorkerPortRef::new(handle.clone());
6270        assert_eq!(handle.connected_page_count(), 2);
6271    }
6272
6273    #[test]
6274    fn test_webview_state_track_shared_worker_port() {
6275        let mut state = BaoWebViewState::default();
6276        let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6277        state.track_shared_worker_port(SharedWorkerPortRef::new(handle));
6278        assert_eq!(state.shared_worker_port_count(), 1);
6279    }
6280
6281    #[test]
6282    fn test_webview_state_disconnect_shared_worker_ports() {
6283        let mut state = BaoWebViewState::default();
6284        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6285        state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6286        state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6287        assert_eq!(state.shared_worker_port_count(), 2);
6288        assert_eq!(handle.connected_page_count(), 2);
6289        state.disconnect_shared_worker_ports();
6290        assert_eq!(state.shared_worker_port_count(), 0);
6291        assert_eq!(
6292            handle.connected_page_count(),
6293            0,
6294            "disconnect should drop ports and decrement counter"
6295        );
6296    }
6297
6298    #[test]
6299    fn test_webview_state_disconnect_shared_worker_ports_empty() {
6300        let mut state = BaoWebViewState::default();
6301        // No panic on empty
6302        state.disconnect_shared_worker_ports();
6303        assert_eq!(state.shared_worker_port_count(), 0);
6304    }
6305
6306    #[test]
6307    fn test_delegate_register_shared_worker_new() {
6308        let delegate = BaoServoDelegate::new();
6309        let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6310        let returned = delegate.register_shared_worker(handle);
6311        assert_eq!(returned.script_url, "sw.js");
6312        assert_eq!(returned.name, "myname");
6313        assert_eq!(delegate.shared_worker_count(), 1);
6314    }
6315
6316    #[test]
6317    fn test_delegate_register_shared_worker_dedup() {
6318        let delegate = BaoServoDelegate::new();
6319        let handle1 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6320        let handle2 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6321        delegate.register_shared_worker(handle1);
6322        let returned = delegate.register_shared_worker(handle2);
6323        // Same (url, name) → returns existing, count stays 1
6324        assert_eq!(delegate.shared_worker_count(), 1);
6325        assert_eq!(returned.script_url, "sw.js");
6326    }
6327
6328    #[test]
6329    fn test_delegate_find_shared_worker() {
6330        let delegate = BaoServoDelegate::new();
6331        let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6332        delegate.register_shared_worker(handle);
6333        let found = delegate.find_shared_worker("sw.js", "myname");
6334        assert!(found.is_some());
6335        assert_eq!(found.unwrap().script_url, "sw.js");
6336        assert!(delegate.find_shared_worker("other.js", "myname").is_none());
6337        assert!(delegate.find_shared_worker("sw.js", "other").is_none());
6338    }
6339
6340    #[test]
6341    fn test_delegate_reap_terminated_shared_workers() {
6342        let delegate = BaoServoDelegate::new();
6343        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6344        delegate.register_shared_worker(handle.clone());
6345        assert_eq!(delegate.shared_worker_count(), 1);
6346        // Mark terminated with zero connected pages
6347        handle.close();
6348        handle.mark_terminated();
6349        delegate.reap_terminated_shared_workers();
6350        assert_eq!(
6351            delegate.shared_worker_count(),
6352            0,
6353            "terminated shared worker with zero pages should be reaped"
6354        );
6355    }
6356
6357    #[test]
6358    fn test_delegate_reap_keeps_terminated_with_connected_pages() {
6359        let delegate = BaoServoDelegate::new();
6360        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6361        delegate.register_shared_worker(handle.clone());
6362        // Connect a page, then terminate the worker
6363        let _port = SharedWorkerPortRef::new(handle.clone());
6364        handle.close();
6365        handle.mark_terminated();
6366        delegate.reap_terminated_shared_workers();
6367        assert_eq!(
6368            delegate.shared_worker_count(),
6369            1,
6370            "terminated but still has connected pages — keep in registry"
6371        );
6372    }
6373
6374    #[test]
6375    fn test_shared_worker_connect_event_creation() {
6376        let event = SharedWorkerConnectEvent {
6377            shared_worker_id: SharedWorkerId {
6378                script_url: "sw.js".to_string(),
6379                name: "myname".to_string(),
6380            },
6381            page_url: "https://example.com/page1".to_string(),
6382        };
6383        assert_eq!(event.shared_worker_id.script_url, "sw.js");
6384        assert_eq!(event.shared_worker_id.name, "myname");
6385        assert_eq!(event.page_url, "https://example.com/page1");
6386    }
6387
6388    #[test]
6389    fn test_forward_shared_worker_connect_event() {
6390        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6391        let state = BaoWebViewState {
6392            event_tx: Some(tx),
6393            ..Default::default()
6394        };
6395        let event = SharedWorkerConnectEvent {
6396            shared_worker_id: SharedWorkerId {
6397                script_url: "sw.js".to_string(),
6398                name: "myname".to_string(),
6399            },
6400            page_url: "https://example.com".to_string(),
6401        };
6402        state.forward_shared_worker_connect_event(event);
6403        let recv = rx.try_recv().unwrap();
6404        match recv {
6405            ServoEvent::Console { level, text, .. } => {
6406                assert_eq!(level, ConsoleLevel::Debug);
6407                assert!(text.contains("sw.js"));
6408                assert!(text.contains("myname"));
6409                assert!(text.contains("https://example.com"));
6410            }
6411            _ => panic!("expected Console event for shared worker connect"),
6412        }
6413    }
6414
6415    #[test]
6416    fn test_forward_shared_worker_connect_event_no_tx() {
6417        let state = BaoWebViewState::default();
6418        let event = SharedWorkerConnectEvent {
6419            shared_worker_id: SharedWorkerId {
6420                script_url: "sw.js".to_string(),
6421                name: String::new(),
6422            },
6423            page_url: "https://example.com".to_string(),
6424        };
6425        // Should not panic
6426        state.forward_shared_worker_connect_event(event);
6427    }
6428
6429    #[test]
6430    fn test_shared_worker_scope_config_default() {
6431        let config = SharedWorkerScopeConfig::default();
6432        assert!(config.stealth_profile.is_none());
6433        assert!(config.user_agent.is_empty());
6434        assert!(config.platform.is_empty());
6435        assert!(config.hardware_concurrency > 0);
6436        assert_eq!(config.language, "en-US");
6437        assert!(!config.languages.is_empty());
6438    }
6439
6440    #[test]
6441    fn test_page_navigation_disconnects_shared_workers() {
6442        let mut state = BaoWebViewState::default();
6443        let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6444        state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6445        assert_eq!(state.shared_worker_port_count(), 1);
6446        assert_eq!(handle.connected_page_count(), 1);
6447        // Simulate page navigation — SharedWorkers survive but ports disconnect
6448        state.disconnect_shared_worker_ports();
6449        assert_eq!(state.shared_worker_port_count(), 0);
6450        assert_eq!(handle.connected_page_count(), 0);
6451    }
6452
6453    #[test]
6454    fn test_shared_worker_cross_page_sharing() {
6455        // Simulate two pages sharing the same SharedWorker
6456        let handle = SharedWorkerHandle::new("sw.js".to_string(), "shared".to_string());
6457
6458        // Page 1 connects
6459        let mut state1 = BaoWebViewState::default();
6460        state1.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6461        assert_eq!(handle.connected_page_count(), 1);
6462
6463        // Page 2 connects
6464        let mut state2 = BaoWebViewState::default();
6465        state2.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6466        assert_eq!(handle.connected_page_count(), 2);
6467
6468        // Page 1 navigates away
6469        state1.disconnect_shared_worker_ports();
6470        assert_eq!(handle.connected_page_count(), 1);
6471
6472        // Page 2 still connected
6473        assert_eq!(state2.shared_worker_port_count(), 1);
6474
6475        // SharedWorker is NOT terminated (only ports disconnect)
6476        assert!(!handle.is_closing());
6477        assert!(!handle.is_terminated());
6478    }
6479
6480    // ─── Structured Clone Message Channel (REQ-BRW-004 criterion #6) ─────
6481    // @trace REQ-BRW-004 [req:REQ-BRW-004] [criterion:6] [level:unit]
6482
6483    #[test]
6484    fn test_structured_clone_payload_creation() {
6485        let payload = StructuredClonePayload {
6486            data: vec![1, 2, 3, 4, 5],
6487            transferable_count: 0,
6488        };
6489        assert_eq!(payload.data.len(), 5);
6490        assert_eq!(payload.transferable_count, 0);
6491    }
6492
6493    #[test]
6494    fn test_structured_clone_payload_with_transferables() {
6495        let payload = StructuredClonePayload {
6496            data: vec![0u8; 1024],
6497            transferable_count: 2,
6498        };
6499        assert_eq!(payload.data.len(), 1024);
6500        assert_eq!(payload.transferable_count, 2);
6501    }
6502
6503    #[test]
6504    fn test_structured_clone_payload_clone() {
6505        let payload = StructuredClonePayload {
6506            data: vec![42u8; 100],
6507            transferable_count: 1,
6508        };
6509        let cloned = payload.clone();
6510        assert_eq!(cloned.data, payload.data);
6511        assert_eq!(cloned.transferable_count, payload.transferable_count);
6512    }
6513
6514    #[test]
6515    fn test_worker_structured_message_metadata_only() {
6516        let msg = WorkerStructuredMessage::metadata_only(
6517            WorkerId("worker1.js".to_string()),
6518            WorkerMessageDirection::PageToWorker,
6519        );
6520        assert!(msg.payload.is_none());
6521        assert_eq!(msg.worker_id.0, "worker1.js");
6522        assert_eq!(msg.direction, WorkerMessageDirection::PageToWorker);
6523        assert!(msg.message_id > 0);
6524    }
6525
6526    #[test]
6527    fn test_worker_structured_message_with_payload() {
6528        let msg = WorkerStructuredMessage::with_payload(
6529            WorkerId("worker2.js".to_string()),
6530            WorkerMessageDirection::WorkerToPage,
6531            vec![1, 2, 3],
6532            1,
6533        );
6534        assert!(msg.payload.is_some());
6535        let payload = msg.payload.unwrap();
6536        assert_eq!(payload.data, vec![1, 2, 3]);
6537        assert_eq!(payload.transferable_count, 1);
6538        assert_eq!(msg.direction, WorkerMessageDirection::WorkerToPage);
6539    }
6540
6541    #[test]
6542    fn test_worker_structured_message_unique_ids() {
6543        let msg1 = WorkerStructuredMessage::metadata_only(
6544            WorkerId("w.js".to_string()),
6545            WorkerMessageDirection::PageToWorker,
6546        );
6547        let msg2 = WorkerStructuredMessage::metadata_only(
6548            WorkerId("w.js".to_string()),
6549            WorkerMessageDirection::PageToWorker,
6550        );
6551        // Each message should get a unique ID
6552        assert_ne!(msg1.message_id, msg2.message_id);
6553    }
6554
6555    #[test]
6556    fn test_worker_channel_bridge_creation() {
6557        let worker_id = WorkerId("worker1.js".to_string());
6558        let (bridge, endpoints) = WorkerChannelBridge::new(worker_id.clone());
6559        assert_eq!(bridge.worker_id, worker_id);
6560        assert_eq!(endpoints.worker_id, worker_id);
6561        // Endpoints should have the rx/tx for the worker thread
6562        assert!(endpoints.page_to_worker_rx.is_some());
6563        assert!(endpoints.worker_to_page_tx.is_some());
6564    }
6565
6566    #[test]
6567    fn test_worker_channel_bridge_page_to_worker() {
6568        let worker_id = WorkerId("worker1.js".to_string());
6569        let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6570        // Page sends a message to worker
6571        let payload = StructuredClonePayload {
6572            data: vec![1, 2, 3],
6573            transferable_count: 0,
6574        };
6575        bridge.post_message_to_worker(payload).unwrap();
6576        // Worker thread receives it
6577        let rx = endpoints.page_to_worker_rx.unwrap();
6578        let received = rx.try_recv().unwrap();
6579        assert_eq!(received.data, vec![1, 2, 3]);
6580    }
6581
6582    #[test]
6583    fn test_worker_channel_bridge_worker_to_page() {
6584        let worker_id = WorkerId("worker1.js".to_string());
6585        let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6586        // Worker sends a message to page
6587        let msg = WorkerStructuredMessage::with_payload(
6588            WorkerId("worker1.js".to_string()),
6589            WorkerMessageDirection::WorkerToPage,
6590            vec![4, 5, 6],
6591            0,
6592        );
6593        let tx = endpoints.worker_to_page_tx.unwrap();
6594        tx.send(msg).unwrap();
6595        // Page receives it
6596        let result = bridge.try_recv_from_worker().unwrap();
6597        assert!(result.is_some());
6598        let received = result.unwrap();
6599        assert_eq!(received.payload.unwrap().data, vec![4, 5, 6]);
6600    }
6601
6602    #[test]
6603    fn test_worker_channel_bridge_drain() {
6604        let worker_id = WorkerId("worker1.js".to_string());
6605        let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6606        let tx = endpoints.worker_to_page_tx.unwrap();
6607        // Send multiple messages
6608        for i in 0..3 {
6609            let msg = WorkerStructuredMessage::with_payload(
6610                WorkerId("worker1.js".to_string()),
6611                WorkerMessageDirection::WorkerToPage,
6612                vec![i],
6613                0,
6614            );
6615            tx.send(msg).unwrap();
6616        }
6617        // Drain all
6618        let result = bridge.drain_worker_messages();
6619        assert_eq!(result.messages.len(), 3);
6620        assert!(!result.disconnected);
6621        // Drain again should be empty
6622        let empty = bridge.drain_worker_messages();
6623        assert!(empty.messages.is_empty());
6624        assert!(!empty.disconnected);
6625    }
6626
6627    #[test]
6628    fn test_webview_state_worker_channel_registration() {
6629        let mut state = BaoWebViewState::default();
6630        let worker_id = WorkerId("worker1.js".to_string());
6631        let (bridge, _endpoints) = WorkerChannelBridge::new(worker_id.clone());
6632        state.register_worker_channel(bridge);
6633        assert_eq!(state.worker_channel_count(), 1);
6634        assert!(state.worker_channel(&worker_id).is_some());
6635    }
6636
6637    #[test]
6638    fn test_webview_state_create_worker_channel() {
6639        let mut state = BaoWebViewState::default();
6640        let worker_id = WorkerId("worker1.js".to_string());
6641        let endpoints = state.create_worker_channel(worker_id.clone());
6642        assert_eq!(state.worker_channel_count(), 1);
6643        assert_eq!(endpoints.worker_id, worker_id);
6644        assert!(endpoints.page_to_worker_rx.is_some());
6645        assert!(endpoints.worker_to_page_tx.is_some());
6646    }
6647
6648    #[test]
6649    fn test_webview_state_post_to_worker() {
6650        let mut state = BaoWebViewState::default();
6651        let worker_id = WorkerId("worker1.js".to_string());
6652        let endpoints = state.create_worker_channel(worker_id.clone());
6653        let payload = StructuredClonePayload {
6654            data: vec![42],
6655            transferable_count: 0,
6656        };
6657        // Post to existing worker
6658        let result = state.post_to_worker(&worker_id, payload);
6659        assert!(result.is_ok());
6660        // Worker thread receives it
6661        let rx = endpoints.page_to_worker_rx.unwrap();
6662        let received = rx.try_recv().unwrap();
6663        assert_eq!(received.data, vec![42]);
6664        // Post to non-existent worker
6665        let result = state.post_to_worker(
6666            &WorkerId("nonexistent.js".to_string()),
6667            StructuredClonePayload {
6668                data: vec![],
6669                transferable_count: 0,
6670            },
6671        );
6672        assert!(result.is_err());
6673    }
6674
6675    #[test]
6676    fn test_webview_state_drain_all_worker_messages() {
6677        let mut state = BaoWebViewState::default();
6678        let worker_id1 = WorkerId("worker1.js".to_string());
6679        let worker_id2 = WorkerId("worker2.js".to_string());
6680        let endpoints1 = state.create_worker_channel(worker_id1);
6681        let endpoints2 = state.create_worker_channel(worker_id2);
6682        // Send messages from both workers
6683        let tx1 = endpoints1.worker_to_page_tx.unwrap();
6684        let tx2 = endpoints2.worker_to_page_tx.unwrap();
6685        tx1.send(WorkerStructuredMessage::metadata_only(
6686            WorkerId("worker1.js".to_string()),
6687            WorkerMessageDirection::WorkerToPage,
6688        ))
6689        .unwrap();
6690        tx2.send(WorkerStructuredMessage::metadata_only(
6691            WorkerId("worker2.js".to_string()),
6692            WorkerMessageDirection::WorkerToPage,
6693        ))
6694        .unwrap();
6695        // Drain all
6696        let (messages, disconnected) = state.drain_all_worker_messages();
6697        assert_eq!(messages.len(), 2);
6698        assert!(disconnected.is_empty());
6699    }
6700
6701    #[test]
6702    fn test_webview_state_terminate_clears_channels() {
6703        let mut state = BaoWebViewState::default();
6704        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
6705        state.create_worker_channel(WorkerId("worker1.js".to_string()));
6706        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
6707        state.create_worker_channel(WorkerId("worker2.js".to_string()));
6708        assert_eq!(state.worker_channel_count(), 2);
6709        // Terminate all — should clear channels too
6710        state.terminate_all_workers();
6711        assert_eq!(state.worker_channel_count(), 0);
6712    }
6713
6714    #[test]
6715    fn test_webview_state_reap_terminated_worker_channels() {
6716        let mut state = BaoWebViewState::default();
6717        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
6718        state.create_worker_channel(WorkerId("worker1.js".to_string()));
6719        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
6720        state.create_worker_channel(WorkerId("worker2.js".to_string()));
6721        // Terminate and reap worker1
6722        state.active_workers[0].handle().terminate();
6723        state.active_workers[0].handle().mark_terminated();
6724        state.reap_terminated_workers();
6725        // worker1's channel should be reaped, worker2's should remain
6726        assert_eq!(state.worker_channel_count(), 1);
6727        assert!(state
6728            .worker_channel(&WorkerId("worker2.js".to_string()))
6729            .is_some());
6730    }
6731
6732    #[test]
6733    fn test_webview_state_remove_worker_channel() {
6734        let mut state = BaoWebViewState::default();
6735        let worker_id = WorkerId("worker1.js".to_string());
6736        state.create_worker_channel(worker_id.clone());
6737        assert_eq!(state.worker_channel_count(), 1);
6738        let removed = state.remove_worker_channel(&worker_id);
6739        assert!(removed.is_some());
6740        assert_eq!(state.worker_channel_count(), 0);
6741    }
6742
6743    #[test]
6744    fn test_forward_worker_structured_message_with_payload() {
6745        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6746        let state = BaoWebViewState {
6747            event_tx: Some(tx),
6748            ..Default::default()
6749        };
6750        let msg = WorkerStructuredMessage::with_payload(
6751            WorkerId("worker1.js".to_string()),
6752            WorkerMessageDirection::WorkerToPage,
6753            vec![1, 2, 3],
6754            1,
6755        );
6756        state.forward_worker_structured_message(&msg);
6757        let event = rx.try_recv().unwrap();
6758        match event {
6759            ServoEvent::Console { level, text, .. } => {
6760                assert_eq!(level, ConsoleLevel::Debug);
6761                assert!(text.contains("worker→page"));
6762                assert!(text.contains("worker1.js"));
6763                assert!(text.contains("3 bytes"));
6764                assert!(text.contains("1 transferable"));
6765            }
6766            _ => panic!("expected Console event for structured message"),
6767        }
6768    }
6769
6770    #[test]
6771    fn test_forward_worker_structured_message_metadata_only() {
6772        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6773        let state = BaoWebViewState {
6774            event_tx: Some(tx),
6775            ..Default::default()
6776        };
6777        let msg = WorkerStructuredMessage::metadata_only(
6778            WorkerId("worker1.js".to_string()),
6779            WorkerMessageDirection::PageToWorker,
6780        );
6781        state.forward_worker_structured_message(&msg);
6782        let event = rx.try_recv().unwrap();
6783        match event {
6784            ServoEvent::Console { text, .. } => {
6785                assert!(text.contains("metadata-only"));
6786            }
6787            _ => panic!("expected Console event"),
6788        }
6789    }
6790
6791    #[test]
6792    fn test_drain_and_forward_worker_messages() {
6793        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6794        let mut state = BaoWebViewState {
6795            event_tx: Some(tx),
6796            ..Default::default()
6797        };
6798        let endpoints = state.create_worker_channel(WorkerId("worker1.js".to_string()));
6799        let worker_tx = endpoints.worker_to_page_tx.unwrap();
6800        worker_tx
6801            .send(WorkerStructuredMessage::metadata_only(
6802                WorkerId("worker1.js".to_string()),
6803                WorkerMessageDirection::WorkerToPage,
6804            ))
6805            .unwrap();
6806        // Drain and forward
6807        let disconnected = state.drain_and_forward_worker_messages();
6808        assert!(disconnected.is_empty());
6809        // Should have forwarded to CDP
6810        let event = rx.try_recv().unwrap();
6811        match event {
6812            ServoEvent::Console { text, .. } => {
6813                assert!(text.contains("worker→page"));
6814            }
6815            _ => panic!("expected Console event"),
6816        }
6817    }
6818
6819    #[test]
6820    fn test_worker_channel_bridge_disconnected() {
6821        let worker_id = WorkerId("worker1.js".to_string());
6822        let (bridge, _endpoints) = WorkerChannelBridge::new(worker_id);
6823        // Drop the worker-side sender to simulate worker exit
6824        // The bridge's try_recv_from_worker should return Err
6825        // (Can't easily test this without moving endpoints to another thread,
6826        // but we can test the drain with disconnected channel)
6827        let result = bridge.try_recv_from_worker();
6828        assert!(result.is_ok()); // Empty channel, not disconnected yet
6829        assert!(result.unwrap().is_none());
6830    }
6831
6832    // ─── WorkerLocation (REQ-BRW-004 entity:WorkerLocation) ──────────────
6833    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:WorkerLocation] [level:unit]
6834
6835    #[test]
6836    fn test_worker_location_from_https_url() {
6837        let loc = WorkerLocation::from_url("https://example.com:8080/path?q=1#hash").unwrap();
6838        assert_eq!(loc.href, "https://example.com:8080/path?q=1#hash");
6839        assert_eq!(loc.protocol, "https:");
6840        assert_eq!(loc.host, "example.com:8080");
6841        assert_eq!(loc.hostname, "example.com");
6842        assert_eq!(loc.port, "8080");
6843        assert_eq!(loc.pathname, "/path");
6844        assert_eq!(loc.search, "?q=1");
6845        assert_eq!(loc.hash, "#hash");
6846        assert_eq!(loc.origin, "https://example.com:8080");
6847    }
6848
6849    #[test]
6850    fn test_worker_location_from_default_port() {
6851        let loc = WorkerLocation::from_url("https://example.com/path").unwrap();
6852        assert_eq!(loc.host, "example.com");
6853        assert_eq!(loc.port, "");
6854        assert_eq!(loc.origin, "https://example.com");
6855    }
6856
6857    #[test]
6858    fn test_worker_location_from_http_url() {
6859        let loc = WorkerLocation::from_url("http://localhost:3000/worker.js").unwrap();
6860        assert_eq!(loc.protocol, "http:");
6861        assert_eq!(loc.hostname, "localhost");
6862        assert_eq!(loc.port, "3000");
6863        assert_eq!(loc.pathname, "/worker.js");
6864    }
6865
6866    #[test]
6867    fn test_worker_location_from_url_no_query_no_hash() {
6868        let loc = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
6869        assert_eq!(loc.search, "");
6870        assert_eq!(loc.hash, "");
6871    }
6872
6873    #[test]
6874    fn test_worker_location_from_invalid_url() {
6875        assert!(WorkerLocation::from_url("not a url").is_none());
6876    }
6877
6878    #[test]
6879    fn test_worker_location_from_url_value() {
6880        let url = url::Url::parse("https://example.com/worker.js").unwrap();
6881        let loc = WorkerLocation::from_url_value(url);
6882        assert_eq!(loc.protocol, "https:");
6883        assert_eq!(loc.hostname, "example.com");
6884        assert_eq!(loc.pathname, "/worker.js");
6885    }
6886
6887    // ─── WorkerNavigator (REQ-BRW-004 entity:WorkerNavigator) ──────────
6888    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:WorkerNavigator] [level:unit]
6889
6890    #[test]
6891    fn test_worker_navigator_default() {
6892        let nav = WorkerNavigator::default();
6893        assert!(nav.user_agent.is_empty());
6894        assert!(nav.platform.is_empty());
6895        assert!(nav.hardware_concurrency > 0);
6896        assert_eq!(nav.language, "en-US");
6897        assert!(!nav.languages.is_empty());
6898        assert!(nav.connection.is_none());
6899        assert!(!nav.cookie_enabled);
6900        assert_eq!(nav.max_touch_points, 0);
6901        assert_eq!(nav.product, "Gecko");
6902        assert_eq!(nav.app_code_name, "Mozilla");
6903        assert_eq!(nav.app_name, "Netscape");
6904        assert!(nav.app_version.is_empty());
6905    }
6906
6907    #[test]
6908    fn test_worker_navigator_from_scope_config() {
6909        let config = WorkerScopeConfig {
6910            stealth_profile: None,
6911            user_agent: "Bao/1.0".to_string(),
6912            platform: "Linux x86_64".to_string(),
6913            hardware_concurrency: 8,
6914            language: "zh-CN".to_string(),
6915            languages: vec!["zh-CN".to_string(), "zh".to_string()],
6916        };
6917        let nav = WorkerNavigator::from_scope_config(&config);
6918        assert_eq!(nav.user_agent, "Bao/1.0");
6919        assert_eq!(nav.platform, "Linux x86_64");
6920        assert_eq!(nav.hardware_concurrency, 8);
6921        assert_eq!(nav.language, "zh-CN");
6922        assert_eq!(nav.languages.len(), 2);
6923        assert_eq!(nav.app_version, "Bao/1.0"); // app_version mirrors user_agent
6924        assert_eq!(nav.product, "Gecko");
6925        assert_eq!(nav.app_code_name, "Mozilla");
6926        assert_eq!(nav.app_name, "Netscape");
6927    }
6928
6929    #[test]
6930    fn test_worker_navigator_from_shared_scope_config() {
6931        let config = SharedWorkerScopeConfig {
6932            stealth_profile: None,
6933            user_agent: "Bao/2.0".to_string(),
6934            platform: "MacOS".to_string(),
6935            hardware_concurrency: 4,
6936            language: "ja".to_string(),
6937            languages: vec!["ja".to_string(), "en".to_string()],
6938        };
6939        let nav = WorkerNavigator::from_scope_config(&config);
6940        assert_eq!(nav.user_agent, "Bao/2.0");
6941        assert_eq!(nav.platform, "MacOS");
6942        assert_eq!(nav.hardware_concurrency, 4);
6943        assert_eq!(nav.app_version, "Bao/2.0");
6944    }
6945
6946    #[test]
6947    fn test_worker_network_information() {
6948        let info = WorkerNetworkInformation {
6949            effective_type: "4g".to_string(),
6950            downlink: 10,
6951            rtt: 50,
6952            save_data: false,
6953        };
6954        assert_eq!(info.effective_type, "4g");
6955        assert_eq!(info.downlink, 10);
6956        assert_eq!(info.rtt, 50);
6957        assert!(!info.save_data);
6958    }
6959
6960    // ─── WorkerGlobalScopeState (REQ-BRW-004 entity:WorkerGlobalScope) ──
6961    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:WorkerGlobalScope] [level:unit]
6962
6963    #[test]
6964    fn test_worker_global_scope_state_new() {
6965        let config = WorkerScopeConfig {
6966            stealth_profile: None,
6967            user_agent: "Bao/1.0".to_string(),
6968            platform: "Linux".to_string(),
6969            hardware_concurrency: 8,
6970            language: "en-US".to_string(),
6971            languages: vec!["en-US".to_string()],
6972        };
6973        let scope =
6974            WorkerGlobalScopeState::new("https://example.com/worker.js".to_string(), &config);
6975        assert_eq!(scope.worker_url, "https://example.com/worker.js");
6976        assert!(!scope.closing);
6977        assert!(scope.location.is_some());
6978        assert_eq!(scope.navigator.user_agent, "Bao/1.0");
6979    }
6980
6981    #[test]
6982    fn test_worker_global_scope_state_new_shared() {
6983        let config = SharedWorkerScopeConfig {
6984            stealth_profile: None,
6985            user_agent: "Bao/2.0".to_string(),
6986            platform: "MacOS".to_string(),
6987            hardware_concurrency: 4,
6988            language: "ja".to_string(),
6989            languages: vec!["ja".to_string()],
6990        };
6991        let scope =
6992            WorkerGlobalScopeState::new_shared("https://example.com/sw.js".to_string(), &config);
6993        assert_eq!(scope.worker_url, "https://example.com/sw.js");
6994        assert_eq!(scope.navigator.user_agent, "Bao/2.0");
6995    }
6996
6997    #[test]
6998    fn test_worker_global_scope_state_location_parsed() {
6999        let config = WorkerScopeConfig::default();
7000        let scope = WorkerGlobalScopeState::new(
7001            "https://example.com:8080/app/worker.js?debug=true#section".to_string(),
7002            &config,
7003        );
7004        let loc = scope.location.unwrap();
7005        assert_eq!(loc.hostname, "example.com");
7006        assert_eq!(loc.port, "8080");
7007        assert_eq!(loc.pathname, "/app/worker.js");
7008        assert_eq!(loc.search, "?debug=true");
7009        assert_eq!(loc.hash, "#section");
7010    }
7011
7012    #[test]
7013    fn test_worker_global_scope_state_invalid_url_no_location() {
7014        let config = WorkerScopeConfig::default();
7015        let scope = WorkerGlobalScopeState::new("not-a-url".to_string(), &config);
7016        assert!(scope.location.is_none());
7017    }
7018
7019    // ─── DedicatedWorkerGlobalScopeState (REQ-BRW-004 entity) ────────────
7020    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:DedicatedWorkerGlobalScope] [level:unit]
7021
7022    #[test]
7023    fn test_dedicated_worker_global_scope_state_new() {
7024        let worker_id = WorkerId("https://example.com/worker.js".to_string());
7025        let config = WorkerScopeConfig {
7026            stealth_profile: None,
7027            user_agent: "Bao/1.0".to_string(),
7028            platform: "Linux".to_string(),
7029            hardware_concurrency: 8,
7030            language: "en-US".to_string(),
7031            languages: vec!["en-US".to_string()],
7032        };
7033        let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7034        assert_eq!(scope.worker_id, worker_id);
7035        assert!(!scope.has_onmessage);
7036        assert!(!scope.has_onerror);
7037        assert_eq!(scope.scope.navigator.user_agent, "Bao/1.0");
7038    }
7039
7040    #[test]
7041    fn test_dedicated_worker_global_scope_state_location() {
7042        let worker_id = WorkerId("https://example.com/worker.js".to_string());
7043        let config = WorkerScopeConfig::default();
7044        let scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7045        let loc = scope.location().unwrap();
7046        assert_eq!(loc.hostname, "example.com");
7047        assert_eq!(loc.pathname, "/worker.js");
7048    }
7049
7050    #[test]
7051    fn test_dedicated_worker_global_scope_state_navigator() {
7052        let worker_id = WorkerId("worker.js".to_string());
7053        let config = WorkerScopeConfig {
7054            stealth_profile: None,
7055            user_agent: "Bao/1.0".to_string(),
7056            platform: "Linux".to_string(),
7057            hardware_concurrency: 8,
7058            language: "zh-CN".to_string(),
7059            languages: vec!["zh-CN".to_string()],
7060        };
7061        let scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7062        let nav = scope.navigator();
7063        assert_eq!(nav.user_agent, "Bao/1.0");
7064        assert_eq!(nav.hardware_concurrency, 8);
7065    }
7066
7067    #[test]
7068    fn test_dedicated_worker_global_scope_state_event_handlers() {
7069        let worker_id = WorkerId("worker.js".to_string());
7070        let config = WorkerScopeConfig::default();
7071        let mut scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7072        assert!(!scope.has_onmessage);
7073        assert!(!scope.has_onerror);
7074        scope.set_onmessage();
7075        assert!(scope.has_onmessage);
7076        assert!(!scope.has_onerror);
7077        scope.set_onerror();
7078        assert!(scope.has_onmessage);
7079        assert!(scope.has_onerror);
7080    }
7081
7082    // ─── DedicatedWorkerGlobalScope BaoWebViewState tracking ─────────────
7083    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:DedicatedWorkerGlobalScope] [level:unit]
7084
7085    #[test]
7086    fn test_webview_state_dedicated_worker_scope_registration() {
7087        let mut state = BaoWebViewState::default();
7088        let worker_id = WorkerId("worker1.js".to_string());
7089        let config = WorkerScopeConfig::default();
7090        let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7091        state.register_dedicated_worker_scope(worker_id.clone(), scope);
7092        assert_eq!(state.dedicated_worker_scope_count(), 1);
7093        assert!(state.dedicated_worker_scope(&worker_id).is_some());
7094    }
7095
7096    #[test]
7097    fn test_webview_state_dedicated_worker_scope_get_mut() {
7098        let mut state = BaoWebViewState::default();
7099        let worker_id = WorkerId("worker1.js".to_string());
7100        let config = WorkerScopeConfig::default();
7101        let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7102        state.register_dedicated_worker_scope(worker_id.clone(), scope);
7103        // Register event handler
7104        state
7105            .dedicated_worker_scope_mut(&worker_id)
7106            .unwrap()
7107            .set_onmessage();
7108        assert!(
7109            state
7110                .dedicated_worker_scope(&worker_id)
7111                .unwrap()
7112                .has_onmessage
7113        );
7114    }
7115
7116    #[test]
7117    fn test_webview_state_dedicated_worker_scope_remove() {
7118        let mut state = BaoWebViewState::default();
7119        let worker_id = WorkerId("worker1.js".to_string());
7120        let config = WorkerScopeConfig::default();
7121        let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7122        state.register_dedicated_worker_scope(worker_id.clone(), scope);
7123        let removed = state.remove_dedicated_worker_scope(&worker_id);
7124        assert!(removed.is_some());
7125        assert_eq!(state.dedicated_worker_scope_count(), 0);
7126    }
7127
7128    #[test]
7129    fn test_webview_state_dedicated_worker_scopes_snapshot() {
7130        let mut state = BaoWebViewState::default();
7131        let config = WorkerScopeConfig::default();
7132        let id1 = WorkerId("worker1.js".to_string());
7133        let id2 = WorkerId("worker2.js".to_string());
7134        state.register_dedicated_worker_scope(
7135            id1,
7136            DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7137        );
7138        state.register_dedicated_worker_scope(
7139            id2,
7140            DedicatedWorkerGlobalScopeState::new(WorkerId("worker2.js".to_string()), &config),
7141        );
7142        let scopes = state.dedicated_worker_scopes();
7143        assert_eq!(scopes.len(), 2);
7144    }
7145
7146    #[test]
7147    fn test_webview_state_terminate_clears_dedicated_worker_scopes() {
7148        let mut state = BaoWebViewState::default();
7149        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7150        let config = WorkerScopeConfig::default();
7151        state.register_dedicated_worker_scope(
7152            WorkerId("worker1.js".to_string()),
7153            DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7154        );
7155        assert_eq!(state.dedicated_worker_scope_count(), 1);
7156        state.terminate_all_workers();
7157        assert_eq!(state.dedicated_worker_scope_count(), 0);
7158    }
7159
7160    #[test]
7161    fn test_webview_state_reap_terminated_dedicated_worker_scopes() {
7162        let mut state = BaoWebViewState::default();
7163        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7164        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
7165        let config = WorkerScopeConfig::default();
7166        state.register_dedicated_worker_scope(
7167            WorkerId("worker1.js".to_string()),
7168            DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7169        );
7170        state.register_dedicated_worker_scope(
7171            WorkerId("worker2.js".to_string()),
7172            DedicatedWorkerGlobalScopeState::new(WorkerId("worker2.js".to_string()), &config),
7173        );
7174        // Terminate and reap worker1
7175        state.active_workers[0].handle().terminate();
7176        state.active_workers[0].handle().mark_terminated();
7177        state.reap_terminated_workers();
7178        // worker1's scope should be reaped, worker2's should remain
7179        assert_eq!(state.dedicated_worker_scope_count(), 1);
7180        assert!(state
7181            .dedicated_worker_scope(&WorkerId("worker2.js".to_string()))
7182            .is_some());
7183    }
7184
7185    #[test]
7186    fn test_worker_location_equality() {
7187        let loc1 = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
7188        let loc2 = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
7189        assert_eq!(loc1, loc2);
7190    }
7191
7192    // ─── Worker Script Loading Pipeline (REQ-BRW-004 / DF-WK-2) ────────
7193    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:Worker] [DF-WK-2] [level:unit]
7194
7195    #[test]
7196    fn test_worker_script_source_inline() {
7197        let source = WorkerScriptSource::Inline("var x = 1;".to_string());
7198        assert_eq!(source, WorkerScriptSource::Inline("var x = 1;".to_string()));
7199        assert_ne!(source, WorkerScriptSource::Inline("var y = 2;".to_string()));
7200    }
7201
7202    #[test]
7203    fn test_worker_script_source_url() {
7204        let source = WorkerScriptSource::Url("https://example.com/worker.js".to_string());
7205        assert_eq!(
7206            source,
7207            WorkerScriptSource::Url("https://example.com/worker.js".to_string())
7208        );
7209        assert_ne!(
7210            source,
7211            WorkerScriptSource::Url("https://other.com/worker.js".to_string())
7212        );
7213    }
7214
7215    #[test]
7216    fn test_worker_script_load_result() {
7217        let result = WorkerScriptLoadResult {
7218            source: "self.onmessage = function(e) {}".to_string(),
7219            final_url: "https://example.com/worker.js".to_string(),
7220            mime_type: Some("text/javascript".to_string()),
7221        };
7222        assert_eq!(result.source, "self.onmessage = function(e) {}");
7223        assert_eq!(result.final_url, "https://example.com/worker.js");
7224        assert_eq!(result.mime_type.as_deref(), Some("text/javascript"));
7225    }
7226
7227    #[test]
7228    fn test_worker_script_load_error_network() {
7229        let err = WorkerScriptLoadError::NetworkError("404 Not Found".to_string());
7230        assert_eq!(
7231            err,
7232            WorkerScriptLoadError::NetworkError("404 Not Found".to_string())
7233        );
7234    }
7235
7236    #[test]
7237    fn test_worker_script_load_error_invalid_mime() {
7238        let err = WorkerScriptLoadError::InvalidMimeType {
7239            received: "text/html".to_string(),
7240            url: "https://example.com/worker.js".to_string(),
7241        };
7242        match err {
7243            WorkerScriptLoadError::InvalidMimeType { received, url } => {
7244                assert_eq!(received, "text/html");
7245                assert_eq!(url, "https://example.com/worker.js");
7246            }
7247            _ => panic!("expected InvalidMimeType"),
7248        }
7249    }
7250
7251    #[test]
7252    fn test_worker_script_load_error_utf8() {
7253        let err = WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string());
7254        assert_eq!(
7255            err,
7256            WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string())
7257        );
7258    }
7259
7260    #[test]
7261    fn test_worker_script_load_error_invalid_url() {
7262        let err = WorkerScriptLoadError::InvalidUrl("bad url".to_string());
7263        assert_eq!(
7264            err,
7265            WorkerScriptLoadError::InvalidUrl("bad url".to_string())
7266        );
7267    }
7268
7269    #[test]
7270    fn test_worker_script_load_error_cancelled() {
7271        let err = WorkerScriptLoadError::Cancelled;
7272        assert_eq!(err, WorkerScriptLoadError::Cancelled);
7273    }
7274
7275    #[test]
7276    fn test_worker_script_type_default_classic() {
7277        assert_eq!(WorkerScriptType::default(), WorkerScriptType::Classic);
7278    }
7279
7280    #[test]
7281    fn test_worker_script_type_equality() {
7282        assert_eq!(WorkerScriptType::Classic, WorkerScriptType::Classic);
7283        assert_eq!(WorkerScriptType::Module, WorkerScriptType::Module);
7284        assert_ne!(WorkerScriptType::Classic, WorkerScriptType::Module);
7285    }
7286
7287    #[test]
7288    fn test_is_javascript_mime_type_valid() {
7289        assert!(is_javascript_mime_type("text/javascript"));
7290        assert!(is_javascript_mime_type("application/javascript"));
7291        assert!(is_javascript_mime_type("application/ecmascript"));
7292        assert!(is_javascript_mime_type("application/x-javascript"));
7293        assert!(is_javascript_mime_type("text/ecmascript"));
7294        assert!(is_javascript_mime_type("text/x-javascript"));
7295        assert!(is_javascript_mime_type("text/jscript"));
7296        assert!(is_javascript_mime_type("text/livescript"));
7297    }
7298
7299    #[test]
7300    fn test_is_javascript_mime_type_case_insensitive() {
7301        assert!(is_javascript_mime_type("Text/JavaScript"));
7302        assert!(is_javascript_mime_type("APPLICATION/JAVASCRIPT"));
7303        assert!(is_javascript_mime_type("text/JavaScript"));
7304    }
7305
7306    #[test]
7307    fn test_is_javascript_mime_type_with_charset() {
7308        // MIME type with parameters should still match
7309        assert!(is_javascript_mime_type("text/javascript; charset=utf-8"));
7310        assert!(is_javascript_mime_type(
7311            "application/javascript;charset=utf-8"
7312        ));
7313    }
7314
7315    #[test]
7316    fn test_is_javascript_mime_type_invalid() {
7317        assert!(!is_javascript_mime_type("text/html"));
7318        assert!(!is_javascript_mime_type("application/json"));
7319        assert!(!is_javascript_mime_type("text/plain"));
7320        assert!(!is_javascript_mime_type("application/octet-stream"));
7321        assert!(!is_javascript_mime_type("text/css"));
7322    }
7323
7324    #[test]
7325    fn test_worker_script_loader_inline() {
7326        let loader =
7327            WorkerScriptLoader::inline("var x = 1;".to_string(), WorkerScriptType::Classic);
7328        assert!(loader.script_url().is_none());
7329        assert!(!loader.requires_fetch());
7330        let resolved = loader.resolve().unwrap();
7331        assert_eq!(
7332            resolved,
7333            WorkerScriptSource::Inline("var x = 1;".to_string())
7334        );
7335    }
7336
7337    #[test]
7338    fn test_worker_script_loader_url_https() {
7339        let loader = WorkerScriptLoader::url(
7340            "https://example.com/worker.js".to_string(),
7341            WorkerScriptType::Classic,
7342        );
7343        assert_eq!(loader.script_url(), Some("https://example.com/worker.js"));
7344        assert!(loader.requires_fetch());
7345        let resolved = loader.resolve().unwrap();
7346        assert_eq!(
7347            resolved,
7348            WorkerScriptSource::Url("https://example.com/worker.js".to_string())
7349        );
7350    }
7351
7352    #[test]
7353    fn test_worker_script_loader_url_http() {
7354        let loader = WorkerScriptLoader::url(
7355            "http://localhost:3000/worker.js".to_string(),
7356            WorkerScriptType::Module,
7357        );
7358        assert!(loader.requires_fetch());
7359        assert_eq!(loader.script_type, WorkerScriptType::Module);
7360    }
7361
7362    #[test]
7363    fn test_worker_script_loader_url_invalid() {
7364        let loader = WorkerScriptLoader::url("not a url".to_string(), WorkerScriptType::Classic);
7365        let result = loader.resolve();
7366        assert!(result.is_err());
7367        match result.unwrap_err() {
7368            WorkerScriptLoadError::InvalidUrl(msg) => {
7369                assert!(msg.contains("Invalid Worker script URL"));
7370            }
7371            _ => panic!("expected InvalidUrl error"),
7372        }
7373    }
7374
7375    #[test]
7376    fn test_worker_script_loader_url_unsupported_scheme() {
7377        let loader = WorkerScriptLoader::url(
7378            "ftp://example.com/worker.js".to_string(),
7379            WorkerScriptType::Classic,
7380        );
7381        let result = loader.resolve();
7382        assert!(result.is_err());
7383        match result.unwrap_err() {
7384            WorkerScriptLoadError::InvalidUrl(msg) => {
7385                assert!(msg.contains("Unsupported") || msg.contains("ftp"));
7386            }
7387            _ => panic!("expected InvalidUrl error"),
7388        }
7389    }
7390
7391    #[test]
7392    fn test_worker_script_loader_data_url_text() {
7393        let loader = WorkerScriptLoader::url(
7394            "data:text/javascript,self.postMessage('hello')".to_string(),
7395            WorkerScriptType::Classic,
7396        );
7397        let resolved = loader.resolve().unwrap();
7398        match resolved {
7399            WorkerScriptSource::Inline(script) => {
7400                assert_eq!(script, "self.postMessage('hello')");
7401            }
7402            WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
7403        }
7404    }
7405
7406    #[test]
7407    fn test_worker_script_loader_data_url_base64() {
7408        // base64 of "var x = 1;" = "dmFyIHggPSAxOw=="
7409        let loader = WorkerScriptLoader::url(
7410            "data:text/javascript;base64,dmFyIHggPSAxOw==".to_string(),
7411            WorkerScriptType::Classic,
7412        );
7413        let resolved = loader.resolve().unwrap();
7414        match resolved {
7415            WorkerScriptSource::Inline(script) => {
7416                assert_eq!(script, "var x = 1;");
7417            }
7418            WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
7419        }
7420    }
7421
7422    #[test]
7423    fn test_worker_script_loader_data_url_invalid_base64() {
7424        let loader = WorkerScriptLoader::url(
7425            "data:text/javascript;base64,!!!invalid!!!".to_string(),
7426            WorkerScriptType::Classic,
7427        );
7428        let result = loader.resolve();
7429        assert!(result.is_err());
7430    }
7431
7432    #[test]
7433    fn test_worker_script_loader_data_url_missing_comma() {
7434        let loader = WorkerScriptLoader::url(
7435            "data:text/javascript".to_string(),
7436            WorkerScriptType::Classic,
7437        );
7438        let result = loader.resolve();
7439        assert!(result.is_err());
7440        match result.unwrap_err() {
7441            WorkerScriptLoadError::InvalidUrl(msg) => {
7442                assert!(msg.contains("comma separator"));
7443            }
7444            _ => panic!("expected InvalidUrl error"),
7445        }
7446    }
7447
7448    #[test]
7449    fn test_worker_script_loader_blob_url_passthrough() {
7450        let loader = WorkerScriptLoader::url(
7451            "blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string(),
7452            WorkerScriptType::Classic,
7453        );
7454        let resolved = loader.resolve().unwrap();
7455        assert_eq!(
7456            resolved,
7457            WorkerScriptSource::Url(
7458                "blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string()
7459            )
7460        );
7461    }
7462
7463    #[test]
7464    fn test_worker_script_loader_from_source() {
7465        let loader = WorkerScriptLoader::from_source(
7466            WorkerScriptSource::Inline("code".to_string()),
7467            WorkerScriptType::Module,
7468        );
7469        assert_eq!(loader.script_type, WorkerScriptType::Module);
7470        assert!(loader.script_url().is_none());
7471    }
7472
7473    #[test]
7474    fn test_worker_script_loader_validate_mime_type_valid() {
7475        assert!(WorkerScriptLoader::validate_mime_type(
7476            "text/javascript",
7477            "https://example.com/worker.js"
7478        )
7479        .is_ok());
7480        assert!(WorkerScriptLoader::validate_mime_type(
7481            "application/javascript",
7482            "https://example.com/worker.js"
7483        )
7484        .is_ok());
7485    }
7486
7487    #[test]
7488    fn test_worker_script_loader_validate_mime_type_invalid() {
7489        let result =
7490            WorkerScriptLoader::validate_mime_type("text/html", "https://example.com/worker.js");
7491        assert!(result.is_err());
7492        match result.unwrap_err() {
7493            WorkerScriptLoadError::InvalidMimeType { received, url } => {
7494                assert_eq!(received, "text/html");
7495                assert_eq!(url, "https://example.com/worker.js");
7496            }
7497            _ => panic!("expected InvalidMimeType error"),
7498        }
7499    }
7500
7501    #[test]
7502    fn test_worker_script_load_state_transitions() {
7503        let mut state = WorkerScriptLoadState::Pending;
7504        assert!(state.is_loading());
7505        assert!(!state.is_ready());
7506        assert!(!state.is_failed());
7507
7508        state = WorkerScriptLoadState::Fetching;
7509        assert!(state.is_loading());
7510
7511        state = WorkerScriptLoadState::Validating;
7512        assert!(state.is_loading());
7513
7514        state = WorkerScriptLoadState::Decoding;
7515        assert!(state.is_loading());
7516
7517        state = WorkerScriptLoadState::Compiling;
7518        assert!(state.is_loading());
7519
7520        state = WorkerScriptLoadState::Ready;
7521        assert!(!state.is_loading());
7522        assert!(state.is_ready());
7523
7524        state = WorkerScriptLoadState::Failed(WorkerScriptLoadError::NetworkError(
7525            "timeout".to_string(),
7526        ));
7527        assert!(!state.is_loading());
7528        assert!(state.is_failed());
7529    }
7530
7531    #[test]
7532    fn test_webview_state_worker_script_load_state_registration() {
7533        let mut state = BaoWebViewState::default();
7534        let worker_id = WorkerId("worker1.js".to_string());
7535        state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7536        assert_eq!(state.worker_script_load_state_count(), 1);
7537        assert!(state.worker_script_load_state(&worker_id).is_some());
7538        assert_eq!(
7539            state.worker_script_load_state(&worker_id).unwrap(),
7540            &WorkerScriptLoadState::Pending
7541        );
7542    }
7543
7544    #[test]
7545    fn test_webview_state_worker_script_load_state_update() {
7546        let mut state = BaoWebViewState::default();
7547        let worker_id = WorkerId("worker1.js".to_string());
7548        state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7549        state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
7550        assert_eq!(
7551            state.worker_script_load_state(&worker_id).unwrap(),
7552            &WorkerScriptLoadState::Fetching
7553        );
7554    }
7555
7556    #[test]
7557    fn test_webview_state_worker_script_load_state_remove() {
7558        let mut state = BaoWebViewState::default();
7559        let worker_id = WorkerId("worker1.js".to_string());
7560        state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Ready);
7561        let removed = state.remove_worker_script_load_state(&worker_id);
7562        assert!(removed.is_some());
7563        assert_eq!(removed.unwrap(), WorkerScriptLoadState::Ready);
7564        assert_eq!(state.worker_script_load_state_count(), 0);
7565    }
7566
7567    #[test]
7568    fn test_webview_state_terminate_clears_script_load_states() {
7569        let mut state = BaoWebViewState::default();
7570        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7571        state.register_worker_script_load_state(
7572            WorkerId("worker1.js".to_string()),
7573            WorkerScriptLoadState::Fetching,
7574        );
7575        assert_eq!(state.worker_script_load_state_count(), 1);
7576        state.terminate_all_workers();
7577        assert_eq!(state.worker_script_load_state_count(), 0);
7578    }
7579
7580    #[test]
7581    fn test_webview_state_reap_terminated_worker_script_load_states() {
7582        let mut state = BaoWebViewState::default();
7583        state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7584        state.track_worker(WorkerHandle::new("worker2.js".to_string()));
7585        state.register_worker_script_load_state(
7586            WorkerId("worker1.js".to_string()),
7587            WorkerScriptLoadState::Ready,
7588        );
7589        state.register_worker_script_load_state(
7590            WorkerId("worker2.js".to_string()),
7591            WorkerScriptLoadState::Fetching,
7592        );
7593        // Terminate and reap worker1
7594        state.active_workers[0].handle().terminate();
7595        state.active_workers[0].handle().mark_terminated();
7596        state.reap_terminated_workers();
7597        // worker1's script load state should be reaped, worker2's should remain
7598        assert_eq!(state.worker_script_load_state_count(), 1);
7599        assert!(state
7600            .worker_script_load_state(&WorkerId("worker2.js".to_string()))
7601            .is_some());
7602    }
7603
7604    #[test]
7605    fn test_worker_script_loader_file_url() {
7606        // Create a temp file with Worker script content
7607        let temp_dir = std::env::temp_dir();
7608        let temp_file = temp_dir.join("bao_test_worker_script.js");
7609        std::fs::write(&temp_file, "var x = 42;").unwrap();
7610
7611        let file_url = format!("file://{}", temp_file.display());
7612        let loader = WorkerScriptLoader::url(file_url, WorkerScriptType::Classic);
7613        let resolved = loader.resolve().unwrap();
7614        match resolved {
7615            WorkerScriptSource::Inline(script) => {
7616                assert_eq!(script, "var x = 42;");
7617            }
7618            WorkerScriptSource::Url(_) => panic!("expected inline source from file: URL"),
7619        }
7620
7621        // Cleanup
7622        let _ = std::fs::remove_file(&temp_file);
7623    }
7624
7625    #[test]
7626    fn test_worker_script_loader_file_url_not_found() {
7627        let loader = WorkerScriptLoader::url(
7628            "file:///nonexistent/path/worker.js".to_string(),
7629            WorkerScriptType::Classic,
7630        );
7631        let result = loader.resolve();
7632        assert!(result.is_err());
7633        match result.unwrap_err() {
7634            WorkerScriptLoadError::NetworkError(msg) => {
7635                assert!(msg.contains("Failed to read") || msg.contains("No such file"));
7636            }
7637            _ => panic!("expected NetworkError for missing file"),
7638        }
7639    }
7640
7641    #[test]
7642    fn test_worker_script_loader_full_pipeline_states() {
7643        // Simulate the full DF-WK-2 pipeline state transitions
7644        let mut state = BaoWebViewState::default();
7645        let worker_id = WorkerId("https://example.com/worker.js".to_string());
7646
7647        // Step 1: Worker created → Pending
7648        state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7649        assert!(state
7650            .worker_script_load_state(&worker_id)
7651            .unwrap()
7652            .is_loading());
7653
7654        // Step 2: Fetch started → Fetching
7655        state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
7656        assert!(matches!(
7657            state.worker_script_load_state(&worker_id).unwrap(),
7658            WorkerScriptLoadState::Fetching
7659        ));
7660
7661        // Step 3: Response received → Validating
7662        state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Validating);
7663
7664        // Step 4: MIME check passed → Decoding
7665        state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Decoding);
7666
7667        // Step 5: UTF-8 decoded → Compiling
7668        state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Compiling);
7669
7670        // Step 6: Compilation succeeded → Ready
7671        state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Ready);
7672        assert!(state
7673            .worker_script_load_state(&worker_id)
7674            .unwrap()
7675            .is_ready());
7676    }
7677
7678    #[test]
7679    fn test_worker_script_loader_pipeline_failure() {
7680        let mut state = BaoWebViewState::default();
7681        let worker_id = WorkerId("https://example.com/bad-worker.js".to_string());
7682
7683        state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7684
7685        // Simulate MIME type failure during validation
7686        state.update_worker_script_load_state(
7687            &worker_id,
7688            WorkerScriptLoadState::Failed(WorkerScriptLoadError::InvalidMimeType {
7689                received: "text/html".to_string(),
7690                url: "https://example.com/bad-worker.js".to_string(),
7691            }),
7692        );
7693        assert!(state
7694            .worker_script_load_state(&worker_id)
7695            .unwrap()
7696            .is_failed());
7697    }
7698
7699    // ─── StealthProfile → WorkerScopeConfig conversion (REQ-BRW-004 criteria #12-17) ───
7700    // @trace REQ-BRW-004 [criterion:12..17] CRIT-STL-WK
7701
7702    #[test]
7703    fn test_worker_scope_config_from_stealth_profile_chrome() {
7704        let profile = bao_stealth::StealthProfile::chrome_default();
7705        let config = WorkerScopeConfig::from(&profile);
7706
7707        assert!(
7708            config.stealth_profile.is_some(),
7709            "stealth_profile must be Some"
7710        );
7711        assert_eq!(config.user_agent, profile.navigator.user_agent);
7712        assert_eq!(config.platform, profile.navigator.platform);
7713        assert_eq!(
7714            config.hardware_concurrency,
7715            profile.navigator.hardware_concurrency as usize
7716        );
7717        assert_eq!(config.language, profile.navigator.language);
7718        assert_eq!(config.languages, profile.navigator.languages);
7719        assert!(
7720            config.user_agent.contains("Chrome"),
7721            "Chrome profile UA must contain Chrome"
7722        );
7723    }
7724
7725    #[test]
7726    fn test_worker_scope_config_from_stealth_profile_firefox() {
7727        let profile = bao_stealth::StealthProfile::firefox_default();
7728        let config = WorkerScopeConfig::from(&profile);
7729
7730        assert!(
7731            config.stealth_profile.is_some(),
7732            "stealth_profile must be Some"
7733        );
7734        assert_eq!(config.user_agent, profile.navigator.user_agent);
7735        assert_eq!(config.platform, profile.navigator.platform);
7736        assert_eq!(
7737            config.hardware_concurrency,
7738            profile.navigator.hardware_concurrency as usize
7739        );
7740        assert_eq!(config.language, profile.navigator.language);
7741        assert_eq!(config.languages, profile.navigator.languages);
7742        assert!(
7743            config.user_agent.contains("Firefox"),
7744            "Firefox profile UA must contain Firefox"
7745        );
7746    }
7747
7748    #[test]
7749    fn test_shared_worker_scope_config_from_stealth_profile() {
7750        let profile = bao_stealth::StealthProfile::chrome_default();
7751        let config = SharedWorkerScopeConfig::from(&profile);
7752
7753        assert!(
7754            config.stealth_profile.is_some(),
7755            "stealth_profile must be Some"
7756        );
7757        assert_eq!(config.user_agent, profile.navigator.user_agent);
7758        assert_eq!(config.platform, profile.navigator.platform);
7759        assert_eq!(
7760            config.hardware_concurrency,
7761            profile.navigator.hardware_concurrency as usize
7762        );
7763        assert_eq!(config.language, profile.navigator.language);
7764        assert_eq!(config.languages, profile.navigator.languages);
7765    }
7766
7767    #[test]
7768    fn test_worker_scope_config_from_stealth_profile_carries_canvas_webgl_audio() {
7769        // CRIT-STL-WK #13-17: Canvas/WebGL/Audio seeds must be identical
7770        // between the profile and the WorkerScopeConfig's embedded profile.
7771        let profile = bao_stealth::StealthProfile::chrome_default();
7772        let config = WorkerScopeConfig::from(&profile);
7773        let worker_profile = config.stealth_profile.unwrap();
7774
7775        assert_eq!(
7776            worker_profile.canvas.seed(),
7777            profile.canvas.seed(),
7778            "Canvas seed must match"
7779        );
7780        assert!(
7781            (worker_profile.canvas.noise_amplitude() - profile.canvas.noise_amplitude()).abs()
7782                < f64::EPSILON,
7783            "Canvas amplitude must match"
7784        );
7785        assert_eq!(
7786            worker_profile.audio.seed(),
7787            profile.audio.seed(),
7788            "Audio seed must match"
7789        );
7790        assert_eq!(
7791            worker_profile.webgl.vendor, profile.webgl.vendor,
7792            "WebGL vendor must match"
7793        );
7794        assert_eq!(
7795            worker_profile.webgl.renderer, profile.webgl.renderer,
7796            "WebGL renderer must match"
7797        );
7798    }
7799
7800    #[test]
7801    fn test_worker_scope_config_from_different_profiles_produces_different_configs() {
7802        // @trace REQ-BRW-004 [criterion:17] new Worker 后 worker 回传指纹摘要 === 主线程指纹摘要
7803        let chrome = bao_stealth::StealthProfile::chrome_default();
7804        let firefox = bao_stealth::StealthProfile::firefox_default();
7805        let chrome_config = WorkerScopeConfig::from(&chrome);
7806        let firefox_config = WorkerScopeConfig::from(&firefox);
7807
7808        assert_ne!(chrome_config.user_agent, firefox_config.user_agent);
7809        assert_ne!(
7810            chrome_config.stealth_profile.unwrap().canvas.seed(),
7811            firefox_config.stealth_profile.unwrap().canvas.seed()
7812        );
7813    }
7814
7815    // ─── SharedWorkerGlobalScopeState (REQ-BRW-004 entity) ────────────────
7816    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:SharedWorkerGlobalScope] [DF-WK-7] [level:unit]
7817
7818    #[test]
7819    fn test_shared_worker_global_scope_state_new() {
7820        let id = SharedWorkerId {
7821            script_url: "sw.js".to_string(),
7822            name: "myworker".to_string(),
7823        };
7824        let config = SharedWorkerScopeConfig {
7825            stealth_profile: None,
7826            user_agent: "Bao/1.0".to_string(),
7827            platform: "Linux".to_string(),
7828            hardware_concurrency: 8,
7829            language: "en-US".to_string(),
7830            languages: vec!["en-US".to_string()],
7831        };
7832        let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
7833        assert_eq!(scope.shared_worker_id, id);
7834        assert!(!scope.has_onconnect);
7835        assert_eq!(scope.connect_count, 0);
7836        assert_eq!(scope.scope.navigator.user_agent, "Bao/1.0");
7837    }
7838
7839    #[test]
7840    fn test_shared_worker_global_scope_state_location() {
7841        let id = SharedWorkerId {
7842            script_url: "https://example.com/sw.js".to_string(),
7843            name: String::new(),
7844        };
7845        let config = SharedWorkerScopeConfig::default();
7846        let scope = SharedWorkerGlobalScopeState::new(id, &config);
7847        let loc = scope.location().unwrap();
7848        assert_eq!(loc.hostname, "example.com");
7849        assert_eq!(loc.pathname, "/sw.js");
7850    }
7851
7852    #[test]
7853    fn test_shared_worker_global_scope_state_navigator() {
7854        let id = SharedWorkerId {
7855            script_url: "sw.js".to_string(),
7856            name: "test".to_string(),
7857        };
7858        let config = SharedWorkerScopeConfig {
7859            stealth_profile: None,
7860            user_agent: "Bao/2.0".to_string(),
7861            platform: "MacOS".to_string(),
7862            hardware_concurrency: 4,
7863            language: "ja".to_string(),
7864            languages: vec!["ja".to_string()],
7865        };
7866        let scope = SharedWorkerGlobalScopeState::new(id, &config);
7867        let nav = scope.navigator();
7868        assert_eq!(nav.user_agent, "Bao/2.0");
7869        assert_eq!(nav.hardware_concurrency, 4);
7870    }
7871
7872    #[test]
7873    fn test_shared_worker_global_scope_state_onconnect() {
7874        let id = SharedWorkerId {
7875            script_url: "sw.js".to_string(),
7876            name: String::new(),
7877        };
7878        let config = SharedWorkerScopeConfig::default();
7879        let mut scope = SharedWorkerGlobalScopeState::new(id, &config);
7880        assert!(!scope.has_onconnect);
7881        scope.set_onconnect();
7882        assert!(scope.has_onconnect);
7883    }
7884
7885    #[test]
7886    fn test_shared_worker_global_scope_state_connect_count() {
7887        let id = SharedWorkerId {
7888            script_url: "sw.js".to_string(),
7889            name: String::new(),
7890        };
7891        let config = SharedWorkerScopeConfig::default();
7892        let mut scope = SharedWorkerGlobalScopeState::new(id, &config);
7893        assert_eq!(scope.connect_count, 0);
7894        scope.page_connected();
7895        assert_eq!(scope.connect_count, 1);
7896        scope.page_connected();
7897        assert_eq!(scope.connect_count, 2);
7898    }
7899
7900    // ─── SharedWorker Port Channel (REQ-BRW-004 / DF-WK-7) ───────────────
7901    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:SharedWorker] [DF-WK-7] [level:unit]
7902
7903    #[test]
7904    fn test_shared_worker_port_channel_creation() {
7905        let id = SharedWorkerId {
7906            script_url: "sw.js".to_string(),
7907            name: "test".to_string(),
7908        };
7909        let (port, endpoints) = SharedWorkerPortChannel::new(id.clone());
7910        assert_eq!(port.shared_worker_id, id);
7911        assert_eq!(endpoints.shared_worker_id, id);
7912        assert!(endpoints.page_to_worker_rx.is_some());
7913        assert!(endpoints.worker_to_page_tx.is_some());
7914    }
7915
7916    #[test]
7917    fn test_shared_worker_port_channel_page_to_worker() {
7918        let id = SharedWorkerId {
7919            script_url: "sw.js".to_string(),
7920            name: String::new(),
7921        };
7922        let (port, endpoints) = SharedWorkerPortChannel::new(id);
7923        let payload = StructuredClonePayload {
7924            data: vec![1, 2, 3],
7925            transferable_count: 0,
7926        };
7927        port.post_message_to_worker(payload).unwrap();
7928        let rx = endpoints.page_to_worker_rx.unwrap();
7929        let received = rx.try_recv().unwrap();
7930        assert_eq!(received.data, vec![1, 2, 3]);
7931    }
7932
7933    #[test]
7934    fn test_shared_worker_port_channel_worker_to_page() {
7935        let id = SharedWorkerId {
7936            script_url: "sw.js".to_string(),
7937            name: String::new(),
7938        };
7939        let (port, endpoints) = SharedWorkerPortChannel::new(id);
7940        let msg = WorkerStructuredMessage::with_payload(
7941            WorkerId("sw.js".to_string()),
7942            WorkerMessageDirection::WorkerToPage,
7943            vec![4, 5, 6],
7944            0,
7945        );
7946        let tx = endpoints.worker_to_page_tx.unwrap();
7947        tx.send(msg).unwrap();
7948        let result = port.try_recv_from_worker().unwrap();
7949        assert!(result.is_some());
7950        assert_eq!(result.unwrap().payload.unwrap().data, vec![4, 5, 6]);
7951    }
7952
7953    #[test]
7954    fn test_shared_worker_port_channel_drain() {
7955        let id = SharedWorkerId {
7956            script_url: "sw.js".to_string(),
7957            name: String::new(),
7958        };
7959        let (port, endpoints) = SharedWorkerPortChannel::new(id);
7960        let tx = endpoints.worker_to_page_tx.unwrap();
7961        for i in 0..3 {
7962            let msg = WorkerStructuredMessage::with_payload(
7963                WorkerId("sw.js".to_string()),
7964                WorkerMessageDirection::WorkerToPage,
7965                vec![i],
7966                0,
7967            );
7968            tx.send(msg).unwrap();
7969        }
7970        let result = port.drain_worker_messages();
7971        assert_eq!(result.messages.len(), 3);
7972        assert!(!result.disconnected);
7973        let empty = port.drain_worker_messages();
7974        assert!(empty.messages.is_empty());
7975        assert!(!empty.disconnected);
7976    }
7977
7978    // ─── SharedWorkerChannelBridge (REQ-BRW-004 / DF-WK-7) ───────────────
7979    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:SharedWorker] [DF-WK-7] [level:unit]
7980
7981    #[test]
7982    fn test_shared_worker_channel_bridge_new() {
7983        let id = SharedWorkerId {
7984            script_url: "sw.js".to_string(),
7985            name: "test".to_string(),
7986        };
7987        let bridge = SharedWorkerChannelBridge::new(id.clone());
7988        assert_eq!(bridge.shared_worker_id, id);
7989        assert_eq!(bridge.port_count(), 0);
7990    }
7991
7992    #[test]
7993    fn test_shared_worker_channel_bridge_add_port() {
7994        let id = SharedWorkerId {
7995            script_url: "sw.js".to_string(),
7996            name: "test".to_string(),
7997        };
7998        let mut bridge = SharedWorkerChannelBridge::new(id.clone());
7999        let endpoints = bridge.add_port();
8000        assert_eq!(bridge.port_count(), 1);
8001        assert_eq!(endpoints.shared_worker_id, id);
8002        assert!(endpoints.page_to_worker_rx.is_some());
8003        assert!(endpoints.worker_to_page_tx.is_some());
8004    }
8005
8006    #[test]
8007    fn test_shared_worker_channel_bridge_multiple_ports() {
8008        let id = SharedWorkerId {
8009            script_url: "sw.js".to_string(),
8010            name: "test".to_string(),
8011        };
8012        let mut bridge = SharedWorkerChannelBridge::new(id);
8013        bridge.add_port(); // Page 1
8014        bridge.add_port(); // Page 2
8015        bridge.add_port(); // Page 3
8016        assert_eq!(bridge.port_count(), 3);
8017    }
8018
8019    #[test]
8020    fn test_shared_worker_channel_bridge_drain_all() {
8021        let id = SharedWorkerId {
8022            script_url: "sw.js".to_string(),
8023            name: "test".to_string(),
8024        };
8025        let mut bridge = SharedWorkerChannelBridge::new(id);
8026        let endpoints1 = bridge.add_port();
8027        let endpoints2 = bridge.add_port();
8028        // Send messages from both ports
8029        let tx1 = endpoints1.worker_to_page_tx.unwrap();
8030        let tx2 = endpoints2.worker_to_page_tx.unwrap();
8031        tx1.send(WorkerStructuredMessage::metadata_only(
8032            WorkerId("sw.js".to_string()),
8033            WorkerMessageDirection::WorkerToPage,
8034        ))
8035        .unwrap();
8036        tx2.send(WorkerStructuredMessage::metadata_only(
8037            WorkerId("sw.js".to_string()),
8038            WorkerMessageDirection::WorkerToPage,
8039        ))
8040        .unwrap();
8041        let (messages, disconnected) = bridge.drain_all_worker_messages();
8042        assert_eq!(messages.len(), 2);
8043        assert!(disconnected.is_empty());
8044    }
8045
8046    #[test]
8047    fn test_shared_worker_channel_bridge_post_to_worker() {
8048        let id = SharedWorkerId {
8049            script_url: "sw.js".to_string(),
8050            name: "test".to_string(),
8051        };
8052        let mut bridge = SharedWorkerChannelBridge::new(id);
8053        let endpoints = bridge.add_port();
8054        let payload = StructuredClonePayload {
8055            data: vec![42],
8056            transferable_count: 0,
8057        };
8058        bridge.post_to_worker_from_port(0, payload).unwrap();
8059        let rx = endpoints.page_to_worker_rx.unwrap();
8060        let received = rx.try_recv().unwrap();
8061        assert_eq!(received.data, vec![42]);
8062    }
8063
8064    #[test]
8065    fn test_shared_worker_channel_bridge_post_invalid_port() {
8066        let id = SharedWorkerId {
8067            script_url: "sw.js".to_string(),
8068            name: "test".to_string(),
8069        };
8070        let mut bridge = SharedWorkerChannelBridge::new(id);
8071        bridge.add_port();
8072        let payload = StructuredClonePayload {
8073            data: vec![],
8074            transferable_count: 0,
8075        };
8076        let result = bridge.post_to_worker_from_port(99, payload);
8077        assert!(result.is_err());
8078    }
8079
8080    // ─── BaoWebViewState SharedWorker Channel & Scope (REQ-BRW-004 / DF-WK-7) ──
8081    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:SharedWorker] [DF-WK-7] [level:unit]
8082
8083    #[test]
8084    fn test_webview_state_shared_worker_channel_registration() {
8085        let mut state = BaoWebViewState::default();
8086        let id = SharedWorkerId {
8087            script_url: "sw.js".to_string(),
8088            name: "test".to_string(),
8089        };
8090        let bridge = SharedWorkerChannelBridge::new(id.clone());
8091        state.register_shared_worker_channel(bridge);
8092        assert!(state.shared_worker_channel(&id).is_some());
8093        assert_eq!(state.shared_worker_channel_count(), 0); // no ports yet
8094    }
8095
8096    #[test]
8097    fn test_webview_state_create_shared_worker_channel() {
8098        let mut state = BaoWebViewState::default();
8099        let id = SharedWorkerId {
8100            script_url: "sw.js".to_string(),
8101            name: "test".to_string(),
8102        };
8103        state.create_shared_worker_channel(id.clone());
8104        assert!(state.shared_worker_channel(&id).is_some());
8105    }
8106
8107    #[test]
8108    fn test_webview_state_add_shared_worker_port() {
8109        let mut state = BaoWebViewState::default();
8110        let id = SharedWorkerId {
8111            script_url: "sw.js".to_string(),
8112            name: "test".to_string(),
8113        };
8114        let endpoints = state.add_shared_worker_port(id.clone());
8115        assert_eq!(state.shared_worker_channel_count(), 1);
8116        assert_eq!(endpoints.shared_worker_id, id);
8117        assert!(endpoints.page_to_worker_rx.is_some());
8118        assert!(endpoints.worker_to_page_tx.is_some());
8119    }
8120
8121    #[test]
8122    fn test_webview_state_add_shared_worker_port_multiple() {
8123        let mut state = BaoWebViewState::default();
8124        let id = SharedWorkerId {
8125            script_url: "sw.js".to_string(),
8126            name: "test".to_string(),
8127        };
8128        state.add_shared_worker_port(id.clone());
8129        state.add_shared_worker_port(id.clone());
8130        assert_eq!(state.shared_worker_channel_count(), 2); // 2 ports
8131    }
8132
8133    #[test]
8134    fn test_webview_state_drain_all_shared_worker_messages() {
8135        let mut state = BaoWebViewState::default();
8136        let id = SharedWorkerId {
8137            script_url: "sw.js".to_string(),
8138            name: "test".to_string(),
8139        };
8140        let endpoints = state.add_shared_worker_port(id);
8141        let tx = endpoints.worker_to_page_tx.unwrap();
8142        tx.send(WorkerStructuredMessage::metadata_only(
8143            WorkerId("sw.js".to_string()),
8144            WorkerMessageDirection::WorkerToPage,
8145        ))
8146        .unwrap();
8147        let (messages, disconnected) = state.drain_all_shared_worker_messages();
8148        assert_eq!(messages.len(), 1);
8149        assert!(disconnected.is_empty());
8150    }
8151
8152    #[test]
8153    fn test_webview_state_drain_and_forward_shared_worker_messages() {
8154        let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
8155        let mut state = BaoWebViewState {
8156            event_tx: Some(tx),
8157            ..Default::default()
8158        };
8159        let id = SharedWorkerId {
8160            script_url: "sw.js".to_string(),
8161            name: "test".to_string(),
8162        };
8163        let endpoints = state.add_shared_worker_port(id);
8164        let worker_tx = endpoints.worker_to_page_tx.unwrap();
8165        worker_tx
8166            .send(WorkerStructuredMessage::metadata_only(
8167                WorkerId("sw.js".to_string()),
8168                WorkerMessageDirection::WorkerToPage,
8169            ))
8170            .unwrap();
8171        state.drain_and_forward_shared_worker_messages();
8172        let event = rx.try_recv().unwrap();
8173        match event {
8174            ServoEvent::Console { text, .. } => {
8175                assert!(text.contains("worker→page"));
8176            }
8177            _ => panic!("expected Console event for shared worker message"),
8178        }
8179    }
8180
8181    #[test]
8182    fn test_webview_state_disconnect_shared_worker_clears_channels() {
8183        let mut state = BaoWebViewState::default();
8184        let id = SharedWorkerId {
8185            script_url: "sw.js".to_string(),
8186            name: "test".to_string(),
8187        };
8188        state.track_shared_worker_port(SharedWorkerPortRef::new(SharedWorkerHandle::new(
8189            "sw.js".to_string(),
8190            "test".to_string(),
8191        )));
8192        state.add_shared_worker_port(id.clone());
8193        assert_eq!(state.shared_worker_port_count(), 1);
8194        assert_eq!(state.shared_worker_channel_count(), 1);
8195        state.disconnect_shared_worker_ports();
8196        assert_eq!(state.shared_worker_port_count(), 0);
8197        assert_eq!(state.shared_worker_channel_count(), 0);
8198    }
8199
8200    #[test]
8201    fn test_webview_state_shared_worker_scope_registration() {
8202        let mut state = BaoWebViewState::default();
8203        let id = SharedWorkerId {
8204            script_url: "sw.js".to_string(),
8205            name: "test".to_string(),
8206        };
8207        let config = SharedWorkerScopeConfig::default();
8208        let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8209        state.register_shared_worker_scope(id.clone(), scope);
8210        assert_eq!(state.shared_worker_scope_count(), 1);
8211        assert!(state.shared_worker_scope(&id).is_some());
8212    }
8213
8214    #[test]
8215    fn test_webview_state_shared_worker_scope_get_mut() {
8216        let mut state = BaoWebViewState::default();
8217        let id = SharedWorkerId {
8218            script_url: "sw.js".to_string(),
8219            name: "test".to_string(),
8220        };
8221        let config = SharedWorkerScopeConfig::default();
8222        let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8223        state.register_shared_worker_scope(id.clone(), scope);
8224        state.shared_worker_scope_mut(&id).unwrap().set_onconnect();
8225        assert!(state.shared_worker_scope(&id).unwrap().has_onconnect);
8226    }
8227
8228    #[test]
8229    fn test_webview_state_shared_worker_scope_remove() {
8230        let mut state = BaoWebViewState::default();
8231        let id = SharedWorkerId {
8232            script_url: "sw.js".to_string(),
8233            name: "test".to_string(),
8234        };
8235        let config = SharedWorkerScopeConfig::default();
8236        let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8237        state.register_shared_worker_scope(id.clone(), scope);
8238        let removed = state.remove_shared_worker_scope(&id);
8239        assert!(removed.is_some());
8240        assert_eq!(state.shared_worker_scope_count(), 0);
8241    }
8242
8243    #[test]
8244    fn test_webview_state_shared_worker_scopes_snapshot() {
8245        let mut state = BaoWebViewState::default();
8246        let id1 = SharedWorkerId {
8247            script_url: "sw1.js".to_string(),
8248            name: "a".to_string(),
8249        };
8250        let id2 = SharedWorkerId {
8251            script_url: "sw2.js".to_string(),
8252            name: "b".to_string(),
8253        };
8254        let config = SharedWorkerScopeConfig::default();
8255        state.register_shared_worker_scope(
8256            id1,
8257            SharedWorkerGlobalScopeState::new(
8258                SharedWorkerId {
8259                    script_url: "sw1.js".to_string(),
8260                    name: "a".to_string(),
8261                },
8262                &config,
8263            ),
8264        );
8265        state.register_shared_worker_scope(
8266            id2,
8267            SharedWorkerGlobalScopeState::new(
8268                SharedWorkerId {
8269                    script_url: "sw2.js".to_string(),
8270                    name: "b".to_string(),
8271                },
8272                &config,
8273            ),
8274        );
8275        let scopes = state.shared_worker_scopes();
8276        assert_eq!(scopes.len(), 2);
8277    }
8278
8279    #[test]
8280    fn test_webview_state_disconnect_shared_worker_clears_scopes() {
8281        let mut state = BaoWebViewState::default();
8282        let id = SharedWorkerId {
8283            script_url: "sw.js".to_string(),
8284            name: "test".to_string(),
8285        };
8286        let config = SharedWorkerScopeConfig::default();
8287        state.register_shared_worker_scope(
8288            id,
8289            SharedWorkerGlobalScopeState::new(
8290                SharedWorkerId {
8291                    script_url: "sw.js".to_string(),
8292                    name: "test".to_string(),
8293                },
8294                &config,
8295            ),
8296        );
8297        assert_eq!(state.shared_worker_scope_count(), 1);
8298        state.disconnect_shared_worker_ports();
8299        assert_eq!(state.shared_worker_scope_count(), 0);
8300    }
8301
8302    #[test]
8303    fn test_webview_state_set_shared_worker_scope_config() {
8304        let mut state = BaoWebViewState::default();
8305        let id = SharedWorkerId {
8306            script_url: "sw.js".to_string(),
8307            name: "test".to_string(),
8308        };
8309        let config = SharedWorkerScopeConfig::default();
8310        state.register_shared_worker_scope(
8311            id.clone(),
8312            SharedWorkerGlobalScopeState::new(id.clone(), &config),
8313        );
8314        assert!(state
8315            .shared_worker_scope(&id)
8316            .unwrap()
8317            .navigator()
8318            .user_agent
8319            .is_empty());
8320        let new_config = SharedWorkerScopeConfig {
8321            stealth_profile: None,
8322            user_agent: "Bao/1.0".to_string(),
8323            platform: "Linux".to_string(),
8324            hardware_concurrency: 8,
8325            language: "en-US".to_string(),
8326            languages: vec!["en-US".to_string()],
8327        };
8328        state.set_shared_worker_scope_config(&id, &new_config);
8329        assert_eq!(
8330            state
8331                .shared_worker_scope(&id)
8332                .unwrap()
8333                .navigator()
8334                .user_agent,
8335            "Bao/1.0"
8336        );
8337    }
8338
8339    // ─── BaoServoDelegate SharedWorker Routing (REQ-BRW-004 / DF-WK-7) ─────
8340    // @trace REQ-BRW-004 [req:REQ-BRW-004] [entity:SharedWorker] [DF-WK-7] [level:unit]
8341
8342    #[test]
8343    fn test_delegate_route_shared_worker_new() {
8344        let delegate = BaoServoDelegate::new();
8345        let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8346        let (returned, is_new) = delegate.route_shared_worker(handle);
8347        assert!(is_new);
8348        assert_eq!(returned.script_url, "sw.js");
8349        assert_eq!(delegate.shared_worker_count(), 1);
8350    }
8351
8352    #[test]
8353    fn test_delegate_route_shared_worker_existing() {
8354        let delegate = BaoServoDelegate::new();
8355        let handle1 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8356        let handle2 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8357        delegate.route_shared_worker(handle1);
8358        let (_, is_new) = delegate.route_shared_worker(handle2);
8359        assert!(
8360            !is_new,
8361            "same (url, name) should return existing, not create new"
8362        );
8363        assert_eq!(delegate.shared_worker_count(), 1);
8364    }
8365
8366    #[test]
8367    fn test_delegate_get_or_create_shared_worker_new() {
8368        let delegate = BaoServoDelegate::new();
8369        let (handle, is_new) = delegate.get_or_create_shared_worker("sw.js", "myname");
8370        assert!(is_new);
8371        assert_eq!(handle.script_url, "sw.js");
8372        assert_eq!(handle.name, "myname");
8373    }
8374
8375    #[test]
8376    fn test_delegate_get_or_create_shared_worker_existing() {
8377        let delegate = BaoServoDelegate::new();
8378        delegate.get_or_create_shared_worker("sw.js", "myname");
8379        let (_, is_new) = delegate.get_or_create_shared_worker("sw.js", "myname");
8380        assert!(!is_new);
8381        assert_eq!(delegate.shared_worker_count(), 1);
8382    }
8383
8384    #[test]
8385    fn test_delegate_unregister_shared_worker() {
8386        let delegate = BaoServoDelegate::new();
8387        let id = SharedWorkerId {
8388            script_url: "sw.js".to_string(),
8389            name: "myname".to_string(),
8390        };
8391        delegate.get_or_create_shared_worker("sw.js", "myname");
8392        assert_eq!(delegate.shared_worker_count(), 1);
8393        let removed = delegate.unregister_shared_worker(&id);
8394        assert!(removed);
8395        assert_eq!(delegate.shared_worker_count(), 0);
8396    }
8397
8398    #[test]
8399    fn test_delegate_unregister_nonexistent_shared_worker() {
8400        let delegate = BaoServoDelegate::new();
8401        let id = SharedWorkerId {
8402            script_url: "sw.js".to_string(),
8403            name: "nonexistent".to_string(),
8404        };
8405        let removed = delegate.unregister_shared_worker(&id);
8406        assert!(!removed);
8407    }
8408
8409    #[test]
8410    fn test_delegate_all_shared_workers() {
8411        let delegate = BaoServoDelegate::new();
8412        delegate.get_or_create_shared_worker("sw1.js", "a");
8413        delegate.get_or_create_shared_worker("sw2.js", "b");
8414        let all = delegate.all_shared_workers();
8415        assert_eq!(all.len(), 2);
8416    }
8417
8418    #[test]
8419    fn test_shared_worker_cross_page_routing_full_lifecycle() {
8420        // @trace REQ-BRW-004 [entity:SharedWorker] [entity:SharedWorkerGlobalScope] DF-WK-7
8421        // Full lifecycle: route → register scope → add ports → drain messages → disconnect → reap
8422        let delegate = BaoServoDelegate::new();
8423
8424        // Page 1 creates SharedWorker
8425        let (handle, is_new) = delegate.route_shared_worker(SharedWorkerHandle::new(
8426            "sw.js".to_string(),
8427            "shared".to_string(),
8428        ));
8429        assert!(is_new);
8430        assert_eq!(handle.connected_page_count(), 0);
8431
8432        // Page 1 connects
8433        let mut state1 = BaoWebViewState::default();
8434        let id = SharedWorkerId {
8435            script_url: "sw.js".to_string(),
8436            name: "shared".to_string(),
8437        };
8438        let config = SharedWorkerScopeConfig::default();
8439        state1.register_shared_worker_scope(
8440            id.clone(),
8441            SharedWorkerGlobalScopeState::new(id.clone(), &config),
8442        );
8443        state1.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
8444        // SharedWorkerPortRef::new already increments connected_page_count
8445        assert_eq!(handle.connected_page_count(), 1);
8446        // Each page gets its own channel bridge (ports are per-page)
8447        let endpoints1 = state1.add_shared_worker_port(id.clone());
8448
8449        // Page 2 connects (same SharedWorker, but its own channel bridge)
8450        let mut state2 = BaoWebViewState::default();
8451        state2.register_shared_worker_scope(
8452            id.clone(),
8453            SharedWorkerGlobalScopeState::new(id.clone(), &config),
8454        );
8455        state2.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
8456        let endpoints2 = state2.add_shared_worker_port(id.clone());
8457        assert_eq!(handle.connected_page_count(), 2);
8458
8459        // Both pages can send messages to the SharedWorker through their own ports
8460        let payload1 = StructuredClonePayload {
8461            data: vec![1],
8462            transferable_count: 0,
8463        };
8464        state1
8465            .post_to_worker_via_shared_port(&id, 0, payload1)
8466            .unwrap();
8467        let payload2 = StructuredClonePayload {
8468            data: vec![2],
8469            transferable_count: 0,
8470        };
8471        state2
8472            .post_to_worker_via_shared_port(&id, 0, payload2)
8473            .unwrap();
8474
8475        // Worker thread receives from both pages
8476        let rx1 = endpoints1.page_to_worker_rx.unwrap();
8477        let rx2 = endpoints2.page_to_worker_rx.unwrap();
8478        assert_eq!(rx1.try_recv().unwrap().data, vec![1]);
8479        assert_eq!(rx2.try_recv().unwrap().data, vec![2]);
8480
8481        // SharedWorker sends messages back to both pages
8482        let tx1 = endpoints1.worker_to_page_tx.unwrap();
8483        let tx2 = endpoints2.worker_to_page_tx.unwrap();
8484        tx1.send(WorkerStructuredMessage::metadata_only(
8485            WorkerId("sw.js".to_string()),
8486            WorkerMessageDirection::WorkerToPage,
8487        ))
8488        .unwrap();
8489        tx2.send(WorkerStructuredMessage::metadata_only(
8490            WorkerId("sw.js".to_string()),
8491            WorkerMessageDirection::WorkerToPage,
8492        ))
8493        .unwrap();
8494
8495        // Page 1 drains its messages
8496        let (msgs1, disc1) = state1.drain_all_shared_worker_messages();
8497        assert_eq!(msgs1.len(), 1);
8498        assert!(disc1.is_empty());
8499        // Page 2 drains its messages
8500        let (msgs2, disc2) = state2.drain_all_shared_worker_messages();
8501        assert_eq!(msgs2.len(), 1);
8502        assert!(disc2.is_empty());
8503
8504        // Page 1 navigates away — SharedWorker survives
8505        state1.disconnect_shared_worker_ports();
8506        // disconnect drops the SharedWorkerPortRef → connected_page_count decrements
8507        assert_eq!(handle.connected_page_count(), 1);
8508        assert!(!handle.is_closing());
8509
8510        // Page 2 still connected
8511        assert_eq!(state2.shared_worker_port_count(), 1);
8512
8513        // SharedWorker self.close() — terminates
8514        handle.close();
8515        handle.mark_terminated();
8516        assert!(handle.is_closing());
8517        assert!(handle.is_terminated());
8518
8519        // Delegate reaps terminated shared worker with zero connected pages
8520        // (after page 2 also disconnects)
8521        state2.disconnect_shared_worker_ports();
8522        assert_eq!(handle.connected_page_count(), 0);
8523        delegate.reap_terminated_shared_workers();
8524        assert_eq!(delegate.shared_worker_count(), 0);
8525    }
8526
8527    // ─── ServiceWorker Registration & Fetch Interception (REQ-BRW-004 criterion #19) ────
8528    // @trace REQ-BRW-004 [entity:ServiceWorker] [entity:ServiceWorkerGlobalScope]
8529    //   [criterion:19] DF-WK-8 / DF-WK-10
8530
8531    #[test]
8532    fn test_service_worker_registration_id_equality() {
8533        let id1 = ServiceWorkerRegistrationId {
8534            script_url: "sw.js".to_string(),
8535            scope: "/".to_string(),
8536        };
8537        let id2 = ServiceWorkerRegistrationId {
8538            script_url: "sw.js".to_string(),
8539            scope: "/".to_string(),
8540        };
8541        let id3 = ServiceWorkerRegistrationId {
8542            script_url: "sw.js".to_string(),
8543            scope: "/app/".to_string(),
8544        };
8545        assert_eq!(id1, id2);
8546        assert_ne!(id1, id3);
8547    }
8548
8549    #[test]
8550    fn test_service_worker_handle_lifecycle() {
8551        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8552        assert!(!handle.is_closing());
8553        assert!(!handle.is_terminated());
8554        assert_eq!(
8555            handle.registration_state(),
8556            ServiceWorkerRegistrationState::Installing
8557        );
8558        assert!(!handle.is_intercepting_fetch());
8559    }
8560
8561    #[test]
8562    fn test_service_worker_handle_state_transitions() {
8563        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8564        // Installing → Installed
8565        handle.transition_state(ServiceWorkerRegistrationState::Installed);
8566        assert_eq!(
8567            handle.registration_state(),
8568            ServiceWorkerRegistrationState::Installed
8569        );
8570
8571        // Installed → Activating
8572        handle.transition_state(ServiceWorkerRegistrationState::Activating);
8573        assert_eq!(
8574            handle.registration_state(),
8575            ServiceWorkerRegistrationState::Activating
8576        );
8577
8578        // Activating → Activated + enable fetch interception
8579        handle.transition_state(ServiceWorkerRegistrationState::Activated);
8580        handle.enable_fetch_interception();
8581        assert_eq!(
8582            handle.registration_state(),
8583            ServiceWorkerRegistrationState::Activated
8584        );
8585        assert!(handle.is_intercepting_fetch());
8586        assert_eq!(
8587            handle.fetch_intercept_mode(),
8588            ServiceWorkerFetchInterceptMode::Intercepting
8589        );
8590    }
8591
8592    #[test]
8593    fn test_service_worker_handle_terminate_disables_interception() {
8594        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8595        handle.enable_fetch_interception();
8596        assert!(handle.is_intercepting_fetch());
8597
8598        // Per SPEC criterion #19: "terminate 后正确注销"
8599        handle.terminate();
8600        assert!(handle.is_closing());
8601        assert!(!handle.is_intercepting_fetch());
8602        assert_eq!(
8603            handle.fetch_intercept_mode(),
8604            ServiceWorkerFetchInterceptMode::None
8605        );
8606    }
8607
8608    #[test]
8609    fn test_service_worker_scope_config_from_stealth_profile() {
8610        let profile = bao_stealth::StealthProfile::chrome_default();
8611        let config = ServiceWorkerScopeConfig::from(&profile);
8612        assert!(config.stealth_profile.is_some());
8613        assert_eq!(config.user_agent, profile.navigator.user_agent);
8614        assert_eq!(config.platform, profile.navigator.platform);
8615        assert_eq!(
8616            config.hardware_concurrency,
8617            profile.navigator.hardware_concurrency as usize
8618        );
8619        assert_eq!(config.language, profile.navigator.language);
8620    }
8621
8622    #[test]
8623    fn test_service_worker_global_scope_state() {
8624        let reg_id = ServiceWorkerRegistrationId {
8625            script_url: "sw.js".to_string(),
8626            scope: "/app/".to_string(),
8627        };
8628        let config = ServiceWorkerScopeConfig::default();
8629        let scope = ServiceWorkerGlobalScopeState::new(reg_id.clone(), &config);
8630
8631        assert!(!scope.has_fetch_handler);
8632        assert!(!scope.has_activate_handler);
8633        assert!(!scope.has_install_handler);
8634        assert!(!scope.has_message_handler);
8635        assert_eq!(scope.scope_url, "/app/");
8636        assert!(scope.is_url_in_scope("/app/page1"));
8637        assert!(scope.is_url_in_scope("/app/sub/page2"));
8638        assert!(!scope.is_url_in_scope("/other/page"));
8639    }
8640
8641    #[test]
8642    fn test_service_worker_global_scope_fetch_handler() {
8643        let reg_id = ServiceWorkerRegistrationId {
8644            script_url: "sw.js".to_string(),
8645            scope: "/".to_string(),
8646        };
8647        let config = ServiceWorkerScopeConfig::default();
8648        let mut scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8649
8650        scope.set_fetch_handler();
8651        assert!(scope.has_fetch_handler);
8652        assert!(scope.is_url_in_scope("/anything"));
8653    }
8654
8655    #[test]
8656    fn test_webview_state_service_worker_control() {
8657        let mut state = BaoWebViewState::default();
8658        assert!(!state.is_controlled_by_service_worker());
8659
8660        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8661        state.set_controlling_service_worker(handle);
8662        assert!(state.is_controlled_by_service_worker());
8663
8664        state.clear_controlling_service_worker();
8665        assert!(!state.is_controlled_by_service_worker());
8666    }
8667
8668    #[test]
8669    fn test_webview_state_service_worker_scope_matching() {
8670        let mut state = BaoWebViewState::default();
8671        assert!(!state.is_url_in_service_worker_scope("/app/page1"));
8672
8673        let reg_id = ServiceWorkerRegistrationId {
8674            script_url: "sw.js".to_string(),
8675            scope: "/app/".to_string(),
8676        };
8677        let config = ServiceWorkerScopeConfig::default();
8678        let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8679        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/app/".to_string(), None);
8680        state.set_controlling_service_worker(handle);
8681        state.register_service_worker_scope(scope);
8682
8683        assert!(state.is_url_in_service_worker_scope("/app/page1"));
8684        assert!(state.is_url_in_service_worker_scope("/app/sub/page2"));
8685        assert!(!state.is_url_in_service_worker_scope("/other/page"));
8686    }
8687
8688    #[test]
8689    fn test_delegate_service_worker_registration() {
8690        let delegate = BaoServoDelegate::new();
8691        assert_eq!(delegate.service_worker_count(), 0);
8692
8693        let (handle, is_new) = delegate.get_or_create_service_worker("sw.js", "/", None);
8694        assert!(is_new);
8695        assert_eq!(delegate.service_worker_count(), 1);
8696
8697        // Re-register same (script_url, scope) returns existing
8698        let (handle2, is_new2) = delegate.get_or_create_service_worker("sw.js", "/", None);
8699        assert!(!is_new2);
8700        assert_eq!(delegate.service_worker_count(), 1);
8701    }
8702
8703    #[test]
8704    fn test_delegate_find_service_worker_for_url() {
8705        let delegate = BaoServoDelegate::new();
8706
8707        // Register a ServiceWorker for /app/ scope
8708        let handle = delegate
8709            .get_or_create_service_worker("sw.js", "/app/", None)
8710            .0;
8711
8712        // Not intercepting yet — find_service_worker_for_url returns None
8713        assert!(delegate.find_service_worker_for_url("/app/page1").is_none());
8714
8715        // Activate and enable fetch interception
8716        handle.transition_state(ServiceWorkerRegistrationState::Activated);
8717        handle.enable_fetch_interception();
8718
8719        // Now it should be found for URLs in scope
8720        let found = delegate.find_service_worker_for_url("/app/page1");
8721        assert!(found.is_some());
8722        assert_eq!(found.unwrap().script_url, "sw.js");
8723
8724        // Not found for URLs outside scope
8725        assert!(delegate
8726            .find_service_worker_for_url("/other/page")
8727            .is_none());
8728    }
8729
8730    #[test]
8731    fn test_delegate_service_worker_unregistration() {
8732        let delegate = BaoServoDelegate::new();
8733        let (handle, _) = delegate.get_or_create_service_worker("sw.js", "/", None);
8734        assert_eq!(delegate.service_worker_count(), 1);
8735
8736        let id = handle.id();
8737        assert!(delegate.unregister_service_worker(&id));
8738        assert_eq!(delegate.service_worker_count(), 0);
8739
8740        // Double unregister returns false
8741        assert!(!delegate.unregister_service_worker(&id));
8742    }
8743
8744    #[test]
8745    fn test_delegate_service_worker_stealth_consistency_no_violations() {
8746        let delegate = BaoServoDelegate::new();
8747        let profile = bao_stealth::StealthProfile::chrome_default();
8748
8749        // Register a ServiceWorker with matching stealth profile
8750        let handle =
8751            ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), Some(profile.clone()));
8752        delegate.register_service_worker(handle);
8753        // Activate to enable fetch interception
8754        let all = delegate.all_service_workers();
8755        all[0].transition_state(ServiceWorkerRegistrationState::Activated);
8756        all[0].enable_fetch_interception();
8757
8758        let violations = delegate.verify_service_worker_stealth_consistency(&profile);
8759        assert!(violations.is_empty());
8760    }
8761
8762    #[test]
8763    fn test_delegate_service_worker_stealth_consistency_violation_no_profile() {
8764        let delegate = BaoServoDelegate::new();
8765        let profile = bao_stealth::StealthProfile::chrome_default();
8766
8767        // Register a ServiceWorker without stealth profile — this is a violation
8768        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8769        delegate.register_service_worker(handle);
8770        let all = delegate.all_service_workers();
8771        all[0].transition_state(ServiceWorkerRegistrationState::Activated);
8772        all[0].enable_fetch_interception();
8773
8774        let violations = delegate.verify_service_worker_stealth_consistency(&profile);
8775        assert_eq!(violations.len(), 1);
8776    }
8777
8778    #[test]
8779    fn test_service_worker_persistent_lifecycle_across_page_navigation() {
8780        // @trace REQ-BRW-004 [entity:ServiceWorker] [criterion:19]
8781        // SPEC criterion #19: "SW 持久生命周期(跨页存活)下 profile 继承注册页
8782        // 且 terminate 后正确注销"
8783        let delegate = BaoServoDelegate::new();
8784        let profile = bao_stealth::StealthProfile::chrome_default();
8785
8786        // Page 1 registers a ServiceWorker
8787        let (handle, is_new) =
8788            delegate.get_or_create_service_worker("sw.js", "/", Some(profile.clone()));
8789        assert!(is_new);
8790        handle.transition_state(ServiceWorkerRegistrationState::Activated);
8791        handle.enable_fetch_interception();
8792
8793        // Page 1 is controlled by the ServiceWorker
8794        let mut page_state = BaoWebViewState::default();
8795        page_state.set_controlling_service_worker(handle.clone());
8796        let reg_id = ServiceWorkerRegistrationId {
8797            script_url: "sw.js".to_string(),
8798            scope: "/".to_string(),
8799        };
8800        let config = ServiceWorkerScopeConfig::from(&profile);
8801        page_state
8802            .register_service_worker_scope(ServiceWorkerGlobalScopeState::new(reg_id, &config));
8803        assert!(page_state.is_controlled_by_service_worker());
8804
8805        // Page navigation: clear controlling reference (SW survives in delegate registry)
8806        page_state.clear_controlling_service_worker();
8807        assert!(!page_state.is_controlled_by_service_worker());
8808
8809        // ServiceWorker still exists in delegate registry
8810        assert_eq!(delegate.service_worker_count(), 1);
8811        assert!(delegate.find_service_worker_for_url("/page2").is_some());
8812
8813        // Page 2 can be controlled by the same ServiceWorker
8814        let mut page2_state = BaoWebViewState::default();
8815        page2_state.set_controlling_service_worker(handle.clone());
8816        assert!(page2_state.is_controlled_by_service_worker());
8817    }
8818
8819    #[test]
8820    fn test_service_worker_fetch_intercept_mode() {
8821        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8822        assert_eq!(
8823            handle.fetch_intercept_mode(),
8824            ServiceWorkerFetchInterceptMode::None
8825        );
8826
8827        handle.enable_fetch_interception();
8828        assert_eq!(
8829            handle.fetch_intercept_mode(),
8830            ServiceWorkerFetchInterceptMode::Intercepting
8831        );
8832
8833        handle.disable_fetch_interception();
8834        assert_eq!(
8835            handle.fetch_intercept_mode(),
8836            ServiceWorkerFetchInterceptMode::None
8837        );
8838    }
8839
8840    #[test]
8841    fn test_service_worker_registration_state_all_transitions() {
8842        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8843        assert_eq!(
8844            handle.registration_state(),
8845            ServiceWorkerRegistrationState::Installing
8846        );
8847
8848        handle.transition_state(ServiceWorkerRegistrationState::Installed);
8849        assert_eq!(
8850            handle.registration_state(),
8851            ServiceWorkerRegistrationState::Installed
8852        );
8853
8854        handle.transition_state(ServiceWorkerRegistrationState::Activating);
8855        assert_eq!(
8856            handle.registration_state(),
8857            ServiceWorkerRegistrationState::Activating
8858        );
8859
8860        handle.transition_state(ServiceWorkerRegistrationState::Activated);
8861        assert_eq!(
8862            handle.registration_state(),
8863            ServiceWorkerRegistrationState::Activated
8864        );
8865
8866        handle.transition_state(ServiceWorkerRegistrationState::Redundant);
8867        assert_eq!(
8868            handle.registration_state(),
8869            ServiceWorkerRegistrationState::Redundant
8870        );
8871    }
8872
8873    #[test]
8874    fn test_service_worker_navigator_from_scope_config() {
8875        let config = ServiceWorkerScopeConfig {
8876            stealth_profile: None,
8877            user_agent: "Mozilla/5.0 Test".to_string(),
8878            platform: "Linux x86_64".to_string(),
8879            hardware_concurrency: 4,
8880            language: "zh-CN".to_string(),
8881            languages: vec!["zh-CN".to_string(), "zh".to_string()],
8882            registering_page_url: "https://example.com/".to_string(),
8883        };
8884        let nav = WorkerNavigator::from_scope_config(&config);
8885        assert_eq!(nav.user_agent, "Mozilla/5.0 Test");
8886        assert_eq!(nav.platform, "Linux x86_64");
8887        assert_eq!(nav.hardware_concurrency, 4);
8888        assert_eq!(nav.language, "zh-CN");
8889        assert_eq!(nav.languages, vec!["zh-CN".to_string(), "zh".to_string()]);
8890    }
8891
8892    #[test]
8893    fn test_webview_state_service_worker_scope_config() {
8894        let mut state = BaoWebViewState::default();
8895        let reg_id = ServiceWorkerRegistrationId {
8896            script_url: "sw.js".to_string(),
8897            scope: "/".to_string(),
8898        };
8899        let config = ServiceWorkerScopeConfig::default();
8900        let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8901        let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8902        state.set_controlling_service_worker(handle);
8903        state.register_service_worker_scope(scope);
8904
8905        let new_config = ServiceWorkerScopeConfig {
8906            user_agent: "Updated Agent".to_string(),
8907            ..ServiceWorkerScopeConfig::default()
8908        };
8909        state.set_service_worker_scope_config(&new_config);
8910        assert_eq!(
8911            state.service_worker_scope().unwrap().navigator().user_agent,
8912            "Updated Agent"
8913        );
8914    }
8915}