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