Skip to main content

runtime_foxdriver/
network.rs

1//! Passive network instrumentation for BiDi browsers.
2//!
3//! Captures every request, response, and fetch error that flows through the
4//! browser, then provides ergonomic query, streaming, and export utilities.
5//!
6//! This module captures response *metadata* only. Response bodies are not
7//! fetched because the `rustenium-bidi-definitions` crate does not expose a
8//! `network.getResponseBody` command and the `ResponseContent` event only
9//! reports `size`. Callers that need body content must fetch it separately.
10
11use anyhow::Result;
12use rustenium_bidi_definitions::network::events::{
13    BeforeRequestSent, FetchError, ResponseCompleted,
14};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::{broadcast, RwLock};
19
20// ------------------------------------------------------------------
21// Data types
22// ------------------------------------------------------------------
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct CapturedHeader {
26    pub name: String,
27    pub value: String,
28}
29
30impl From<&rustenium_bidi_definitions::network::types::Header> for CapturedHeader {
31    fn from(h: &rustenium_bidi_definitions::network::types::Header) -> Self {
32        let value = match &h.value {
33            rustenium_bidi_definitions::network::types::BytesValue::StringValue(s) => {
34                s.value.clone()
35            }
36            rustenium_bidi_definitions::network::types::BytesValue::Base64Value(b) => {
37                b.value.clone()
38            }
39        };
40        Self {
41            name: h.name.clone(),
42            value,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
48pub struct CapturedTiming {
49    pub dns_start_ms: Option<f64>,
50    pub dns_end_ms: Option<f64>,
51    pub connect_start_ms: Option<f64>,
52    pub connect_end_ms: Option<f64>,
53    pub tls_start_ms: Option<f64>,
54    pub response_start_ms: Option<f64>,
55    pub response_end_ms: Option<f64>,
56}
57
58impl From<&rustenium_bidi_definitions::network::types::FetchTimingInfo> for CapturedTiming {
59    fn from(t: &rustenium_bidi_definitions::network::types::FetchTimingInfo) -> Self {
60        let origin = t.request_time;
61        Self {
62            dns_start_ms: non_neg(t.dns_start - origin),
63            dns_end_ms: non_neg(t.dns_end - origin),
64            connect_start_ms: non_neg(t.connect_start - origin),
65            connect_end_ms: non_neg(t.connect_end - origin),
66            tls_start_ms: non_neg(t.tls_start - origin),
67            response_start_ms: non_neg(t.response_start - origin),
68            response_end_ms: non_neg(t.response_end - origin),
69        }
70    }
71}
72
73fn non_neg(v: f64) -> Option<f64> {
74    if v >= 0.0 {
75        Some(v)
76    } else {
77        None
78    }
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct CapturedCookie {
83    pub name: String,
84    pub value: String,
85    pub domain: String,
86    pub path: String,
87    pub size: u64,
88    pub http_only: bool,
89    pub secure: bool,
90    pub same_site: String,
91}
92
93impl From<&rustenium_bidi_definitions::network::types::Cookie> for CapturedCookie {
94    fn from(c: &rustenium_bidi_definitions::network::types::Cookie) -> Self {
95        let value = match &c.value {
96            rustenium_bidi_definitions::network::types::BytesValue::StringValue(s) => {
97                s.value.clone()
98            }
99            rustenium_bidi_definitions::network::types::BytesValue::Base64Value(b) => {
100                b.value.clone()
101            }
102        };
103        Self {
104            name: c.name.clone(),
105            value,
106            domain: c.domain.clone(),
107            path: c.path.clone(),
108            size: c.size,
109            http_only: c.http_only,
110            secure: c.secure,
111            same_site: format!("{:?}", c.same_site).to_lowercase(),
112        }
113    }
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct CapturedRequest {
118    pub id: String,
119    pub context: Option<String>,
120    pub method: String,
121    pub url: String,
122    pub headers: Vec<CapturedHeader>,
123    pub post_data: Option<String>,
124    pub timestamp: u64,
125    pub destination: String,
126    pub initiator_type: Option<String>,
127    pub timing: CapturedTiming,
128    pub cookies: Vec<CapturedCookie>,
129}
130
131impl CapturedRequest {
132    /// If `post_data` looks like JSON, return it parsed.
133    pub fn json_body(&self) -> Option<serde_json::Value> {
134        self.post_data
135            .as_ref()
136            .and_then(|s| serde_json::from_str(s).ok())
137    }
138
139    /// Return the parsed query parameters as a vec of (key, value) pairs.
140    ///
141    /// Returns `Err` when `self.url` cannot be parsed as a URL, so callers can
142    /// distinguish "URL did not parse" from "URL had no query string".
143    pub fn query_params(&self) -> Result<Vec<(String, String)>, url::ParseError> {
144        let u = url::Url::parse(&self.url)?;
145        Ok(u.query_pairs()
146            .map(|(k, v)| (k.to_string(), v.to_string()))
147            .collect())
148    }
149
150    /// Case-insensitive header lookup.
151    pub fn request_header(&self, name: &str) -> Option<&str> {
152        let name_lower = name.to_lowercase();
153        self.headers
154            .iter()
155            .find(|h| h.name.to_lowercase() == name_lower)
156            .map(|h| h.value.as_str())
157    }
158}
159
160/// A captured network response.
161///
162/// `foxdriver` captures response **metadata** (headers, status, MIME, size)
163/// from BiDi `network.responseCompleted` events. It does **not** fetch or
164/// store response bodies: the `rustenium-bidi-definitions` crate used by this
165/// driver does not expose a `network.getResponseBody` command, and the
166/// `ResponseContent` event field only carries `size`. Callers that need body
167/// content must fetch it through another mechanism.
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
169pub struct CapturedResponse {
170    pub id: String,
171    pub url: String,
172    pub protocol: String,
173    pub status: u16,
174    pub status_text: String,
175    pub headers: Vec<CapturedHeader>,
176    pub mime_type: String,
177    /// Response body size, as reported by the browser.
178    ///
179    /// This is metadata only; the response body bytes are not captured.
180    pub body_size: Option<u64>,
181    pub from_cache: bool,
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct CapturedError {
186    pub id: String,
187    pub url: String,
188    pub error_text: String,
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct NetworkEntry {
193    pub request: CapturedRequest,
194    pub response: Option<CapturedResponse>,
195    pub error: Option<CapturedError>,
196}
197
198impl NetworkEntry {
199    pub fn has_response(&self) -> bool {
200        self.response.is_some()
201    }
202
203    pub fn is_error(&self) -> bool {
204        self.error.is_some()
205    }
206
207    pub fn final_url(&self) -> &str {
208        self.response
209            .as_ref()
210            .map(|r| r.url.as_str())
211            .unwrap_or(&self.request.url)
212    }
213
214    pub fn status(&self) -> Option<u16> {
215        self.response.as_ref().map(|r| r.status)
216    }
217
218    pub fn response_header(&self, name: &str) -> Option<&str> {
219        let name_lower = name.to_lowercase();
220        self.response.as_ref().and_then(|r| {
221            r.headers
222                .iter()
223                .find(|h| h.name.to_lowercase() == name_lower)
224                .map(|h| h.value.as_str())
225        })
226    }
227
228    pub fn request_header(&self, name: &str) -> Option<&str> {
229        self.request.request_header(name)
230    }
231
232    /// Generate a curl command that replays this request.
233    pub fn to_curl(&self) -> String {
234        fn shell_quote(s: &str) -> String {
235            // Escape single quotes for bash: ' -> '\''
236            s.replace('\'', "'\\''")
237        }
238        let req = &self.request;
239        let mut parts = vec![format!("curl -X {}", req.method)];
240        for h in &req.headers {
241            if h.name.eq_ignore_ascii_case("host")
242                || h.name.eq_ignore_ascii_case("connection")
243                || h.name.eq_ignore_ascii_case("accept-encoding")
244            {
245                continue;
246            }
247            parts.push(format!(
248                "-H '{}: {}'",
249                shell_quote(&h.name),
250                shell_quote(&h.value)
251            ));
252        }
253        if let Some(ref body) = req.post_data {
254            parts.push(format!("-d '{}'", shell_quote(body)));
255        }
256        parts.push(format!("'{}'", shell_quote(&req.url)));
257        parts.join(" ")
258    }
259}
260
261// ------------------------------------------------------------------
262// Filter
263// ------------------------------------------------------------------
264
265#[derive(Debug, Clone, Default)]
266pub struct Filter {
267    method: Option<String>,
268    status_range: Option<std::ops::RangeInclusive<u16>>,
269    url_substring: Option<String>,
270    url_regex: Option<regex::Regex>,
271    header_name: Option<String>,
272    header_value_substring: Option<String>,
273    destination: Option<String>,
274    has_response: Option<bool>,
275    has_error: Option<bool>,
276}
277
278impl Filter {
279    pub fn new() -> Self {
280        Self::default()
281    }
282
283    pub fn method(mut self, m: impl Into<String>) -> Self {
284        self.method = Some(m.into().to_uppercase());
285        self
286    }
287
288    pub fn status_range(mut self, r: std::ops::RangeInclusive<u16>) -> Self {
289        self.status_range = Some(r);
290        self
291    }
292
293    pub fn url_contains(mut self, needle: impl Into<String>) -> Self {
294        self.url_substring = Some(needle.into().to_lowercase());
295        self
296    }
297
298    pub fn url_regex(mut self, pattern: &str) -> Result<Self> {
299        self.url_regex = Some(regex::Regex::new(pattern)?);
300        Ok(self)
301    }
302
303    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
304        self.header_name = Some(name.into().to_lowercase());
305        self.header_value_substring = Some(value.into().to_lowercase());
306        self
307    }
308
309    pub fn destination(mut self, d: impl Into<String>) -> Self {
310        self.destination = Some(d.into().to_lowercase());
311        self
312    }
313
314    pub fn with_response(mut self) -> Self {
315        self.has_response = Some(true);
316        self
317    }
318
319    pub fn without_response(mut self) -> Self {
320        self.has_response = Some(false);
321        self
322    }
323
324    pub fn with_error(mut self) -> Self {
325        self.has_error = Some(true);
326        self
327    }
328
329    fn matches(&self, e: &NetworkEntry) -> bool {
330        if let Some(ref m) = self.method {
331            if e.request.method.to_uppercase() != *m {
332                return false;
333            }
334        }
335        if let Some(ref r) = self.status_range {
336            // A response with no status (fetch error, incomplete response)
337            // must not match a status range; treat missing as absent, not 0.
338            let Some(st) = e.status() else {
339                return false;
340            };
341            if !r.contains(&st) {
342                return false;
343            }
344        }
345        if let Some(ref needle) = self.url_substring {
346            if !e.request.url.to_lowercase().contains(needle) {
347                return false;
348            }
349        }
350        if let Some(ref re) = self.url_regex {
351            if !re.is_match(&e.request.url) {
352                return false;
353            }
354        }
355        if let (Some(ref name), Some(ref value)) = (&self.header_name, &self.header_value_substring)
356        {
357            let found = e
358                .request
359                .headers
360                .iter()
361                .chain(
362                    e.response
363                        .as_ref()
364                        .map(|r| r.headers.as_slice())
365                        .unwrap_or(&[]),
366                )
367                .any(|h| {
368                    h.name.to_lowercase().contains(name) && h.value.to_lowercase().contains(value)
369                });
370            if !found {
371                return false;
372            }
373        }
374        if let Some(ref d) = self.destination {
375            if e.request.destination.to_lowercase() != *d {
376                return false;
377            }
378        }
379        if let Some(want) = self.has_response {
380            if e.has_response() != want {
381                return false;
382            }
383        }
384        if let Some(want) = self.has_error {
385            if e.is_error() != want {
386                return false;
387            }
388        }
389        true
390    }
391}
392
393// ------------------------------------------------------------------
394// Metrics
395// ------------------------------------------------------------------
396
397#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
398pub struct NetworkMetrics {
399    pub requests_received: u64,
400    pub responses_received: u64,
401    pub errors_received: u64,
402    pub entries_evicted: u64,
403    pub pending_responses_dropped: u64,
404    pub pending_errors_dropped: u64,
405    pub broadcast_drops: u64,
406    pub duplicate_responses: u64,
407    pub duplicate_errors: u64,
408    pub max_entries: usize,
409}
410
411// ------------------------------------------------------------------
412// Inner log state
413// ------------------------------------------------------------------
414
415#[derive(Debug)]
416struct Inner {
417    entries: Vec<Arc<NetworkEntry>>,
418    by_id: HashMap<String, usize>,
419    pending_responses: HashMap<String, CapturedResponse>,
420    pending_errors: HashMap<String, CapturedError>,
421    max_entries: usize,
422    max_pending: usize,
423    tx: broadcast::Sender<Arc<NetworkEntry>>,
424    metrics: NetworkMetrics,
425    /// Whether we have already warned about entry eviction this session.
426    eviction_warned: bool,
427}
428
429impl Inner {
430    fn new(
431        max_entries: usize,
432        max_pending: usize,
433        tx: broadcast::Sender<Arc<NetworkEntry>>,
434    ) -> Self {
435        let metrics = NetworkMetrics {
436            max_entries,
437            ..NetworkMetrics::default()
438        };
439        Self {
440            entries: if max_entries > 0 {
441                Vec::with_capacity(max_entries)
442            } else {
443                Vec::new()
444            },
445            by_id: HashMap::new(),
446            pending_responses: HashMap::new(),
447            pending_errors: HashMap::new(),
448            max_entries,
449            max_pending,
450            tx,
451            metrics,
452            eviction_warned: false,
453        }
454    }
455
456    fn push_entry(&mut self, entry: Arc<NetworkEntry>) {
457        // Skip duplicate request IDs: BiDi can occasionally resend.
458        if self.by_id.contains_key(&entry.request.id) {
459            return;
460        }
461
462        // Evict just enough oldest entries to stay under the cap.
463        if self.max_entries > 0 && self.entries.len() >= self.max_entries {
464            let remove = self.entries.len().saturating_sub(self.max_entries - 1);
465            let removed = self.entries.drain(..remove).len();
466            self.metrics.entries_evicted += removed as u64;
467            self.by_id.clear();
468            for (i, e) in self.entries.iter().enumerate() {
469                self.by_id.insert(e.request.id.clone(), i);
470            }
471            if !self.eviction_warned {
472                self.eviction_warned = true;
473                tracing::warn!(
474                    removed,
475                    max_entries = self.max_entries,
476                    "[foxdriver] network capture entry evicted; oldest requests are being dropped"
477                );
478            }
479        }
480
481        let idx = self.entries.len();
482        self.by_id.insert(entry.request.id.clone(), idx);
483        self.entries.push(entry.clone());
484
485        // Broadcast (ignore errors (all receivers dropped)).
486        if self.tx.send(entry).is_err() {
487            self.metrics.broadcast_drops += 1;
488        }
489    }
490}
491
492// ------------------------------------------------------------------
493// NetworkLog
494// ------------------------------------------------------------------
495
496#[derive(Debug, Clone)]
497pub struct NetworkLog {
498    inner: Arc<RwLock<Inner>>,
499}
500
501impl NetworkLog {
502    pub fn new() -> Self {
503        Self::with_limits(50_000, 10_000)
504    }
505
506    pub fn with_limits(max_entries: usize, max_pending: usize) -> Self {
507        let (tx, _) = broadcast::channel(1024);
508        Self {
509            inner: Arc::new(RwLock::new(Inner::new(max_entries, max_pending, tx))),
510        }
511    }
512
513    pub async fn subscribe(&self) -> broadcast::Receiver<Arc<NetworkEntry>> {
514        self.inner.read().await.tx.subscribe()
515    }
516
517    pub async fn entries(&self) -> Vec<Arc<NetworkEntry>> {
518        let inner = self.inner.read().await;
519        inner.entries.clone()
520    }
521
522    pub async fn completed(&self) -> Vec<Arc<NetworkEntry>> {
523        let inner = self.inner.read().await;
524        inner
525            .entries
526            .iter()
527            .filter(|e| e.has_response() || e.is_error())
528            .cloned()
529            .collect()
530    }
531
532    pub async fn count(&self, filter: Filter) -> usize {
533        let inner = self.inner.read().await;
534        inner.entries.iter().filter(|e| filter.matches(e)).count()
535    }
536
537    pub async fn filter(&self, f: Filter) -> Vec<Arc<NetworkEntry>> {
538        let inner = self.inner.read().await;
539        inner
540            .entries
541            .iter()
542            .filter(|e| f.matches(e))
543            .cloned()
544            .collect()
545    }
546
547    pub async fn first(&self) -> Option<Arc<NetworkEntry>> {
548        let inner = self.inner.read().await;
549        inner.entries.first().cloned()
550    }
551
552    pub async fn last(&self) -> Option<Arc<NetworkEntry>> {
553        let inner = self.inner.read().await;
554        inner.entries.last().cloned()
555    }
556
557    pub async fn nth(&self, n: usize) -> Option<Arc<NetworkEntry>> {
558        let inner = self.inner.read().await;
559        inner.entries.get(n).cloned()
560    }
561
562    pub async fn find_by_url(&self, substring: &str) -> Option<Arc<NetworkEntry>> {
563        let inner = self.inner.read().await;
564        inner
565            .entries
566            .iter()
567            .find(|e| e.request.url.contains(substring))
568            .cloned()
569    }
570
571    pub async fn find_by_url_regex(&self, re: &regex::Regex) -> Option<Arc<NetworkEntry>> {
572        let inner = self.inner.read().await;
573        inner
574            .entries
575            .iter()
576            .find(|e| re.is_match(&e.request.url))
577            .cloned()
578    }
579
580    pub async fn endpoints(&self) -> Vec<String> {
581        let inner = self.inner.read().await;
582        let mut seen = std::collections::HashSet::new();
583        inner
584            .entries
585            .iter()
586            .filter(|e| seen.insert(e.request.url.clone()))
587            .map(|e| e.request.url.clone())
588            .collect()
589    }
590
591    pub async fn hostnames(&self) -> Vec<String> {
592        let inner = self.inner.read().await;
593        let mut seen = std::collections::HashSet::new();
594        inner
595            .entries
596            .iter()
597            .filter_map(|e| {
598                url::Url::parse(&e.request.url).ok().and_then(|u| {
599                    let host = u.host_str()?.to_string();
600                    seen.insert(host.clone()).then_some(host)
601                })
602            })
603            .collect()
604    }
605
606    pub async fn distinct_methods(&self) -> Vec<String> {
607        let inner = self.inner.read().await;
608        let mut seen = std::collections::HashSet::new();
609        let mut out: Vec<String> = inner
610            .entries
611            .iter()
612            .filter_map(|e| {
613                seen.insert(e.request.method.clone())
614                    .then_some(e.request.method.clone())
615            })
616            .collect();
617        out.sort_unstable();
618        out.dedup();
619        out
620    }
621
622    pub async fn distinct_statuses(&self) -> Vec<u16> {
623        let inner = self.inner.read().await;
624        let mut seen = std::collections::HashSet::new();
625        let mut out: Vec<u16> = inner
626            .entries
627            .iter()
628            .filter_map(|e| {
629                let status = e.status()?;
630                seen.insert(status).then_some(status)
631            })
632            .collect();
633        out.sort_unstable();
634        out
635    }
636
637    pub async fn total_bytes_in(&self) -> u64 {
638        let inner = self.inner.read().await;
639        inner
640            .entries
641            .iter()
642            .filter_map(|e| e.response.as_ref().and_then(|r| r.body_size))
643            .sum()
644    }
645
646    pub async fn total_bytes_out(&self) -> u64 {
647        let inner = self.inner.read().await;
648        inner
649            .entries
650            .iter()
651            .map(|e| {
652                e.request
653                    .post_data
654                    .as_ref()
655                    .map(|b| b.len() as u64)
656                    .unwrap_or(0)
657            })
658            .sum()
659    }
660
661    pub async fn len(&self) -> usize {
662        self.inner.read().await.entries.len()
663    }
664
665    pub async fn is_empty(&self) -> bool {
666        self.len().await == 0
667    }
668
669    pub async fn clear(&self) {
670        let mut inner = self.inner.write().await;
671        inner.entries.clear();
672        inner.by_id.clear();
673    }
674
675    pub async fn metrics(&self) -> NetworkMetrics {
676        self.inner.read().await.metrics.clone()
677    }
678
679    pub async fn contains_id(&self, id: &str) -> bool {
680        self.inner.read().await.by_id.contains_key(id)
681    }
682
683    pub async fn remove_by_id(&self, id: &str) -> Option<Arc<NetworkEntry>> {
684        let mut inner = self.inner.write().await;
685        let idx = inner.by_id.remove(id)?;
686        let entry = inner.entries.remove(idx);
687        // Rebuild indices for entries after the removed one
688        let updates: Vec<(String, usize)> = inner
689            .entries
690            .iter()
691            .enumerate()
692            .skip(idx)
693            .map(|(i, e)| (e.request.id.clone(), i))
694            .collect();
695        for (id, i) in updates {
696            inner.by_id.insert(id, i);
697        }
698        Some(entry)
699    }
700
701    /// Return all captured request IDs in chronological order.
702    pub async fn request_ids(&self) -> Vec<String> {
703        let inner = self.inner.read().await;
704        inner.entries.iter().map(|e| e.request.id.clone()).collect()
705    }
706
707    /// Return an approximate memory footprint in bytes.
708    /// Useful for monitoring and backpressure decisions.
709    pub async fn memory_estimate(&self) -> usize {
710        let inner = self.inner.read().await;
711        let mut estimate = 0;
712        estimate += inner.entries.capacity() * std::mem::size_of::<Arc<NetworkEntry>>();
713        estimate +=
714            inner.by_id.capacity() * (std::mem::size_of::<String>() + std::mem::size_of::<usize>());
715        estimate += inner.pending_responses.capacity()
716            * (std::mem::size_of::<String>() + std::mem::size_of::<CapturedResponse>());
717        estimate += inner.pending_errors.capacity()
718            * (std::mem::size_of::<String>() + std::mem::size_of::<CapturedError>());
719        for e in &inner.entries {
720            estimate += e.request.url.len();
721            estimate += e.request.method.len();
722            estimate += e
723                .request
724                .headers
725                .iter()
726                .map(|h| h.name.len() + h.value.len())
727                .sum::<usize>();
728            if let Some(ref body) = e.request.post_data {
729                estimate += body.len();
730            }
731            if let Some(ref r) = e.response {
732                estimate +=
733                    r.url.len() + r.status_text.len() + r.mime_type.len() + r.protocol.len();
734                estimate += r
735                    .headers
736                    .iter()
737                    .map(|h| h.name.len() + h.value.len())
738                    .sum::<usize>();
739            }
740            if let Some(ref err) = e.error {
741                estimate += err.url.len() + err.error_text.len();
742            }
743        }
744        estimate
745    }
746
747    /// Return whether the entry with `id` has a response.
748    pub async fn has_response(&self, id: &str) -> bool {
749        let inner = self.inner.read().await;
750        inner
751            .by_id
752            .get(id)
753            .map(|&idx| inner.entries[idx].response.is_some())
754            .unwrap_or(false)
755    }
756
757    /// Return whether the entry with `id` has an error.
758    pub async fn has_error(&self, id: &str) -> bool {
759        let inner = self.inner.read().await;
760        inner
761            .by_id
762            .get(id)
763            .map(|&idx| inner.entries[idx].error.is_some())
764            .unwrap_or(false)
765    }
766
767    /// Return the number of out-of-order responses/errors stashed in
768    /// pending maps (orphaned entries waiting for their request).
769    pub async fn pending_count(&self) -> usize {
770        let inner = self.inner.read().await;
771        inner.pending_responses.len() + inner.pending_errors.len()
772    }
773
774    /// Drain all orphaned pending responses and errors, returning them.
775    /// Useful for preventing unbounded growth when requests never arrive
776    /// (e.g. page navigation interrupts the request lifecycle).
777    pub async fn drain_pending(&self) -> (Vec<CapturedResponse>, Vec<CapturedError>) {
778        let mut inner = self.inner.write().await;
779        let responses: Vec<CapturedResponse> =
780            inner.pending_responses.drain().map(|(_, v)| v).collect();
781        let errors: Vec<CapturedError> = inner.pending_errors.drain().map(|(_, v)| v).collect();
782        (responses, errors)
783    }
784
785    /// Retain only entries that satisfy the predicate.
786    /// More efficient than repeated `remove_by_id` for bulk filtering.
787    pub async fn retain<F>(&self, mut f: F)
788    where
789        F: FnMut(&NetworkEntry) -> bool,
790    {
791        let mut inner = self.inner.write().await;
792        let mut new_entries = Vec::new();
793        let mut new_by_id = HashMap::new();
794        for entry in inner.entries.drain(..) {
795            if f(&entry) {
796                let idx = new_entries.len();
797                new_by_id.insert(entry.request.id.clone(), idx);
798                new_entries.push(entry);
799            }
800        }
801        inner.entries = new_entries;
802        inner.by_id = new_by_id;
803    }
804
805    /// Wait up to `timeout` for an entry whose request URL contains `substring`.
806    pub async fn wait_for_url(
807        &self,
808        substring: &str,
809        timeout: std::time::Duration,
810    ) -> Option<Arc<NetworkEntry>> {
811        let mut rx = self.subscribe().await;
812        let deadline = tokio::time::Instant::now() + timeout;
813
814        // Check existing entries first
815        if let Some(e) = self.find_by_url(substring).await {
816            return Some(e);
817        }
818
819        loop {
820            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
821            match tokio::time::timeout(remaining, rx.recv()).await {
822                Ok(Ok(entry)) => {
823                    if entry.request.url.contains(substring) {
824                        return Some(entry);
825                    }
826                }
827                _ => return None,
828            }
829        }
830    }
831
832    /// Wait up to `timeout` for a response to the given request id.
833    pub async fn wait_for_response(
834        &self,
835        id: &str,
836        timeout: std::time::Duration,
837    ) -> Option<Arc<NetworkEntry>> {
838        let mut rx = self.subscribe().await;
839        let deadline = tokio::time::Instant::now() + timeout;
840
841        // Check existing entries first
842        {
843            let inner = self.inner.read().await;
844            if let Some(idx) = inner.by_id.get(id) {
845                if inner.entries[*idx].has_response() || inner.entries[*idx].is_error() {
846                    return Some(inner.entries[*idx].clone());
847                }
848            }
849        }
850
851        loop {
852            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
853            match tokio::time::timeout(remaining, rx.recv()).await {
854                Ok(Ok(entry)) => {
855                    if entry.request.id == id && (entry.has_response() || entry.is_error()) {
856                        return Some(entry);
857                    }
858                }
859                _ => return None,
860            }
861        }
862    }
863
864    // ------------------------------------------------------------------
865    // Convenience queries
866    // ------------------------------------------------------------------
867
868    /// Return all entries with the given HTTP status code.
869    pub async fn find_by_status(&self, status: u16) -> Vec<Arc<NetworkEntry>> {
870        let inner = self.inner.read().await;
871        inner
872            .entries
873            .iter()
874            .filter(|e| e.status() == Some(status))
875            .cloned()
876            .collect()
877    }
878
879    /// Return entries whose request timestamp is >= the given value.
880    pub async fn entries_since(&self, timestamp: u64) -> Vec<Arc<NetworkEntry>> {
881        let inner = self.inner.read().await;
882        inner
883            .entries
884            .iter()
885            .filter(|e| e.request.timestamp >= timestamp)
886            .cloned()
887            .collect()
888    }
889
890    /// Return the most recent `n` entries.
891    pub async fn last_n(&self, n: usize) -> Vec<Arc<NetworkEntry>> {
892        let inner = self.inner.read().await;
893        inner
894            .entries
895            .iter()
896            .rev()
897            .take(n)
898            .cloned()
899            .collect::<Vec<_>>()
900            .into_iter()
901            .rev()
902            .collect()
903    }
904
905    /// Return a deduplicated list of every captured request URL.
906    pub async fn unique_urls(&self) -> Vec<String> {
907        let inner = self.inner.read().await;
908        let mut seen = std::collections::HashSet::new();
909        inner
910            .entries
911            .iter()
912            .filter_map(|e| {
913                if seen.insert(e.request.url.clone()) {
914                    Some(e.request.url.clone())
915                } else {
916                    None
917                }
918            })
919            .collect()
920    }
921
922    // ------------------------------------------------------------------
923    // Export
924    // ------------------------------------------------------------------
925
926    pub async fn save_to_json(&self, path: &std::path::Path) -> Result<()> {
927        let entries: Vec<NetworkEntry> =
928            self.entries().await.iter().map(|e| (**e).clone()).collect();
929        let json = serde_json::to_string_pretty(&entries)?;
930        tokio::fs::write(path, json).await?;
931        Ok(())
932    }
933
934    /// Write a HAR 1.2 file.
935    pub async fn save_as_har(
936        &self,
937        path: &std::path::Path,
938        page_title: Option<&str>,
939    ) -> Result<()> {
940        use chrono::Utc;
941
942        let entries = self.entries().await;
943        let started = if let Some(e) = entries.first() {
944            chrono::DateTime::from_timestamp_millis(e.request.timestamp as i64)
945                .unwrap_or_else(Utc::now)
946        } else {
947            Utc::now()
948        };
949
950        let mut har_entries = Vec::new();
951        for e in &entries {
952            let req = &e.request;
953            let resp = e.response.as_ref();
954            let timing = &req.timing;
955
956            let query_string: Vec<_> = match req.query_params() {
957                Ok(params) => params
958                    .into_iter()
959                    .map(|(name, value)| serde_json::json!({"name": name, "value": value}))
960                    .collect(),
961                Err(error) => {
962                    tracing::warn!(
963                        "failed to parse request URL for query parameters: {error}; url={}",
964                        req.url
965                    );
966                    Vec::new()
967                }
968            };
969
970            let request_json = serde_json::json!({
971                "method": req.method,
972                "url": req.url,
973                "httpVersion": "HTTP/1.1",
974                "headers": req.headers.iter().map(|h| serde_json::json!({"name": h.name, "value": h.value})).collect::<Vec<_>>(),
975                "cookies": req.cookies.iter().map(|c| serde_json::json!({"name": c.name, "value": c.value, "domain": c.domain, "path": c.path})).collect::<Vec<_>>(),
976                "queryString": query_string,
977                "headersSize": -1,
978                "bodySize": req.post_data.as_ref().map(|b| b.len() as i64).unwrap_or(-1),
979                "postData": req.post_data.as_ref().map(|b| {
980                    let mime = req.request_header("content-type").unwrap_or("application/octet-stream");
981                    serde_json::json!({"mimeType": mime, "text": b})
982                }),
983            });
984
985            let response_json = resp.map(|r| serde_json::json!({
986                "status": r.status,
987                "statusText": r.status_text,
988                "httpVersion": "HTTP/1.1",
989                "headers": r.headers.iter().map(|h| serde_json::json!({"name": h.name, "value": h.value})).collect::<Vec<_>>(),
990                "cookies": [],
991                "content": {
992                    "size": r.body_size.unwrap_or(0),
993                    "mimeType": r.mime_type,
994                },
995                "redirectURL": "",
996                "headersSize": -1,
997                "bodySize": r.body_size.map(|b| b as i64).unwrap_or(-1),
998            }));
999
1000            let timings_json = serde_json::json!({
1001                "blocked": -1,
1002                "dns": option_f64_ms(timing.dns_end_ms, timing.dns_start_ms),
1003                "connect": option_f64_ms(timing.connect_end_ms, timing.connect_start_ms),
1004                "ssl": option_f64_ms(timing.connect_end_ms, timing.tls_start_ms),
1005                "send": -1,
1006                "wait": option_f64_ms(timing.response_start_ms, timing.connect_end_ms),
1007                "receive": option_f64_ms(timing.response_end_ms, timing.response_start_ms),
1008            });
1009
1010            har_entries.push(serde_json::json!({
1011                "startedDateTime": format!("{}", chrono::DateTime::from_timestamp_millis(req.timestamp as i64).unwrap_or_else(Utc::now)),
1012                "time": 0,
1013                "request": request_json,
1014                "response": response_json,
1015                "cache": {},
1016                "timings": timings_json,
1017            }));
1018        }
1019
1020        let har = serde_json::json!({
1021            "log": {
1022                "version": "1.2",
1023                "creator": {
1024                    "name": "runtime_foxdriver",
1025                    "version": env!("CARGO_PKG_VERSION"),
1026                },
1027                "pages": [{
1028                    "startedDateTime": format!("{}", started),
1029                    "id": "page_1",
1030                    "title": page_title.unwrap_or("unknown"),
1031                    "pageTimings": { "onContentLoad": -1, "onLoad": -1 },
1032                }],
1033                "entries": har_entries,
1034            }
1035        });
1036
1037        tokio::fs::write(path, serde_json::to_string_pretty(&har)?).await?;
1038        Ok(())
1039    }
1040
1041    // ------------------------------------------------------------------
1042    // Ingestion helpers (called by Page::start_network_log handler)
1043    // ------------------------------------------------------------------
1044
1045    pub(crate) async fn ingest_before_request_sent(&self, evt: &BeforeRequestSent) {
1046        let mut inner = self.inner.write().await;
1047        inner.metrics.requests_received += 1;
1048
1049        let req = build_request(evt);
1050        let resp = inner.pending_responses.remove(&req.id);
1051        let err = inner.pending_errors.remove(&req.id);
1052
1053        let entry = Arc::new(NetworkEntry {
1054            request: req,
1055            response: resp,
1056            error: err,
1057        });
1058        inner.push_entry(entry);
1059    }
1060
1061    pub(crate) async fn ingest_response_completed(&self, evt: &ResponseCompleted) {
1062        let mut inner = self.inner.write().await;
1063        inner.metrics.responses_received += 1;
1064
1065        let id = evt.params.base_parameters.request.request.inner().clone();
1066        let resp = build_response(evt);
1067
1068        if let Some(idx) = inner.by_id.get(&id).copied() {
1069            let mut new_entry = (*inner.entries[idx]).clone();
1070            if new_entry.response.is_some() {
1071                inner.metrics.duplicate_responses += 1;
1072            }
1073            new_entry.response = Some(resp);
1074            let new_arc = Arc::new(new_entry);
1075            inner.entries[idx] = new_arc.clone();
1076            let _ = inner.tx.send(new_arc);
1077        } else {
1078            while inner.max_pending > 0 && inner.pending_responses.len() >= inner.max_pending {
1079                if let Some(k) = inner.pending_responses.keys().next().cloned() {
1080                    inner.pending_responses.remove(&k);
1081                    inner.metrics.pending_responses_dropped += 1;
1082                }
1083            }
1084            inner.pending_responses.insert(id, resp);
1085        }
1086    }
1087
1088    pub(crate) async fn ingest_fetch_error(&self, evt: &FetchError) {
1089        let mut inner = self.inner.write().await;
1090        inner.metrics.errors_received += 1;
1091
1092        let id = evt.params.base_parameters.request.request.inner().clone();
1093        let err = CapturedError {
1094            id: id.clone(),
1095            // The request's own URL, not `navigation`. `navigation` is the
1096            // top-level document URL and is None for subresource requests
1097            // (scripts, images, XHR), so reading it there produced an EMPTY
1098            // error URL for every failed subresource - the operator could not
1099            // tell which resource failed. `request.url` is always the actual
1100            // requested URL, matching build_request's success-path source.
1101            url: evt.params.base_parameters.request.url.clone(),
1102            error_text: evt.params.error_text.clone(),
1103        };
1104
1105        if let Some(idx) = inner.by_id.get(&id).copied() {
1106            let mut new_entry = (*inner.entries[idx]).clone();
1107            if new_entry.error.is_some() {
1108                inner.metrics.duplicate_errors += 1;
1109            }
1110            new_entry.error = Some(err);
1111            let new_arc = Arc::new(new_entry);
1112            inner.entries[idx] = new_arc.clone();
1113            let _ = inner.tx.send(new_arc);
1114        } else {
1115            while inner.max_pending > 0 && inner.pending_errors.len() >= inner.max_pending {
1116                if let Some(k) = inner.pending_errors.keys().next().cloned() {
1117                    inner.pending_errors.remove(&k);
1118                    inner.metrics.pending_errors_dropped += 1;
1119                }
1120            }
1121            inner.pending_errors.insert(id, err);
1122        }
1123    }
1124}
1125
1126impl Default for NetworkLog {
1127    fn default() -> Self {
1128        Self::new()
1129    }
1130}
1131
1132fn option_f64_ms(end: Option<f64>, start: Option<f64>) -> f64 {
1133    match (end, start) {
1134        (Some(e), Some(s)) => (e - s).max(0.0),
1135        _ => -1.0,
1136    }
1137}
1138
1139// ------------------------------------------------------------------
1140// Build helpers (BiDi events -> our structs)
1141// ------------------------------------------------------------------
1142
1143fn build_request(evt: &BeforeRequestSent) -> CapturedRequest {
1144    let bp = &evt.params.base_parameters;
1145    let id = bp.request.request.inner().clone();
1146    let url = bp.request.url.clone();
1147    let headers = bp
1148        .request
1149        .headers
1150        .iter()
1151        .map(CapturedHeader::from)
1152        .collect();
1153    let post_data = None; // BiDi doesn't expose body in beforeRequestSent
1154    let timestamp = bp.timestamp;
1155    let destination = bp.request.destination.clone();
1156    let initiator_type = evt
1157        .params
1158        .initiator
1159        .as_ref()
1160        .and_then(|i| i.r#type.as_ref().map(|t| format!("{:?}", t).to_lowercase()));
1161    let timing = CapturedTiming::from(&bp.request.timings);
1162    let cookies = bp
1163        .request
1164        .cookies
1165        .iter()
1166        .map(CapturedCookie::from)
1167        .collect();
1168
1169    CapturedRequest {
1170        id,
1171        context: bp.context.as_ref().map(|c| c.inner().to_string()),
1172        method: bp.request.method.clone(),
1173        url,
1174        headers,
1175        post_data,
1176        timestamp,
1177        destination,
1178        initiator_type,
1179        timing,
1180        cookies,
1181    }
1182}
1183
1184fn build_response(evt: &ResponseCompleted) -> CapturedResponse {
1185    let bp = &evt.params.base_parameters;
1186    let resp = &evt.params.response;
1187    let id = bp.request.request.inner().clone();
1188    let url = resp.url.clone();
1189    let headers = resp.headers.iter().map(CapturedHeader::from).collect();
1190    let protocol = resp.protocol.clone();
1191    let status = resp.status as u16;
1192    let status_text = resp.status_text.clone();
1193    let mime_type = resp.mime_type.clone();
1194    let body_size = resp.body_size;
1195    let from_cache = resp.from_cache;
1196
1197    CapturedResponse {
1198        id,
1199        url,
1200        protocol,
1201        status,
1202        status_text,
1203        headers,
1204        mime_type,
1205        body_size,
1206        from_cache,
1207    }
1208}
1209
1210// ------------------------------------------------------------------
1211// Handler factory (used by browser.rs)
1212// ------------------------------------------------------------------
1213
1214pub fn make_network_handler(
1215    log: NetworkLog,
1216) -> impl FnMut(
1217    rustenium_bidi_definitions::Event,
1218) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
1219    use rustenium_bidi_definitions::network::events::NetworkEvent;
1220    move |evt| {
1221        let log = log.clone();
1222        Box::pin(async move {
1223            if let rustenium_bidi_definitions::Event::Network(nev) = evt {
1224                match nev {
1225                    NetworkEvent::BeforeRequestSent(evt) => {
1226                        log.ingest_before_request_sent(&evt).await
1227                    }
1228                    NetworkEvent::ResponseCompleted(evt) => {
1229                        log.ingest_response_completed(&evt).await
1230                    }
1231                    NetworkEvent::FetchError(evt) => log.ingest_fetch_error(&evt).await,
1232                    _ => {}
1233                }
1234            }
1235        })
1236    }
1237}
1238
1239// ------------------------------------------------------------------
1240// Tests
1241// ------------------------------------------------------------------
1242
1243#[cfg(test)]
1244mod tests {
1245    use super::*;
1246
1247    // ---------- helpers ----------
1248
1249    fn make_request(id: &str, method: &str, url: &str) -> CapturedRequest {
1250        CapturedRequest {
1251            id: id.into(),
1252            context: None,
1253            method: method.into(),
1254            url: url.into(),
1255            headers: vec![],
1256            post_data: None,
1257            timestamp: 0,
1258            destination: "document".into(),
1259            initiator_type: None,
1260            timing: CapturedTiming::default(),
1261            cookies: vec![],
1262        }
1263    }
1264
1265    fn make_request_with_headers(
1266        id: &str,
1267        method: &str,
1268        url: &str,
1269        headers: Vec<CapturedHeader>,
1270    ) -> CapturedRequest {
1271        CapturedRequest {
1272            id: id.into(),
1273            context: None,
1274            method: method.into(),
1275            url: url.into(),
1276            headers,
1277            post_data: None,
1278            timestamp: 0,
1279            destination: "document".into(),
1280            initiator_type: None,
1281            timing: CapturedTiming::default(),
1282            cookies: vec![],
1283        }
1284    }
1285
1286    fn make_response(id: &str, status: u16, url: &str) -> CapturedResponse {
1287        CapturedResponse {
1288            id: id.into(),
1289            url: url.into(),
1290            protocol: "h2".into(),
1291            status,
1292            status_text: "OK".into(),
1293            headers: vec![],
1294            mime_type: "application/json".into(),
1295            body_size: Some(100),
1296            from_cache: false,
1297        }
1298    }
1299
1300    fn make_error(id: &str, url: &str, text: &str) -> CapturedError {
1301        CapturedError {
1302            id: id.into(),
1303            url: url.into(),
1304            error_text: text.into(),
1305        }
1306    }
1307
1308    async fn push_request(log: &NetworkLog, req: CapturedRequest) {
1309        let entry = Arc::new(NetworkEntry {
1310            request: req,
1311            response: None,
1312            error: None,
1313        });
1314        let mut inner = log.inner.write().await;
1315        inner.push_entry(entry);
1316    }
1317
1318    async fn push_entry(
1319        log: &NetworkLog,
1320        req: CapturedRequest,
1321        resp: Option<CapturedResponse>,
1322        err: Option<CapturedError>,
1323    ) {
1324        let entry = Arc::new(NetworkEntry {
1325            request: req,
1326            response: resp,
1327            error: err,
1328        });
1329        let mut inner = log.inner.write().await;
1330        inner.push_entry(entry);
1331    }
1332
1333    // ---------- basic lifecycle ----------
1334
1335    #[tokio::test]
1336    async fn test_new_log_is_empty() {
1337        let log = NetworkLog::new();
1338        assert!(log.is_empty().await);
1339        assert_eq!(log.len().await, 0);
1340    }
1341
1342    #[tokio::test]
1343    async fn test_with_limits() {
1344        let log = NetworkLog::with_limits(10, 5);
1345        let m = log.metrics().await;
1346        assert_eq!(m.max_entries, 10);
1347    }
1348
1349    #[tokio::test]
1350    async fn test_clear() {
1351        let log = NetworkLog::new();
1352        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1353        assert_eq!(log.len().await, 1);
1354        log.clear().await;
1355        assert!(log.is_empty().await);
1356    }
1357
1358    #[tokio::test]
1359    async fn test_entries_returns_all() {
1360        let log = NetworkLog::new();
1361        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1362        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1363        let entries = log.entries().await;
1364        assert_eq!(entries.len(), 2);
1365        assert_eq!(entries[0].request.id, "1");
1366        assert_eq!(entries[1].request.id, "2");
1367    }
1368
1369    #[tokio::test]
1370    async fn test_first_last_nth() {
1371        let log = NetworkLog::new();
1372        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1373        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1374        push_request(&log, make_request("3", "PUT", "https://c.com")).await;
1375
1376        assert_eq!(log.first().await.unwrap().request.id, "1");
1377        assert_eq!(log.last().await.unwrap().request.id, "3");
1378        assert_eq!(log.nth(0).await.unwrap().request.id, "1");
1379        assert_eq!(log.nth(1).await.unwrap().request.id, "2");
1380        assert_eq!(log.nth(2).await.unwrap().request.id, "3");
1381        assert!(log.nth(99).await.is_none());
1382    }
1383
1384    // ---------- filtering ----------
1385
1386    #[tokio::test]
1387    async fn test_filter_method() {
1388        let log = NetworkLog::new();
1389        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1390        push_request(&log, make_request("2", "POST", "https://example.com")).await;
1391        let get = log.filter(Filter::new().method("GET")).await;
1392        assert_eq!(get.len(), 1);
1393        assert_eq!(get[0].request.method, "GET");
1394    }
1395
1396    #[tokio::test]
1397    async fn test_filter_status_range() {
1398        let log = NetworkLog::new();
1399        push_entry(
1400            &log,
1401            make_request("1", "GET", "https://a.com"),
1402            Some(make_response("1", 200, "https://a.com")),
1403            None,
1404        )
1405        .await;
1406        push_entry(
1407            &log,
1408            make_request("2", "GET", "https://b.com"),
1409            Some(make_response("2", 404, "https://b.com")),
1410            None,
1411        )
1412        .await;
1413        push_entry(&log, make_request("3", "GET", "https://c.com"), None, None).await;
1414        let f = log.filter(Filter::new().status_range(200..=299)).await;
1415        assert_eq!(f.len(), 1);
1416        assert_eq!(f[0].request.id, "1");
1417    }
1418
1419    #[tokio::test]
1420    async fn test_filter_url_contains() {
1421        let log = NetworkLog::new();
1422        push_request(&log, make_request("1", "GET", "https://api.example.com/v1")).await;
1423        push_request(&log, make_request("2", "GET", "https://other.com")).await;
1424        let f = log.filter(Filter::new().url_contains("api")).await;
1425        assert_eq!(f.len(), 1);
1426        assert_eq!(f[0].request.id, "1");
1427    }
1428
1429    #[tokio::test]
1430    async fn test_filter_url_regex() {
1431        let log = NetworkLog::new();
1432        push_request(&log, make_request("1", "GET", "https://api.example.com/v1")).await;
1433        push_request(&log, make_request("2", "GET", "https://other.com")).await;
1434        let f = log
1435            .filter(Filter::new().url_regex(r"api\.\w+\.com").unwrap())
1436            .await;
1437        assert_eq!(f.len(), 1);
1438    }
1439
1440    #[tokio::test]
1441    async fn test_filter_header() {
1442        let log = NetworkLog::new();
1443        let h1 = vec![CapturedHeader {
1444            name: "Authorization".into(),
1445            value: "Bearer abc".into(),
1446        }];
1447        let h2 = vec![CapturedHeader {
1448            name: "Content-Type".into(),
1449            value: "application/json".into(),
1450        }];
1451        push_request(
1452            &log,
1453            make_request_with_headers("1", "GET", "https://a.com", h1),
1454        )
1455        .await;
1456        push_request(
1457            &log,
1458            make_request_with_headers("2", "GET", "https://b.com", h2),
1459        )
1460        .await;
1461        let f = log
1462            .filter(Filter::new().header("authorization", "bearer"))
1463            .await;
1464        assert_eq!(f.len(), 1);
1465        assert_eq!(f[0].request.id, "1");
1466    }
1467
1468    #[tokio::test]
1469    async fn test_filter_destination() {
1470        let log = NetworkLog::new();
1471        let mut r1 = make_request("1", "GET", "https://a.com");
1472        r1.destination = "image".into();
1473        let mut r2 = make_request("2", "GET", "https://b.com");
1474        r2.destination = "document".into();
1475        push_request(&log, r1).await;
1476        push_request(&log, r2).await;
1477        let f = log.filter(Filter::new().destination("image")).await;
1478        assert_eq!(f.len(), 1);
1479        assert_eq!(f[0].request.id, "1");
1480    }
1481
1482    #[tokio::test]
1483    async fn test_filter_with_response() {
1484        let log = NetworkLog::new();
1485        push_entry(
1486            &log,
1487            make_request("1", "GET", "https://a.com"),
1488            Some(make_response("1", 200, "https://a.com")),
1489            None,
1490        )
1491        .await;
1492        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1493        let f = log.filter(Filter::new().with_response()).await;
1494        assert_eq!(f.len(), 1);
1495        assert_eq!(f[0].request.id, "1");
1496    }
1497
1498    #[tokio::test]
1499    async fn test_filter_without_response() {
1500        let log = NetworkLog::new();
1501        push_entry(
1502            &log,
1503            make_request("1", "GET", "https://a.com"),
1504            Some(make_response("1", 200, "https://a.com")),
1505            None,
1506        )
1507        .await;
1508        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1509        let f = log.filter(Filter::new().without_response()).await;
1510        assert_eq!(f.len(), 1);
1511        assert_eq!(f[0].request.id, "2");
1512    }
1513
1514    #[tokio::test]
1515    async fn test_filter_with_error() {
1516        let log = NetworkLog::new();
1517        push_entry(
1518            &log,
1519            make_request("1", "GET", "https://a.com"),
1520            None,
1521            Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
1522        )
1523        .await;
1524        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1525        let f = log.filter(Filter::new().with_error()).await;
1526        assert_eq!(f.len(), 1);
1527        assert_eq!(f[0].request.id, "1");
1528    }
1529
1530    #[tokio::test]
1531    async fn test_filter_composition() {
1532        let log = NetworkLog::new();
1533        push_entry(
1534            &log,
1535            make_request("1", "POST", "https://api.example.com/login"),
1536            Some(make_response("1", 200, "https://api.example.com/login")),
1537            None,
1538        )
1539        .await;
1540        push_request(
1541            &log,
1542            make_request("2", "GET", "https://api.example.com/login"),
1543        )
1544        .await;
1545        let f = log
1546            .filter(
1547                Filter::new()
1548                    .method("POST")
1549                    .url_contains("login")
1550                    .with_response(),
1551            )
1552            .await;
1553        assert_eq!(f.len(), 1);
1554        assert_eq!(f[0].request.id, "1");
1555    }
1556
1557    #[tokio::test]
1558    async fn test_count() {
1559        let log = NetworkLog::new();
1560        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1561        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1562        assert_eq!(log.count(Filter::new().method("GET")).await, 1);
1563        assert_eq!(log.count(Filter::new()).await, 2);
1564    }
1565
1566    // ---------- completed ----------
1567
1568    #[tokio::test]
1569    async fn test_completed() {
1570        let log = NetworkLog::new();
1571        push_entry(
1572            &log,
1573            make_request("1", "GET", "https://a.com"),
1574            Some(make_response("1", 200, "https://a.com")),
1575            None,
1576        )
1577        .await;
1578        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1579        push_entry(
1580            &log,
1581            make_request("3", "GET", "https://c.com"),
1582            None,
1583            Some(make_error("3", "https://c.com", "err")),
1584        )
1585        .await;
1586        let c = log.completed().await;
1587        assert_eq!(c.len(), 2);
1588    }
1589
1590    // ---------- find_by_url / regex ----------
1591
1592    #[tokio::test]
1593    async fn test_find_by_url() {
1594        let log = NetworkLog::new();
1595        push_request(&log, make_request("1", "GET", "https://api.example.com")).await;
1596        push_request(&log, make_request("2", "GET", "https://other.com")).await;
1597        assert_eq!(log.find_by_url("api").await.unwrap().request.id, "1");
1598        assert!(log.find_by_url("notfound").await.is_none());
1599    }
1600
1601    #[tokio::test]
1602    async fn test_find_by_url_regex() {
1603        let log = NetworkLog::new();
1604        push_request(&log, make_request("1", "GET", "https://api.example.com")).await;
1605        push_request(&log, make_request("2", "GET", "https://other.com")).await;
1606        let re = regex::Regex::new(r"api\.\w+\.com").unwrap();
1607        assert_eq!(log.find_by_url_regex(&re).await.unwrap().request.id, "1");
1608    }
1609
1610    // ---------- aggregation ----------
1611
1612    #[tokio::test]
1613    async fn test_endpoints() {
1614        let log = NetworkLog::new();
1615        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
1616        push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
1617        push_request(&log, make_request("3", "GET", "https://b.com/z")).await;
1618        let ep = log.endpoints().await;
1619        assert_eq!(ep.len(), 3);
1620    }
1621
1622    #[tokio::test]
1623    async fn test_hostnames() {
1624        let log = NetworkLog::new();
1625        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
1626        push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
1627        push_request(&log, make_request("3", "GET", "https://b.com/z")).await;
1628        let h = log.hostnames().await;
1629        assert_eq!(h.len(), 2);
1630        assert!(h.contains(&"a.com".into()));
1631        assert!(h.contains(&"b.com".into()));
1632    }
1633
1634    #[tokio::test]
1635    async fn test_distinct_methods() {
1636        let log = NetworkLog::new();
1637        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1638        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1639        push_request(&log, make_request("3", "GET", "https://c.com")).await;
1640        let m = log.distinct_methods().await;
1641        assert_eq!(m, vec!["GET", "POST"]);
1642    }
1643
1644    #[tokio::test]
1645    async fn test_distinct_statuses() {
1646        let log = NetworkLog::new();
1647        push_entry(
1648            &log,
1649            make_request("1", "GET", "https://a.com"),
1650            Some(make_response("1", 200, "https://a.com")),
1651            None,
1652        )
1653        .await;
1654        push_entry(
1655            &log,
1656            make_request("2", "GET", "https://b.com"),
1657            Some(make_response("2", 404, "https://b.com")),
1658            None,
1659        )
1660        .await;
1661        push_entry(
1662            &log,
1663            make_request("3", "GET", "https://c.com"),
1664            Some(make_response("3", 200, "https://c.com")),
1665            None,
1666        )
1667        .await;
1668        let s = log.distinct_statuses().await;
1669        assert_eq!(s, vec![200, 404]);
1670    }
1671
1672    #[tokio::test]
1673    async fn test_total_bytes() {
1674        let log = NetworkLog::new();
1675        let mut r1 = make_request("1", "POST", "https://a.com");
1676        r1.post_data = Some("hello".into());
1677        push_entry(
1678            &log,
1679            r1,
1680            Some(make_response("1", 200, "https://a.com")),
1681            None,
1682        )
1683        .await;
1684        let mut r2 = make_request("2", "POST", "https://b.com");
1685        r2.post_data = Some("world!!".into());
1686        push_entry(
1687            &log,
1688            r2,
1689            Some(make_response("2", 200, "https://b.com")),
1690            None,
1691        )
1692        .await;
1693        assert_eq!(log.total_bytes_in().await, 200);
1694        assert_eq!(log.total_bytes_out().await, 12); // "hello" + "world!!"
1695    }
1696
1697    // ---------- entry helpers ----------
1698
1699    #[tokio::test]
1700    async fn test_entry_status_and_final_url() {
1701        let entry = NetworkEntry {
1702            request: make_request("1", "GET", "https://a.com"),
1703            response: Some(make_response("1", 301, "https://b.com")),
1704            error: None,
1705        };
1706        assert_eq!(entry.status(), Some(301));
1707        assert_eq!(entry.final_url(), "https://b.com");
1708        assert!(entry.has_response());
1709        assert!(!entry.is_error());
1710    }
1711
1712    #[tokio::test]
1713    async fn test_entry_request_header() {
1714        let req = make_request_with_headers(
1715            "1",
1716            "GET",
1717            "https://a.com",
1718            vec![CapturedHeader {
1719                name: "X-Custom".into(),
1720                value: "123".into(),
1721            }],
1722        );
1723        let entry = NetworkEntry {
1724            request: req,
1725            response: None,
1726            error: None,
1727        };
1728        assert_eq!(entry.request_header("x-custom"), Some("123"));
1729        assert_eq!(entry.request_header("missing"), None);
1730    }
1731
1732    #[tokio::test]
1733    async fn test_entry_response_header() {
1734        let req = make_request("1", "GET", "https://a.com");
1735        let resp = CapturedResponse {
1736            id: "1".into(),
1737            url: "https://a.com".into(),
1738            protocol: "h2".into(),
1739            status: 200,
1740            status_text: "OK".into(),
1741            headers: vec![CapturedHeader {
1742                name: "Content-Type".into(),
1743                value: "application/json".into(),
1744            }],
1745            mime_type: "application/json".into(),
1746            body_size: Some(10),
1747            from_cache: false,
1748        };
1749        let entry = NetworkEntry {
1750            request: req,
1751            response: Some(resp),
1752            error: None,
1753        };
1754        assert_eq!(
1755            entry.response_header("content-type"),
1756            Some("application/json")
1757        );
1758        assert_eq!(entry.response_header("missing"), None);
1759    }
1760
1761    #[tokio::test]
1762    async fn test_entry_to_curl() {
1763        let req = CapturedRequest {
1764            id: "1".into(),
1765            context: None,
1766            method: "POST".into(),
1767            url: "https://api.example.com/login".into(),
1768            headers: vec![
1769                CapturedHeader {
1770                    name: "Content-Type".into(),
1771                    value: "application/json".into(),
1772                },
1773                CapturedHeader {
1774                    name: "Host".into(),
1775                    value: "api.example.com".into(),
1776                },
1777            ],
1778            post_data: Some(r#"{"user":"admin"}"#.into()),
1779            timestamp: 0,
1780            destination: "document".into(),
1781            initiator_type: None,
1782            timing: CapturedTiming::default(),
1783            cookies: vec![],
1784        };
1785        let entry = NetworkEntry {
1786            request: req,
1787            response: None,
1788            error: None,
1789        };
1790        let curl = entry.to_curl();
1791        assert!(curl.starts_with("curl -X POST"));
1792        assert!(curl.contains("-H 'Content-Type: application/json'"));
1793        assert!(!curl.contains("Host")); // stripped
1794        assert!(curl.contains(r#"-d '{"user":"admin"}'"#));
1795        assert!(curl.contains("'https://api.example.com/login'"));
1796    }
1797
1798    // ---------- request helpers ----------
1799
1800    #[tokio::test]
1801    async fn test_request_json_body() {
1802        let mut req = make_request("1", "POST", "https://a.com");
1803        req.post_data = Some(r#"{"key":"value"}"#.into());
1804        assert_eq!(req.json_body(), Some(serde_json::json!({"key": "value"})));
1805    }
1806
1807    #[tokio::test]
1808    async fn test_request_json_body_invalid() {
1809        let mut req = make_request("1", "POST", "https://a.com");
1810        req.post_data = Some("not json".into());
1811        assert!(req.json_body().is_none());
1812    }
1813
1814    #[tokio::test]
1815    async fn test_request_query_params() {
1816        let req = make_request("1", "GET", "https://a.com?foo=bar&baz=qux");
1817        let params = req.query_params().unwrap();
1818        assert_eq!(params.len(), 2);
1819        assert!(params.contains(&("foo".into(), "bar".into())));
1820        assert!(params.contains(&("baz".into(), "qux".into())));
1821    }
1822
1823    #[tokio::test]
1824    async fn test_request_query_params_malformed_url() {
1825        let mut req = make_request("1", "GET", "https://a.com?foo=bar");
1826        req.url = "not a valid url".into();
1827        assert!(
1828            req.query_params().is_err(),
1829            "query_params must report a parse failure, not silently return an empty vec"
1830        );
1831    }
1832
1833    // ---------- secret scanning ----------
1834
1835    // ---------- internal requests ----------
1836
1837    // ---------- max entries eviction ----------
1838
1839    #[tokio::test]
1840    async fn test_max_entries_eviction() {
1841        let log = NetworkLog::with_limits(4, 10);
1842        for i in 0..6 {
1843            push_request(
1844                &log,
1845                make_request(&format!("{}", i), "GET", "https://example.com"),
1846            )
1847            .await;
1848        }
1849        // After 6 inserts with max 4, eviction triggers when len >= 4
1850        // First eviction at insert 4: remove 2, leaving [2,3]
1851        // Then insert 4: [2,3,4]
1852        // Insert 5: [2,3,4,5]
1853        let entries = log.entries().await;
1854        let ids: Vec<_> = entries.iter().map(|e| e.request.id.clone()).collect();
1855        assert_eq!(ids, vec!["2", "3", "4", "5"]);
1856
1857        let m = log.metrics().await;
1858        assert_eq!(m.entries_evicted, 2);
1859    }
1860
1861    // ---------- broadcast ----------
1862
1863    #[tokio::test]
1864    async fn test_broadcast_receives_entries() {
1865        let log = NetworkLog::new();
1866        let mut rx = log.subscribe().await;
1867        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1868        let received = rx.recv().await.unwrap();
1869        assert_eq!(received.request.id, "1");
1870    }
1871
1872    #[tokio::test]
1873    async fn test_broadcast_multiple_receivers() {
1874        let log = NetworkLog::new();
1875        let mut rx1 = log.subscribe().await;
1876        let mut rx2 = log.subscribe().await;
1877        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1878        assert_eq!(rx1.recv().await.unwrap().request.id, "1");
1879        assert_eq!(rx2.recv().await.unwrap().request.id, "1");
1880    }
1881
1882    // ---------- metrics ----------
1883
1884    #[tokio::test]
1885    async fn test_metrics_requests_received() {
1886        let log = NetworkLog::new();
1887        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1888        push_request(&log, make_request("2", "GET", "https://b.com")).await;
1889        let m = log.metrics().await;
1890        assert_eq!(m.requests_received, 0); // push_request bypasses metrics
1891        assert_eq!(m.max_entries, 50_000);
1892    }
1893
1894    // ---------- serialization ----------
1895
1896    #[tokio::test]
1897    async fn test_network_entry_serialize_roundtrip() {
1898        let entry = NetworkEntry {
1899            request: make_request("1", "GET", "https://example.com"),
1900            response: Some(make_response("1", 200, "https://example.com")),
1901            error: None,
1902        };
1903        let json = serde_json::to_string(&entry).unwrap();
1904        let de: NetworkEntry = serde_json::from_str(&json).unwrap();
1905        assert_eq!(de.request.id, entry.request.id);
1906        assert_eq!(de.status(), Some(200));
1907    }
1908
1909    // ---------- clone semantics ----------
1910
1911    #[tokio::test]
1912    async fn test_log_clone_shares_state() {
1913        let log1 = NetworkLog::new();
1914        let log2 = log1.clone();
1915        push_request(&log1, make_request("1", "GET", "https://example.com")).await;
1916        assert_eq!(log2.len().await, 1);
1917    }
1918
1919    // ---------- timing helpers ----------
1920
1921    #[tokio::test]
1922    async fn test_non_neg() {
1923        assert_eq!(non_neg(5.0), Some(5.0));
1924        assert_eq!(non_neg(-1.0), None);
1925        assert_eq!(non_neg(0.0), Some(0.0));
1926    }
1927
1928    #[tokio::test]
1929    async fn test_option_f64_ms() {
1930        assert_eq!(option_f64_ms(Some(10.0), Some(3.0)), 7.0);
1931        assert_eq!(option_f64_ms(Some(3.0), Some(10.0)), 0.0); // clamped
1932        assert_eq!(option_f64_ms(None, Some(3.0)), -1.0);
1933    }
1934
1935    // ---------- edge cases ----------
1936
1937    #[tokio::test]
1938    async fn test_empty_filter_matches_all() {
1939        let log = NetworkLog::new();
1940        push_request(&log, make_request("1", "GET", "https://a.com")).await;
1941        push_request(&log, make_request("2", "POST", "https://b.com")).await;
1942        assert_eq!(log.filter(Filter::new()).await.len(), 2);
1943        assert_eq!(log.count(Filter::new()).await, 2);
1944    }
1945
1946    #[tokio::test]
1947    async fn test_eviction_rebuilds_by_id() {
1948        let log = NetworkLog::with_limits(4, 10);
1949        for i in 0..6 {
1950            push_request(
1951                &log,
1952                make_request(&format!("{}", i), "GET", "https://example.com"),
1953            )
1954            .await;
1955        }
1956        // After eviction, by_id should still be valid
1957        let entries = log.entries().await;
1958        assert_eq!(entries.len(), 4);
1959        for e in &entries {
1960            assert!(log.find_by_url(&e.request.url).await.is_some());
1961        }
1962    }
1963
1964    #[tokio::test]
1965    async fn test_broadcast_dropped_receiver_does_not_panic() {
1966        let log = NetworkLog::new();
1967        {
1968            let _rx = log.subscribe().await;
1969        } // receiver dropped
1970        push_request(&log, make_request("1", "GET", "https://example.com")).await;
1971        // should not panic
1972    }
1973
1974    #[tokio::test]
1975    async fn test_save_to_json_empty() {
1976        let log = NetworkLog::new();
1977        let path = std::path::Path::new("/tmp/foxdriver_network_empty.json");
1978        log.save_to_json(path).await.unwrap();
1979        let content = std::fs::read_to_string(path).unwrap();
1980        assert_eq!(content.trim(), "[]");
1981    }
1982
1983    #[tokio::test]
1984    async fn test_save_as_har_empty() {
1985        let log = NetworkLog::new();
1986        let path = std::path::Path::new("/tmp/foxdriver_network_empty.har");
1987        log.save_as_har(path, Some("test")).await.unwrap();
1988        let content = std::fs::read_to_string(path).unwrap();
1989        assert!(content.contains("1.2"));
1990        assert!(content.contains("runtime_foxdriver"));
1991    }
1992
1993    #[tokio::test]
1994    async fn test_captured_timing_default() {
1995        let t = CapturedTiming::default();
1996        assert!(t.dns_start_ms.is_none());
1997        assert!(t.response_end_ms.is_none());
1998    }
1999
2000    #[tokio::test]
2001    async fn test_network_entry_no_response_final_url() {
2002        let entry = NetworkEntry {
2003            request: make_request("1", "GET", "https://a.com"),
2004            response: None,
2005            error: None,
2006        };
2007        assert_eq!(entry.final_url(), "https://a.com");
2008        assert_eq!(entry.status(), None);
2009        assert!(!entry.has_response());
2010        assert!(!entry.is_error());
2011    }
2012
2013    #[tokio::test]
2014    async fn test_distinct_methods_empty() {
2015        let log = NetworkLog::new();
2016        assert!(log.distinct_methods().await.is_empty());
2017    }
2018
2019    #[tokio::test]
2020    async fn test_distinct_statuses_empty() {
2021        let log = NetworkLog::new();
2022        assert!(log.distinct_statuses().await.is_empty());
2023    }
2024
2025    #[tokio::test]
2026    async fn test_hostnames_malformed_url() {
2027        let log = NetworkLog::new();
2028        push_request(&log, make_request("1", "GET", "not-a-url")).await;
2029        assert!(log.hostnames().await.is_empty());
2030    }
2031
2032    #[tokio::test]
2033    async fn test_total_bytes_no_response() {
2034        let log = NetworkLog::new();
2035        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2036        assert_eq!(log.total_bytes_in().await, 0);
2037    }
2038
2039    // ---------- contains_id / remove_by_id ----------
2040
2041    #[tokio::test]
2042    async fn test_contains_id() {
2043        let log = NetworkLog::new();
2044        push_request(&log, make_request("abc", "GET", "https://a.com")).await;
2045        assert!(log.contains_id("abc").await);
2046        assert!(!log.contains_id("xyz").await);
2047    }
2048
2049    #[tokio::test]
2050    async fn test_remove_by_id() {
2051        let log = NetworkLog::new();
2052        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2053        push_request(&log, make_request("2", "GET", "https://b.com")).await;
2054        push_request(&log, make_request("3", "GET", "https://c.com")).await;
2055
2056        let removed = log.remove_by_id("2").await;
2057        assert_eq!(removed.unwrap().request.id, "2");
2058        assert_eq!(log.len().await, 2);
2059        assert!(!log.contains_id("2").await);
2060
2061        // Verify remaining indices are valid
2062        assert!(log.contains_id("1").await);
2063        assert!(log.contains_id("3").await);
2064        assert_eq!(log.nth(0).await.unwrap().request.id, "1");
2065        assert_eq!(log.nth(1).await.unwrap().request.id, "3");
2066    }
2067
2068    #[tokio::test]
2069    async fn test_remove_by_id_unknown() {
2070        let log = NetworkLog::new();
2071        assert!(log.remove_by_id("nope").await.is_none());
2072    }
2073
2074    // ---------- wait_for helpers ----------
2075
2076    #[tokio::test]
2077    async fn test_wait_for_url_existing() {
2078        let log = NetworkLog::new();
2079        push_request(&log, make_request("1", "GET", "https://target.com/page")).await;
2080        let found = log
2081            .wait_for_url("target.com", std::time::Duration::from_secs(1))
2082            .await;
2083        assert_eq!(found.unwrap().request.id, "1");
2084    }
2085
2086    #[tokio::test]
2087    async fn test_wait_for_url_future() {
2088        let log = NetworkLog::new();
2089        let log2 = log.clone();
2090        tokio::spawn(async move {
2091            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2092            push_request(&log2, make_request("1", "GET", "https://target.com/page")).await;
2093        });
2094        let found = log
2095            .wait_for_url("target.com", std::time::Duration::from_secs(1))
2096            .await;
2097        assert_eq!(found.unwrap().request.id, "1");
2098    }
2099
2100    #[tokio::test]
2101    async fn test_wait_for_url_timeout() {
2102        let log = NetworkLog::new();
2103        let found = log
2104            .wait_for_url("never", std::time::Duration::from_millis(50))
2105            .await;
2106        assert!(found.is_none());
2107    }
2108
2109    #[tokio::test]
2110    async fn test_wait_for_response_existing() {
2111        let log = NetworkLog::new();
2112        push_entry(
2113            &log,
2114            make_request("1", "GET", "https://a.com"),
2115            Some(make_response("1", 200, "https://a.com")),
2116            None,
2117        )
2118        .await;
2119        let found = log
2120            .wait_for_response("1", std::time::Duration::from_secs(1))
2121            .await;
2122        assert_eq!(found.unwrap().request.id, "1");
2123    }
2124
2125    #[tokio::test]
2126    async fn test_wait_for_response_future() {
2127        let log = NetworkLog::new();
2128        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2129        let log2 = log.clone();
2130        tokio::spawn(async move {
2131            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2132            // Update with response
2133            let mut inner = log2.inner.write().await;
2134            if let Some(idx) = inner.by_id.get("1").copied() {
2135                let mut new_entry = (*inner.entries[idx]).clone();
2136                new_entry.response = Some(make_response("1", 200, "https://a.com"));
2137                let new_arc = Arc::new(new_entry);
2138                inner.entries[idx] = new_arc.clone();
2139                let _ = inner.tx.send(new_arc);
2140            }
2141        });
2142        let found = log
2143            .wait_for_response("1", std::time::Duration::from_secs(1))
2144            .await;
2145        assert_eq!(found.unwrap().request.id, "1");
2146    }
2147
2148    #[tokio::test]
2149    async fn test_wait_for_response_timeout() {
2150        let log = NetworkLog::new();
2151        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2152        let found = log
2153            .wait_for_response("1", std::time::Duration::from_millis(50))
2154            .await;
2155        assert!(found.is_none());
2156    }
2157
2158    // ---------- concurrency stress ----------
2159
2160    #[tokio::test]
2161    async fn test_concurrent_pushes() {
2162        let log = NetworkLog::new();
2163        let mut handles = Vec::new();
2164        for t in 0..10 {
2165            let log = log.clone();
2166            handles.push(tokio::spawn(async move {
2167                for i in 0..100 {
2168                    push_request(
2169                        &log,
2170                        make_request(&format!("{}-{}", t, i), "GET", "https://example.com"),
2171                    )
2172                    .await;
2173                }
2174            }));
2175        }
2176        for h in handles {
2177            h.await.unwrap();
2178        }
2179        assert_eq!(log.len().await, 1000);
2180    }
2181
2182    #[tokio::test]
2183    async fn test_concurrent_read_while_write() {
2184        let log = NetworkLog::new();
2185        let log2 = log.clone();
2186
2187        let writer = tokio::spawn(async move {
2188            for i in 0..500 {
2189                push_request(
2190                    &log,
2191                    make_request(&format!("{}", i), "GET", "https://example.com"),
2192                )
2193                .await;
2194                if i % 50 == 0 {
2195                    tokio::task::yield_now().await;
2196                }
2197            }
2198        });
2199
2200        let reader = tokio::spawn(async move {
2201            let mut last_len = 0;
2202            for _ in 0..50 {
2203                tokio::time::sleep(std::time::Duration::from_millis(1)).await;
2204                let len = log2.len().await;
2205                assert!(len >= last_len); // never decreases
2206                last_len = len;
2207            }
2208        });
2209
2210        let (r1, r2) = tokio::join!(writer, reader);
2211        r1.unwrap();
2212        r2.unwrap();
2213    }
2214
2215    // ---------- export with real data ----------
2216
2217    #[tokio::test]
2218    async fn test_save_to_json_roundtrip() {
2219        let log = NetworkLog::new();
2220        push_entry(
2221            &log,
2222            make_request("1", "GET", "https://example.com"),
2223            Some(make_response("1", 200, "https://example.com")),
2224            None,
2225        )
2226        .await;
2227        let path = std::path::Path::new("/tmp/foxdriver_network_real.json");
2228        log.save_to_json(path).await.unwrap();
2229        let content = std::fs::read_to_string(path).unwrap();
2230        let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
2231        assert_eq!(parsed.len(), 1);
2232        assert_eq!(parsed[0].request.id, "1");
2233        assert_eq!(parsed[0].status(), Some(200));
2234    }
2235
2236    #[tokio::test]
2237    async fn test_save_as_har_with_data() {
2238        let log = NetworkLog::new();
2239        let mut req = make_request("1", "POST", "https://api.example.com/data?key=val");
2240        req.post_data = Some(r#"{"hello":"world"}"#.into());
2241        req.headers = vec![CapturedHeader {
2242            name: "Content-Type".into(),
2243            value: "application/json".into(),
2244        }];
2245        push_entry(
2246            &log,
2247            req,
2248            Some(make_response(
2249                "1",
2250                201,
2251                "https://api.example.com/data?key=val",
2252            )),
2253            None,
2254        )
2255        .await;
2256
2257        let path = std::path::Path::new("/tmp/foxdriver_network_real.har");
2258        log.save_as_har(path, Some("test page")).await.unwrap();
2259        let content = std::fs::read_to_string(path).unwrap();
2260        assert!(content.contains("1.2"));
2261        assert!(content.contains("POST"));
2262        assert!(content.contains("201"));
2263        assert!(content.contains("key=val"));
2264        assert!(content.contains("hello"));
2265        assert!(content.contains("test page"));
2266    }
2267
2268    // ---------- curl edge cases ----------
2269
2270    #[tokio::test]
2271    async fn test_to_curl_no_headers_no_body() {
2272        let req = make_request("1", "GET", "https://example.com");
2273        let entry = NetworkEntry {
2274            request: req,
2275            response: None,
2276            error: None,
2277        };
2278        let curl = entry.to_curl();
2279        assert_eq!(curl, "curl -X GET 'https://example.com'");
2280    }
2281
2282    #[tokio::test]
2283    async fn test_to_curl_special_chars() {
2284        let req = CapturedRequest {
2285            id: "1".into(),
2286            context: None,
2287            method: "POST".into(),
2288            url: "https://example.com?a=1&b=2".into(),
2289            headers: vec![CapturedHeader {
2290                name: "X-Special".into(),
2291                value: "val'ue\".txt".into(),
2292            }],
2293            post_data: Some("data='quoted'".into()),
2294            timestamp: 0,
2295            destination: "document".into(),
2296            initiator_type: None,
2297            timing: CapturedTiming::default(),
2298            cookies: vec![],
2299        };
2300        let entry = NetworkEntry {
2301            request: req,
2302            response: None,
2303            error: None,
2304        };
2305        let curl = entry.to_curl();
2306        assert!(curl.contains("-H 'X-Special: val'\\''ue\".txt'"));
2307        assert!(curl.contains("-d 'data='\\''quoted'\\'''"));
2308    }
2309
2310    // ---------- false positive tests ----------
2311
2312    // ---------- filter boundary tests ----------
2313
2314    #[tokio::test]
2315    async fn test_filter_empty_method_matches_all() {
2316        let log = NetworkLog::new();
2317        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2318        push_request(&log, make_request("2", "POST", "https://b.com")).await;
2319        // Filter::new() without method() should match all
2320        assert_eq!(log.filter(Filter::new()).await.len(), 2);
2321    }
2322
2323    #[tokio::test]
2324    async fn test_filter_status_range_no_matches() {
2325        let log = NetworkLog::new();
2326        push_entry(
2327            &log,
2328            make_request("1", "GET", "https://a.com"),
2329            Some(make_response("1", 200, "https://a.com")),
2330            None,
2331        )
2332        .await;
2333        let f = log.filter(Filter::new().status_range(500..=599)).await;
2334        assert!(f.is_empty());
2335    }
2336
2337    #[tokio::test]
2338    async fn test_filter_url_contains_case_insensitive() {
2339        let log = NetworkLog::new();
2340        push_request(&log, make_request("1", "GET", "https://API.example.com")).await;
2341        let f = log.filter(Filter::new().url_contains("api")).await;
2342        assert_eq!(f.len(), 1);
2343    }
2344
2345    #[tokio::test]
2346    async fn test_filter_destination_case_insensitive() {
2347        let log = NetworkLog::new();
2348        let mut r = make_request("1", "GET", "https://a.com");
2349        r.destination = "IMAGE".into();
2350        push_request(&log, r).await;
2351        let f = log.filter(Filter::new().destination("image")).await;
2352        assert_eq!(f.len(), 1);
2353    }
2354
2355    // ---------- query params edge cases ----------
2356
2357    #[tokio::test]
2358    async fn test_query_params_empty() {
2359        let req = make_request("1", "GET", "https://a.com");
2360        assert!(req.query_params().unwrap().is_empty());
2361    }
2362
2363    #[tokio::test]
2364    async fn test_query_params_url_encoded() {
2365        let req = make_request("1", "GET", "https://a.com?foo=%20bar&baz=qux");
2366        let params = req.query_params().unwrap();
2367        assert!(params.contains(&("foo".into(), " bar".into())));
2368        assert!(params.contains(&("baz".into(), "qux".into())));
2369    }
2370
2371    // ---------- multiple evictions ----------
2372
2373    #[tokio::test]
2374    async fn test_multiple_evictions() {
2375        let log = NetworkLog::with_limits(4, 10);
2376        for i in 0..20 {
2377            push_request(
2378                &log,
2379                make_request(&format!("{}", i), "GET", "https://example.com"),
2380            )
2381            .await;
2382        }
2383        let entries = log.entries().await;
2384        assert_eq!(entries.len(), 4);
2385        let ids: Vec<_> = entries.iter().map(|e| e.request.id.clone()).collect();
2386        assert_eq!(ids, vec!["16", "17", "18", "19"]);
2387
2388        let m = log.metrics().await;
2389        assert_eq!(m.entries_evicted, 16);
2390    }
2391
2392    // ---------- header edge cases ----------
2393
2394    #[tokio::test]
2395    async fn test_request_header_multiple_same_name() {
2396        let req = make_request_with_headers(
2397            "1",
2398            "GET",
2399            "https://a.com",
2400            vec![
2401                CapturedHeader {
2402                    name: "X-Dup".into(),
2403                    value: "first".into(),
2404                },
2405                CapturedHeader {
2406                    name: "X-Dup".into(),
2407                    value: "second".into(),
2408                },
2409            ],
2410        );
2411        assert_eq!(req.request_header("x-dup"), Some("first")); // first match wins
2412    }
2413
2414    #[tokio::test]
2415    async fn test_response_header_no_response() {
2416        let entry = NetworkEntry {
2417            request: make_request("1", "GET", "https://a.com"),
2418            response: None,
2419            error: None,
2420        };
2421        assert_eq!(entry.response_header("anything"), None);
2422    }
2423
2424    // ---------- timing edge cases ----------
2425
2426    #[tokio::test]
2427    async fn test_option_f64_ms_both_none() {
2428        assert_eq!(option_f64_ms(None, None), -1.0);
2429    }
2430
2431    #[tokio::test]
2432    async fn test_option_f64_ms_end_none() {
2433        assert_eq!(option_f64_ms(None, Some(5.0)), -1.0);
2434    }
2435
2436    // ---------- ingestion pipeline ----------
2437
2438    fn make_bidi_request_data(
2439        id: &str,
2440        url: &str,
2441        method: &str,
2442    ) -> rustenium_bidi_definitions::network::types::RequestData {
2443        use rustenium_bidi_definitions::network::types::{FetchTimingInfo, Request, RequestData};
2444        RequestData {
2445            request: Request::new(id),
2446            url: url.into(),
2447            method: method.into(),
2448            headers: vec![],
2449            cookies: vec![],
2450            headers_size: 0,
2451            body_size: None,
2452            destination: "document".into(),
2453            initiator_type: None,
2454            timings: FetchTimingInfo {
2455                time_origin: 0.0,
2456                request_time: 0.0,
2457                redirect_start: 0.0,
2458                redirect_end: 0.0,
2459                fetch_start: 0.0,
2460                dns_start: 0.0,
2461                dns_end: 0.0,
2462                connect_start: 0.0,
2463                connect_end: 0.0,
2464                tls_start: 0.0,
2465                request_start: 0.0,
2466                response_start: 0.0,
2467                response_end: 0.0,
2468            },
2469            extensible: std::collections::HashMap::new(),
2470        }
2471    }
2472
2473    fn make_before_request_sent(id: &str, url: &str, method: &str) -> BeforeRequestSent {
2474        use rustenium_bidi_definitions::network::events::BeforeRequestSentParams;
2475        use rustenium_bidi_definitions::network::types::BaseParameters;
2476        BeforeRequestSent {
2477            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
2478            params: BeforeRequestSentParams {
2479                base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, method), 0u64),
2480                initiator: None,
2481            },
2482        }
2483    }
2484
2485    fn make_response_completed(id: &str, url: &str, status: u64) -> ResponseCompleted {
2486        use rustenium_bidi_definitions::network::events::ResponseCompletedParams;
2487        use rustenium_bidi_definitions::network::types::{
2488            BaseParameters, ResponseContent, ResponseData,
2489        };
2490        ResponseCompleted {
2491            method: rustenium_bidi_definitions::network::events::ResponseCompletedMethod::ResponseCompleted,
2492            params: ResponseCompletedParams {
2493                base_parameters: BaseParameters::new(false, 0u64, make_bidi_request_data(id, url, "GET"), 0u64),
2494                response: ResponseData {
2495                    url: url.into(),
2496                    protocol: "h2".into(),
2497                    status,
2498                    status_text: "OK".into(),
2499                    from_cache: false,
2500                    headers: vec![],
2501                    mime_type: "application/json".into(),
2502                    bytes_received: 100,
2503                    headers_size: None,
2504                    body_size: Some(100),
2505                    content: ResponseContent::new(100u64),
2506                    auth_challenges: None,
2507                },
2508            },
2509        }
2510    }
2511
2512    fn make_fetch_error(id: &str, url: &str, error_text: &str) -> FetchError {
2513        use rustenium_bidi_definitions::network::events::FetchErrorParams;
2514        use rustenium_bidi_definitions::network::types::BaseParameters;
2515        FetchError {
2516            method: rustenium_bidi_definitions::network::events::FetchErrorMethod::FetchError,
2517            params: FetchErrorParams {
2518                base_parameters: BaseParameters::new(
2519                    false,
2520                    0u64,
2521                    make_bidi_request_data(id, url, "GET"),
2522                    0u64,
2523                ),
2524                error_text: error_text.into(),
2525            },
2526        }
2527    }
2528
2529    #[tokio::test]
2530    async fn test_ingest_before_request_sent_creates_entry() {
2531        let log = NetworkLog::new();
2532        let evt = make_before_request_sent("req-1", "https://example.com", "GET");
2533        log.ingest_before_request_sent(&evt).await;
2534        assert_eq!(log.len().await, 1);
2535        assert!(log.contains_id("req-1").await);
2536        let m = log.metrics().await;
2537        assert_eq!(m.requests_received, 1);
2538    }
2539
2540    #[tokio::test]
2541    async fn test_ingest_response_completed_attaches_to_existing() {
2542        let log = NetworkLog::new();
2543        log.ingest_before_request_sent(&make_before_request_sent(
2544            "req-1",
2545            "https://example.com",
2546            "GET",
2547        ))
2548        .await;
2549        log.ingest_response_completed(&make_response_completed(
2550            "req-1",
2551            "https://example.com",
2552            200,
2553        ))
2554        .await;
2555
2556        let entry = log.first().await.unwrap();
2557        assert_eq!(entry.status(), Some(200));
2558        assert!(entry.has_response());
2559        let m = log.metrics().await;
2560        assert_eq!(m.responses_received, 1);
2561    }
2562
2563    #[tokio::test]
2564    async fn test_ingest_fetch_error_attaches_to_existing() {
2565        let log = NetworkLog::new();
2566        log.ingest_before_request_sent(&make_before_request_sent(
2567            "req-1",
2568            "https://example.com",
2569            "GET",
2570        ))
2571        .await;
2572        log.ingest_fetch_error(&make_fetch_error(
2573            "req-1",
2574            "https://example.com",
2575            "net::ERR_FAILED",
2576        ))
2577        .await;
2578
2579        let entry = log.first().await.unwrap();
2580        assert!(entry.is_error());
2581        assert_eq!(entry.error.as_ref().unwrap().error_text, "net::ERR_FAILED");
2582        let m = log.metrics().await;
2583        assert_eq!(m.errors_received, 1);
2584    }
2585
2586    #[tokio::test]
2587    async fn test_fetch_error_url_comes_from_request_not_navigation() {
2588        // make_fetch_error builds an event with navigation = None (the common
2589        // subresource case: a failed script/image/XHR carries no top-level
2590        // document URL). The captured error URL must still be the actual
2591        // requested URL, never empty. The old code read `navigation` and
2592        // defaulted to "" here, hiding which resource failed.
2593        let log = NetworkLog::new();
2594        let failed_url = "https://cdn.example.com/app.js";
2595        log.ingest_fetch_error(&make_fetch_error("sub-1", failed_url, "net::ERR_FAILED"))
2596            .await;
2597
2598        let (_resp, errors) = log.drain_pending().await;
2599        assert_eq!(errors.len(), 1);
2600        assert_eq!(
2601            errors[0].url, failed_url,
2602            "error URL must be the request URL, not an empty navigation fallback"
2603        );
2604    }
2605
2606    #[tokio::test]
2607    async fn test_out_of_order_response_reconciliation() {
2608        let log = NetworkLog::new();
2609        // Response arrives before request
2610        log.ingest_response_completed(&make_response_completed(
2611            "req-1",
2612            "https://example.com",
2613            200,
2614        ))
2615        .await;
2616        assert_eq!(log.len().await, 0); // No entry yet
2617        assert_eq!(log.metrics().await.responses_received, 1);
2618
2619        // Now request arrives
2620        log.ingest_before_request_sent(&make_before_request_sent(
2621            "req-1",
2622            "https://example.com",
2623            "GET",
2624        ))
2625        .await;
2626        assert_eq!(log.len().await, 1);
2627        let entry = log.first().await.unwrap();
2628        assert_eq!(entry.status(), Some(200));
2629    }
2630
2631    #[tokio::test]
2632    async fn test_out_of_order_error_reconciliation() {
2633        let log = NetworkLog::new();
2634        log.ingest_fetch_error(&make_fetch_error(
2635            "req-1",
2636            "https://example.com",
2637            "net::ERR_ABORTED",
2638        ))
2639        .await;
2640        assert_eq!(log.len().await, 0);
2641
2642        log.ingest_before_request_sent(&make_before_request_sent(
2643            "req-1",
2644            "https://example.com",
2645            "GET",
2646        ))
2647        .await;
2648        let entry = log.first().await.unwrap();
2649        assert!(entry.is_error());
2650    }
2651
2652    #[tokio::test]
2653    async fn test_response_update_rebroadcasts() {
2654        let log = NetworkLog::new();
2655        let mut rx = log.subscribe().await;
2656
2657        log.ingest_before_request_sent(&make_before_request_sent(
2658            "req-1",
2659            "https://example.com",
2660            "GET",
2661        ))
2662        .await;
2663        let first = rx.recv().await.unwrap();
2664        assert!(!first.has_response());
2665
2666        log.ingest_response_completed(&make_response_completed(
2667            "req-1",
2668            "https://example.com",
2669            200,
2670        ))
2671        .await;
2672        let updated = rx.recv().await.unwrap();
2673        assert!(updated.has_response());
2674        assert_eq!(updated.status(), Some(200));
2675    }
2676
2677    #[tokio::test]
2678    async fn test_pending_response_overflow() {
2679        let log = NetworkLog::with_limits(100, 3);
2680        // 5 responses arrive before their requests
2681        for i in 0..5 {
2682            log.ingest_response_completed(&make_response_completed(
2683                &format!("req-{}", i),
2684                "https://example.com",
2685                200,
2686            ))
2687            .await;
2688        }
2689        let m = log.metrics().await;
2690        assert_eq!(m.pending_responses_dropped, 2); // max_pending=3, so 2 dropped
2691        assert_eq!(m.responses_received, 5);
2692    }
2693
2694    #[tokio::test]
2695    async fn test_pending_error_overflow() {
2696        let log = NetworkLog::with_limits(100, 2);
2697        for i in 0..4 {
2698            log.ingest_fetch_error(&make_fetch_error(
2699                &format!("req-{}", i),
2700                "https://example.com",
2701                "err",
2702            ))
2703            .await;
2704        }
2705        let m = log.metrics().await;
2706        assert_eq!(m.pending_errors_dropped, 2); // max_pending=2, so 2 dropped
2707        assert_eq!(m.errors_received, 4);
2708    }
2709
2710    #[tokio::test]
2711    async fn test_clear_then_ingest() {
2712        let log = NetworkLog::new();
2713        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
2714            .await;
2715        log.clear().await;
2716        log.ingest_before_request_sent(&make_before_request_sent("req-2", "https://b.com", "POST"))
2717            .await;
2718        assert_eq!(log.len().await, 1);
2719        assert!(!log.contains_id("req-1").await);
2720        assert!(log.contains_id("req-2").await);
2721    }
2722
2723    #[tokio::test]
2724    async fn test_entry_with_both_response_and_error() {
2725        let log = NetworkLog::new();
2726        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
2727            .await;
2728        log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
2729            .await;
2730        log.ingest_fetch_error(&make_fetch_error(
2731            "req-1",
2732            "https://a.com",
2733            "net::ERR_FAILED",
2734        ))
2735        .await;
2736
2737        let entry = log.first().await.unwrap();
2738        assert!(entry.has_response());
2739        assert!(entry.is_error());
2740        assert_eq!(entry.status(), Some(200));
2741    }
2742
2743    // ---------- cookie / header serialization ----------
2744
2745    #[tokio::test]
2746    async fn test_captured_cookie_roundtrip() {
2747        let cookie = CapturedCookie {
2748            name: "session".into(),
2749            value: "abc123".into(),
2750            domain: ".example.com".into(),
2751            path: "/".into(),
2752            size: 42,
2753            http_only: true,
2754            secure: true,
2755            same_site: "strict".into(),
2756        };
2757        let json = serde_json::to_string(&cookie).unwrap();
2758        let de: CapturedCookie = serde_json::from_str(&json).unwrap();
2759        assert_eq!(de, cookie);
2760    }
2761
2762    #[tokio::test]
2763    async fn test_captured_header_roundtrip() {
2764        let h = CapturedHeader {
2765            name: "X-Test".into(),
2766            value: "value".into(),
2767        };
2768        let json = serde_json::to_string(&h).unwrap();
2769        let de: CapturedHeader = serde_json::from_str(&json).unwrap();
2770        assert_eq!(de, h);
2771    }
2772
2773    #[tokio::test]
2774    async fn test_filter_url_regex_invalid_pattern() {
2775        let result = Filter::new().url_regex("[");
2776        assert!(result.is_err());
2777    }
2778
2779    #[tokio::test]
2780    async fn test_large_url_does_not_panic() {
2781        let log = NetworkLog::new();
2782        let long_url = format!("https://example.com/{}", "a".repeat(10000));
2783        push_request(&log, make_request("1", "GET", &long_url)).await;
2784        assert_eq!(log.len().await, 1);
2785        assert!(log.find_by_url("example.com").await.is_some());
2786    }
2787
2788    #[tokio::test]
2789    async fn test_eviction_with_subscriber() {
2790        let log = NetworkLog::with_limits(2, 10);
2791        let mut rx = log.subscribe().await;
2792        for i in 0..5 {
2793            push_request(
2794                &log,
2795                make_request(&format!("{}", i), "GET", "https://example.com"),
2796            )
2797            .await;
2798        }
2799        // Should have received all 5 broadcast messages despite eviction
2800        let mut count = 0;
2801        while rx.try_recv().is_ok() {
2802            count += 1;
2803        }
2804        assert_eq!(count, 5);
2805    }
2806
2807    // ---------- deeper edge cases ----------
2808
2809    #[tokio::test]
2810    async fn test_filter_method_case_insensitive() {
2811        let log = NetworkLog::new();
2812        push_request(&log, make_request("1", "DELETE", "https://a.com")).await;
2813        assert_eq!(log.filter(Filter::new().method("delete")).await.len(), 1);
2814        assert_eq!(log.filter(Filter::new().method("DELETE")).await.len(), 1);
2815    }
2816
2817    #[tokio::test]
2818    async fn test_filter_header_in_response() {
2819        let log = NetworkLog::new();
2820        let req = make_request("1", "GET", "https://a.com");
2821        let mut resp = make_response("1", 200, "https://a.com");
2822        resp.headers = vec![CapturedHeader {
2823            name: "X-Resp".into(),
2824            value: "secret-val".into(),
2825        }];
2826        push_entry(&log, req, Some(resp), None).await;
2827        let f = log.filter(Filter::new().header("x-resp", "secret")).await;
2828        assert_eq!(f.len(), 1);
2829    }
2830
2831    #[tokio::test]
2832    async fn test_filter_composition_conflicting() {
2833        let log = NetworkLog::new();
2834        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2835        // method GET AND method POST -> impossible
2836        let f = log.filter(Filter::new().method("GET").method("POST")).await;
2837        assert!(f.is_empty());
2838    }
2839
2840    #[tokio::test]
2841    async fn test_count_zero() {
2842        let log = NetworkLog::new();
2843        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2844        assert_eq!(log.count(Filter::new().method("POST")).await, 0);
2845    }
2846
2847    #[tokio::test]
2848    async fn test_is_empty_false() {
2849        let log = NetworkLog::new();
2850        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2851        assert!(!log.is_empty().await);
2852    }
2853
2854    #[tokio::test]
2855    async fn test_endpoints_dedupes() {
2856        let log = NetworkLog::new();
2857        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
2858        push_request(&log, make_request("2", "GET", "https://a.com/x")).await;
2859        push_request(&log, make_request("3", "GET", "https://a.com/y")).await;
2860        let ep = log.endpoints().await;
2861        assert_eq!(ep.len(), 2);
2862    }
2863
2864    #[tokio::test]
2865    async fn test_hostnames_with_port() {
2866        let log = NetworkLog::new();
2867        push_request(
2868            &log,
2869            make_request("1", "GET", "https://example.com:8443/path"),
2870        )
2871        .await;
2872        let h = log.hostnames().await;
2873        assert_eq!(h.len(), 1);
2874        assert!(h.contains(&"example.com".into()));
2875    }
2876
2877    #[tokio::test]
2878    async fn test_total_bytes_out_none() {
2879        let log = NetworkLog::new();
2880        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2881        assert_eq!(log.total_bytes_out().await, 0);
2882    }
2883
2884    #[tokio::test]
2885    async fn test_completed_mixed_states() {
2886        let log = NetworkLog::new();
2887        push_entry(
2888            &log,
2889            make_request("1", "GET", "https://a.com"),
2890            Some(make_response("1", 200, "https://a.com")),
2891            None,
2892        )
2893        .await;
2894        push_request(&log, make_request("2", "GET", "https://b.com")).await;
2895        push_entry(
2896            &log,
2897            make_request("3", "GET", "https://c.com"),
2898            None,
2899            Some(make_error("3", "https://c.com", "err")),
2900        )
2901        .await;
2902        push_entry(
2903            &log,
2904            make_request("4", "GET", "https://d.com"),
2905            Some(make_response("4", 500, "https://d.com")),
2906            Some(make_error("4", "https://d.com", "err")),
2907        )
2908        .await;
2909        let c = log.completed().await;
2910        assert_eq!(c.len(), 3);
2911    }
2912
2913    #[tokio::test]
2914    async fn test_distinct_statuses_with_no_response() {
2915        let log = NetworkLog::new();
2916        push_entry(
2917            &log,
2918            make_request("1", "GET", "https://a.com"),
2919            Some(make_response("1", 200, "https://a.com")),
2920            None,
2921        )
2922        .await;
2923        push_request(&log, make_request("2", "GET", "https://b.com")).await;
2924        push_entry(
2925            &log,
2926            make_request("3", "GET", "https://c.com"),
2927            Some(make_response("3", 404, "https://c.com")),
2928            None,
2929        )
2930        .await;
2931        let s = log.distinct_statuses().await;
2932        assert_eq!(s, vec![200, 404]);
2933    }
2934
2935    #[tokio::test]
2936    async fn test_distinct_methods_dedupes() {
2937        let log = NetworkLog::new();
2938        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2939        push_request(&log, make_request("2", "GET", "https://b.com")).await;
2940        push_request(&log, make_request("3", "POST", "https://c.com")).await;
2941        push_request(&log, make_request("4", "POST", "https://d.com")).await;
2942        let m = log.distinct_methods().await;
2943        assert_eq!(m, vec!["GET", "POST"]);
2944    }
2945
2946    #[tokio::test]
2947    async fn test_find_by_url_empty_substring() {
2948        let log = NetworkLog::new();
2949        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2950        // Empty substring should match everything that contains ""
2951        assert!(log.find_by_url("").await.is_some());
2952    }
2953
2954    #[tokio::test]
2955    async fn test_find_by_url_regex_no_match() {
2956        let log = NetworkLog::new();
2957        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2958        let re = regex::Regex::new(r"zzz").unwrap();
2959        assert!(log.find_by_url_regex(&re).await.is_none());
2960    }
2961
2962    #[tokio::test]
2963    async fn test_filter_url_contains_empty() {
2964        let log = NetworkLog::new();
2965        push_request(&log, make_request("1", "GET", "https://a.com")).await;
2966        let f = log.filter(Filter::new().url_contains("")).await;
2967        assert_eq!(f.len(), 1);
2968    }
2969
2970    #[tokio::test]
2971    async fn test_to_curl_binary_body() {
2972        let req = CapturedRequest {
2973            id: "1".into(),
2974            context: None,
2975            method: "POST".into(),
2976            url: "https://example.com".into(),
2977            headers: vec![],
2978            post_data: Some("\x00\x01\x02".into()),
2979            timestamp: 0,
2980            destination: "document".into(),
2981            initiator_type: None,
2982            timing: CapturedTiming::default(),
2983            cookies: vec![],
2984        };
2985        let entry = NetworkEntry {
2986            request: req,
2987            response: None,
2988            error: None,
2989        };
2990        let curl = entry.to_curl();
2991        assert!(curl.contains("-d '"));
2992    }
2993
2994    #[tokio::test]
2995    async fn test_json_body_empty_string() {
2996        let mut req = make_request("1", "POST", "https://a.com");
2997        req.post_data = Some("".into());
2998        assert!(req.json_body().is_none());
2999    }
3000
3001    #[tokio::test]
3002    async fn test_query_params_duplicate_keys() {
3003        let req = make_request("1", "GET", "https://a.com?foo=1&foo=2");
3004        let params = req.query_params().unwrap();
3005        assert_eq!(params.len(), 2);
3006        assert!(params.contains(&("foo".into(), "1".into())));
3007        assert!(params.contains(&("foo".into(), "2".into())));
3008    }
3009
3010    #[tokio::test]
3011    async fn test_request_header_empty_name() {
3012        let req = make_request_with_headers("1", "GET", "https://a.com", vec![]);
3013        assert_eq!(req.request_header(""), None);
3014    }
3015
3016    #[tokio::test]
3017    async fn test_response_header_empty_name() {
3018        let entry = NetworkEntry {
3019            request: make_request("1", "GET", "https://a.com"),
3020            response: Some(make_response("1", 200, "https://a.com")),
3021            error: None,
3022        };
3023        assert_eq!(entry.response_header(""), None);
3024    }
3025
3026    #[tokio::test]
3027    async fn test_remove_by_id_first() {
3028        let log = NetworkLog::new();
3029        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3030        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3031        log.remove_by_id("1").await;
3032        assert_eq!(log.first().await.unwrap().request.id, "2");
3033    }
3034
3035    #[tokio::test]
3036    async fn test_remove_by_id_last() {
3037        let log = NetworkLog::new();
3038        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3039        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3040        log.remove_by_id("2").await;
3041        assert_eq!(log.last().await.unwrap().request.id, "1");
3042    }
3043
3044    #[tokio::test]
3045    async fn test_contains_id_after_eviction() {
3046        let log = NetworkLog::with_limits(4, 10);
3047        for i in 0..10 {
3048            push_request(
3049                &log,
3050                make_request(&format!("{}", i), "GET", "https://example.com"),
3051            )
3052            .await;
3053        }
3054        // After eviction, only 4-9 should remain
3055        for i in 0..4 {
3056            assert!(!log.contains_id(&format!("{}", i)).await);
3057        }
3058        for i in 6..10 {
3059            assert!(log.contains_id(&format!("{}", i)).await);
3060        }
3061    }
3062
3063    #[tokio::test]
3064    async fn test_wait_for_response_error_instead_of_response() {
3065        let log = NetworkLog::new();
3066        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3067        let log2 = log.clone();
3068        tokio::spawn(async move {
3069            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3070            let mut inner = log2.inner.write().await;
3071            if let Some(idx) = inner.by_id.get("1").copied() {
3072                let mut new_entry = (*inner.entries[idx]).clone();
3073                new_entry.error = Some(make_error("1", "https://a.com", "net::ERR_ABORTED"));
3074                let new_arc = Arc::new(new_entry);
3075                inner.entries[idx] = new_arc.clone();
3076                let _ = inner.tx.send(new_arc);
3077            }
3078        });
3079        let found = log
3080            .wait_for_response("1", std::time::Duration::from_secs(1))
3081            .await;
3082        assert!(found.unwrap().is_error());
3083    }
3084
3085    #[tokio::test]
3086    async fn test_captured_timing_from_bidi() {
3087        use rustenium_bidi_definitions::network::types::FetchTimingInfo;
3088        let info = FetchTimingInfo {
3089            time_origin: 0.0,
3090            request_time: 10.0,
3091            redirect_start: 0.0,
3092            redirect_end: 0.0,
3093            fetch_start: 10.0,
3094            dns_start: 11.0,
3095            dns_end: 12.0,
3096            connect_start: 12.0,
3097            connect_end: 14.0,
3098            tls_start: 13.0,
3099            request_start: 14.0,
3100            response_start: 15.0,
3101            response_end: 16.0,
3102        };
3103        let t = CapturedTiming::from(&info);
3104        assert_eq!(t.dns_start_ms, Some(1.0)); // 11 - 10
3105        assert_eq!(t.dns_end_ms, Some(2.0)); // 12 - 10
3106        assert_eq!(t.connect_start_ms, Some(2.0)); // 12 - 10
3107        assert_eq!(t.connect_end_ms, Some(4.0)); // 14 - 10
3108        assert_eq!(t.tls_start_ms, Some(3.0)); // 13 - 10
3109        assert_eq!(t.response_start_ms, Some(5.0)); // 15 - 10
3110        assert_eq!(t.response_end_ms, Some(6.0)); // 16 - 10
3111    }
3112
3113    #[tokio::test]
3114    async fn test_captured_timing_negative_clamped() {
3115        use rustenium_bidi_definitions::network::types::FetchTimingInfo;
3116        let info = FetchTimingInfo {
3117            time_origin: 0.0,
3118            request_time: 10.0,
3119            redirect_start: 0.0,
3120            redirect_end: 0.0,
3121            fetch_start: 10.0,
3122            dns_start: 9.0, // before request_time
3123            dns_end: 8.0,   // before request_time
3124            connect_start: 0.0,
3125            connect_end: 0.0,
3126            tls_start: 0.0,
3127            request_start: 0.0,
3128            response_start: 0.0,
3129            response_end: 0.0,
3130        };
3131        let t = CapturedTiming::from(&info);
3132        assert_eq!(t.dns_start_ms, None); // negative clamped to None
3133        assert_eq!(t.dns_end_ms, None);
3134    }
3135
3136    #[tokio::test]
3137    async fn test_har_timings_correct() {
3138        let log = NetworkLog::new();
3139        let mut req = make_request("1", "GET", "https://api.example.com");
3140        req.timing = CapturedTiming {
3141            dns_start_ms: Some(1.0),
3142            dns_end_ms: Some(3.0),
3143            connect_start_ms: Some(3.0),
3144            connect_end_ms: Some(7.0),
3145            tls_start_ms: Some(5.0),
3146            response_start_ms: Some(8.0),
3147            response_end_ms: Some(10.0),
3148        };
3149        push_entry(
3150            &log,
3151            req,
3152            Some(make_response("1", 200, "https://api.example.com")),
3153            None,
3154        )
3155        .await;
3156
3157        let path = std::path::Path::new("/tmp/foxdriver_har_timings.har");
3158        log.save_as_har(path, None).await.unwrap();
3159        let content = std::fs::read_to_string(path).unwrap();
3160        // dns should be 2.0 (3.0 - 1.0)
3161        assert!(content.contains("\"dns\": 2.0") || content.contains("\"dns\": 2"));
3162        // connect should be 4.0 (7.0 - 3.0)
3163        assert!(content.contains("\"connect\": 4.0") || content.contains("\"connect\": 4"));
3164        // ssl should be 2.0 (7.0 - 5.0)
3165        assert!(content.contains("\"ssl\": 2.0") || content.contains("\"ssl\": 2"));
3166    }
3167
3168    #[tokio::test]
3169    async fn test_concurrent_ingestion_stress() {
3170        let log = NetworkLog::new();
3171        let mut handles = Vec::new();
3172        for t in 0..20 {
3173            let log = log.clone();
3174            handles.push(tokio::spawn(async move {
3175                for i in 0..50 {
3176                    let id = format!("{}-{}", t, i);
3177                    log.ingest_before_request_sent(&make_before_request_sent(
3178                        &id,
3179                        "https://example.com",
3180                        "GET",
3181                    ))
3182                    .await;
3183                    if i % 2 == 0 {
3184                        log.ingest_response_completed(&make_response_completed(
3185                            &id,
3186                            "https://example.com",
3187                            200,
3188                        ))
3189                        .await;
3190                    } else {
3191                        log.ingest_fetch_error(&make_fetch_error(
3192                            &id,
3193                            "https://example.com",
3194                            "err",
3195                        ))
3196                        .await;
3197                    }
3198                }
3199            }));
3200        }
3201        for h in handles {
3202            h.await.unwrap();
3203        }
3204        assert_eq!(log.len().await, 1000);
3205        let completed = log.completed().await;
3206        assert_eq!(completed.len(), 1000); // all have response or error
3207        let m = log.metrics().await;
3208        assert_eq!(m.requests_received, 1000);
3209        assert_eq!(m.responses_received, 500);
3210        assert_eq!(m.errors_received, 500);
3211    }
3212
3213    #[tokio::test]
3214    async fn test_property_filter_then_count_matches() {
3215        let log = NetworkLog::new();
3216        for i in 0..100 {
3217            let method = if i % 2 == 0 { "GET" } else { "POST" };
3218            push_request(
3219                &log,
3220                make_request(&format!("{}", i), method, "https://example.com"),
3221            )
3222            .await;
3223        }
3224        let f = Filter::new().method("GET");
3225        let filtered = log.filter(f.clone()).await;
3226        let counted = log.count(f).await;
3227        assert_eq!(filtered.len(), counted);
3228        assert_eq!(counted, 50);
3229    }
3230
3231    #[tokio::test]
3232    async fn test_property_first_last_consistency() {
3233        let log = NetworkLog::new();
3234        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3235        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3236        push_request(&log, make_request("3", "GET", "https://c.com")).await;
3237
3238        assert_eq!(
3239            log.first().await.unwrap().request.id,
3240            log.nth(0).await.unwrap().request.id
3241        );
3242        assert_eq!(
3243            log.last().await.unwrap().request.id,
3244            log.nth(2).await.unwrap().request.id
3245        );
3246    }
3247
3248    #[tokio::test]
3249    async fn test_property_endpoints_is_subset_of_entries() {
3250        let log = NetworkLog::new();
3251        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
3252        push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
3253        let entries = log.entries().await;
3254        let endpoints = log.endpoints().await;
3255        assert!(endpoints.len() <= entries.len());
3256        for ep in &endpoints {
3257            assert!(entries.iter().any(|e| e.request.url == *ep));
3258        }
3259    }
3260
3261    #[tokio::test]
3262    async fn test_property_hostnames_is_subset_of_endpoints() {
3263        let log = NetworkLog::new();
3264        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
3265        push_request(&log, make_request("2", "GET", "https://b.com/y")).await;
3266        let hostnames = log.hostnames().await;
3267        let endpoints = log.endpoints().await;
3268        for h in &hostnames {
3269            assert!(endpoints.iter().any(|e| e.contains(h)));
3270        }
3271    }
3272
3273    #[tokio::test]
3274    async fn test_stress_many_entries() {
3275        let log = NetworkLog::with_limits(100_000, 10_000);
3276        for i in 0..10_000 {
3277            push_request(
3278                &log,
3279                make_request(
3280                    &format!("{}", i),
3281                    "GET",
3282                    &format!("https://example.com/{}", i),
3283                ),
3284            )
3285            .await;
3286        }
3287        assert_eq!(log.len().await, 10_000);
3288        assert!(log.find_by_url("example.com").await.is_some());
3289        assert_eq!(log.distinct_methods().await, vec!["GET"]);
3290    }
3291
3292    // ---------- make_network_handler ----------
3293
3294    #[tokio::test]
3295    async fn test_handler_ignores_non_network_events() {
3296        let log = NetworkLog::new();
3297        let mut handler = make_network_handler(log.clone());
3298        // Log event should be ignored
3299        let evt = rustenium_bidi_definitions::Event::Log(
3300            rustenium_bidi_definitions::log::events::LogEvent::EntryAdded(
3301                rustenium_bidi_definitions::log::events::EntryAdded {
3302                    method: rustenium_bidi_definitions::log::events::EntryAddedMethod::EntryAdded,
3303                    params: rustenium_bidi_definitions::log::events::EntryAddedParams {},
3304                },
3305            ),
3306        );
3307        handler(evt).await;
3308        assert!(log.is_empty().await);
3309    }
3310
3311    #[tokio::test]
3312    async fn test_handler_processes_before_request_sent() {
3313        let log = NetworkLog::new();
3314        let mut handler = make_network_handler(log.clone());
3315        let evt = rustenium_bidi_definitions::Event::Network(
3316            rustenium_bidi_definitions::network::events::NetworkEvent::BeforeRequestSent(
3317                make_before_request_sent("req-1", "https://example.com", "GET"),
3318            ),
3319        );
3320        handler(evt).await;
3321        assert_eq!(log.len().await, 1);
3322        assert!(log.contains_id("req-1").await);
3323    }
3324
3325    #[tokio::test]
3326    async fn test_handler_processes_response_completed() {
3327        let log = NetworkLog::new();
3328        let mut handler = make_network_handler(log.clone());
3329        handler(rustenium_bidi_definitions::Event::Network(
3330            rustenium_bidi_definitions::network::events::NetworkEvent::BeforeRequestSent(
3331                make_before_request_sent("req-1", "https://example.com", "GET"),
3332            ),
3333        ))
3334        .await;
3335        handler(rustenium_bidi_definitions::Event::Network(
3336            rustenium_bidi_definitions::network::events::NetworkEvent::ResponseCompleted(
3337                make_response_completed("req-1", "https://example.com", 200),
3338            ),
3339        ))
3340        .await;
3341        let entry = log.first().await.unwrap();
3342        assert_eq!(entry.status(), Some(200));
3343    }
3344
3345    #[tokio::test]
3346    async fn test_handler_ignores_unknown_network_events() {
3347        let log = NetworkLog::new();
3348        let mut handler = make_network_handler(log.clone());
3349        // ResponseStarted is not handled
3350        handler(rustenium_bidi_definitions::Event::Network(
3351            rustenium_bidi_definitions::network::events::NetworkEvent::ResponseStarted(
3352                rustenium_bidi_definitions::network::events::ResponseStarted {
3353                    method: rustenium_bidi_definitions::network::events::ResponseStartedMethod::ResponseStarted,
3354                    params: rustenium_bidi_definitions::network::events::ResponseStartedParams {
3355                        base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(
3356                            false, 0u64, make_bidi_request_data("req-1", "https://example.com", "GET"), 0u64
3357                        ),
3358                        response: rustenium_bidi_definitions::network::types::ResponseData {
3359                            url: "https://example.com".into(),
3360                            protocol: "h2".into(),
3361                            status: 200,
3362                            status_text: "OK".into(),
3363                            from_cache: false,
3364                            headers: vec![],
3365                            mime_type: "application/json".into(),
3366                            bytes_received: 100,
3367                            headers_size: None,
3368                            body_size: Some(100),
3369                            content: rustenium_bidi_definitions::network::types::ResponseContent::new(100u64),
3370                            auth_challenges: None,
3371                        },
3372                    },
3373                }
3374            )
3375        )).await;
3376        assert!(log.is_empty().await);
3377    }
3378
3379    // ---------- export edge cases ----------
3380
3381    #[tokio::test]
3382    async fn test_har_with_null_response() {
3383        let log = NetworkLog::new();
3384        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3385        let path = std::path::Path::new("/tmp/foxdriver_har_null_resp.har");
3386        log.save_as_har(path, None).await.unwrap();
3387        let content = std::fs::read_to_string(path).unwrap();
3388        assert!(content.contains("\"response\": null"));
3389    }
3390
3391    #[tokio::test]
3392    async fn test_har_with_error_entry() {
3393        let log = NetworkLog::new();
3394        push_entry(
3395            &log,
3396            make_request("1", "GET", "https://a.com"),
3397            None,
3398            Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
3399        )
3400        .await;
3401        let path = std::path::Path::new("/tmp/foxdriver_har_error.har");
3402        log.save_as_har(path, None).await.unwrap();
3403        let content = std::fs::read_to_string(path).unwrap();
3404        assert!(content.contains("\"response\": null"));
3405    }
3406
3407    #[tokio::test]
3408    async fn test_default_impl() {
3409        let log: NetworkLog = Default::default();
3410        assert!(log.is_empty().await);
3411        let m = log.metrics().await;
3412        assert_eq!(m.max_entries, 50_000);
3413    }
3414
3415    #[tokio::test]
3416    async fn test_clear_does_not_break_broadcast() {
3417        let log = NetworkLog::new();
3418        let mut rx = log.subscribe().await;
3419        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3420        log.clear().await;
3421        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3422        // Should still receive both (clear doesn't close channel)
3423        let first = rx.recv().await.unwrap();
3424        assert_eq!(first.request.id, "1");
3425        let second = rx.recv().await.unwrap();
3426        assert_eq!(second.request.id, "2");
3427    }
3428
3429    #[tokio::test]
3430    async fn test_entries_returns_arc_clones() {
3431        let log = NetworkLog::new();
3432        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3433        let e1 = log.entries().await;
3434        let e2 = log.entries().await;
3435        // Arc::ptr_eq should be true since they point to the same data
3436        assert!(Arc::ptr_eq(&e1[0], &e2[0]));
3437    }
3438
3439    #[tokio::test]
3440    async fn test_filter_header_only_response() {
3441        let log = NetworkLog::new();
3442        let req = make_request("1", "GET", "https://a.com");
3443        let mut resp = make_response("1", 200, "https://a.com");
3444        resp.headers = vec![CapturedHeader {
3445            name: "X-Resp-Only".into(),
3446            value: "found-me".into(),
3447        }];
3448        push_entry(&log, req, Some(resp), None).await;
3449        // Filter should find the header even though it's only in response
3450        let f = log
3451            .filter(Filter::new().header("x-resp-only", "found"))
3452            .await;
3453        assert_eq!(f.len(), 1);
3454    }
3455
3456    #[tokio::test]
3457    async fn test_find_by_url_after_remove() {
3458        let log = NetworkLog::new();
3459        push_request(&log, make_request("1", "GET", "https://a.com/page")).await;
3460        push_request(&log, make_request("2", "GET", "https://b.com/page")).await;
3461        log.remove_by_id("1").await;
3462        assert!(log.find_by_url("a.com").await.is_none());
3463        assert!(log.find_by_url("b.com").await.is_some());
3464    }
3465
3466    #[tokio::test]
3467    async fn test_concurrent_wait_for_url() {
3468        let log = NetworkLog::new();
3469        let log2 = log.clone();
3470        let log3 = log.clone();
3471        let h1 = tokio::spawn(async move {
3472            log2.wait_for_url("target", std::time::Duration::from_secs(1))
3473                .await
3474        });
3475        let h2 = tokio::spawn(async move {
3476            log3.wait_for_url("target", std::time::Duration::from_secs(1))
3477                .await
3478        });
3479        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
3480        push_request(&log, make_request("1", "GET", "https://target.com")).await;
3481        let r1 = h1.await.unwrap();
3482        let r2 = h2.await.unwrap();
3483        assert!(r1.is_some());
3484        assert!(r2.is_some());
3485    }
3486
3487    #[tokio::test]
3488    async fn test_build_response_from_cache() {
3489        let log = NetworkLog::new();
3490        let mut resp = make_response("1", 200, "https://a.com");
3491        resp.from_cache = true;
3492        push_entry(
3493            &log,
3494            make_request("1", "GET", "https://a.com"),
3495            Some(resp),
3496            None,
3497        )
3498        .await;
3499        let entry = log.first().await.unwrap();
3500        assert!(entry.response.as_ref().unwrap().from_cache);
3501    }
3502
3503    #[tokio::test]
3504    async fn test_build_response_no_body_size() {
3505        let log = NetworkLog::new();
3506        let mut resp = make_response("1", 204, "https://a.com");
3507        resp.body_size = None;
3508        push_entry(
3509            &log,
3510            make_request("1", "GET", "https://a.com"),
3511            Some(resp),
3512            None,
3513        )
3514        .await;
3515        let entry = log.first().await.unwrap();
3516        assert_eq!(entry.response.as_ref().unwrap().body_size, None);
3517        assert_eq!(log.total_bytes_in().await, 0);
3518    }
3519
3520    #[tokio::test]
3521    async fn test_filter_has_response_and_has_error_same_entry() {
3522        let log = NetworkLog::new();
3523        push_entry(
3524            &log,
3525            make_request("1", "GET", "https://a.com"),
3526            Some(make_response("1", 500, "https://a.com")),
3527            Some(make_error("1", "https://a.com", "err")),
3528        )
3529        .await;
3530        // with_response should match
3531        assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
3532        // with_error should match
3533        assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
3534    }
3535
3536    #[tokio::test]
3537    async fn test_nth_after_remove() {
3538        let log = NetworkLog::new();
3539        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3540        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3541        push_request(&log, make_request("3", "GET", "https://c.com")).await;
3542        log.remove_by_id("2").await;
3543        assert_eq!(log.nth(0).await.unwrap().request.id, "1");
3544        assert_eq!(log.nth(1).await.unwrap().request.id, "3");
3545        assert!(log.nth(2).await.is_none());
3546    }
3547
3548    #[tokio::test]
3549    async fn test_pending_response_then_error_then_request() {
3550        let log = NetworkLog::new();
3551        // Response arrives first
3552        log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
3553            .await;
3554        // Then error arrives
3555        log.ingest_fetch_error(&make_fetch_error(
3556            "req-1",
3557            "https://a.com",
3558            "net::ERR_ABORTED",
3559        ))
3560        .await;
3561        // Then request arrives - should reconcile both response and error
3562        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3563            .await;
3564        let entry = log.first().await.unwrap();
3565        assert!(entry.has_response());
3566        assert!(entry.is_error());
3567    }
3568
3569    #[tokio::test]
3570    async fn test_metrics_persist_after_eviction() {
3571        let log = NetworkLog::with_limits(4, 10);
3572        for i in 0..10 {
3573            log.ingest_before_request_sent(&make_before_request_sent(
3574                &format!("{}", i),
3575                "https://a.com",
3576                "GET",
3577            ))
3578            .await;
3579        }
3580        let m = log.metrics().await;
3581        assert_eq!(m.requests_received, 10);
3582        assert_eq!(m.entries_evicted, 6); // 10 - 4 = 6 evicted (actually eviction removes half each time)
3583    }
3584
3585    #[tokio::test]
3586    async fn test_curl_strips_accept_encoding() {
3587        let req = CapturedRequest {
3588            id: "1".into(),
3589            context: None,
3590            method: "GET".into(),
3591            url: "https://example.com".into(),
3592            headers: vec![CapturedHeader {
3593                name: "Accept-Encoding".into(),
3594                value: "gzip".into(),
3595            }],
3596            post_data: None,
3597            timestamp: 0,
3598            destination: "document".into(),
3599            initiator_type: None,
3600            timing: CapturedTiming::default(),
3601            cookies: vec![],
3602        };
3603        let entry = NetworkEntry {
3604            request: req,
3605            response: None,
3606            error: None,
3607        };
3608        let curl = entry.to_curl();
3609        assert!(!curl.contains("Accept-Encoding"));
3610    }
3611
3612    // ---------- boundary condition bug fixes ----------
3613
3614    #[tokio::test]
3615    async fn test_max_entries_zero_does_not_panic() {
3616        let log = NetworkLog::with_limits(0, 10);
3617        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3618        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3619        // With max_entries=0, no eviction should occur (special case)
3620        assert_eq!(log.len().await, 2);
3621    }
3622
3623    #[tokio::test]
3624    async fn test_max_pending_zero_does_not_hang() {
3625        let log = NetworkLog::with_limits(100, 0);
3626        // Should not hang even though max_pending=0
3627        log.ingest_response_completed(&make_response_completed("req-1", "https://a.com", 200))
3628            .await;
3629        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3630            .await;
3631        assert_eq!(log.len().await, 1);
3632        assert!(log.first().await.unwrap().has_response());
3633    }
3634
3635    #[tokio::test]
3636    async fn test_max_entries_one() {
3637        let log = NetworkLog::with_limits(1, 10);
3638        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3639        push_request(&log, make_request("2", "GET", "https://b.com")).await;
3640        // Incremental eviction removes the oldest entry to make room for each new one.
3641        assert_eq!(log.len().await, 1);
3642        assert_eq!(log.last().await.unwrap().request.id, "2");
3643    }
3644
3645    // ---------- deeper ingestion coverage ----------
3646
3647    #[tokio::test]
3648    async fn test_ingest_request_with_headers() {
3649        let log = NetworkLog::new();
3650        let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3651        req_data.headers = vec![rustenium_bidi_definitions::network::types::Header {
3652            name: "X-Test".into(),
3653            value: rustenium_bidi_definitions::network::types::BytesValue::StringValue(
3654                rustenium_bidi_definitions::network::types::StringValue::new(
3655                    rustenium_bidi_definitions::network::types::StringValueType::String,
3656                    "hello",
3657                ),
3658            ),
3659        }];
3660        let evt = BeforeRequestSent {
3661            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3662            params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3663                base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3664                initiator: None,
3665            },
3666        };
3667        log.ingest_before_request_sent(&evt).await;
3668        let entry = log.first().await.unwrap();
3669        assert_eq!(entry.request.headers.len(), 1);
3670        assert_eq!(entry.request.headers[0].name, "X-Test");
3671        assert_eq!(entry.request.headers[0].value, "hello");
3672    }
3673
3674    #[tokio::test]
3675    async fn test_ingest_request_with_base64_header() {
3676        let log = NetworkLog::new();
3677        let mut req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3678        req_data.headers = vec![rustenium_bidi_definitions::network::types::Header {
3679            name: "X-Binary".into(),
3680            value: rustenium_bidi_definitions::network::types::BytesValue::Base64Value(
3681                rustenium_bidi_definitions::network::types::Base64Value::new(
3682                    rustenium_bidi_definitions::network::types::Base64ValueType::Base64,
3683                    "SGVsbG8=",
3684                ),
3685            ),
3686        }];
3687        let evt = BeforeRequestSent {
3688            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3689            params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3690                base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3691                initiator: None,
3692            },
3693        };
3694        log.ingest_before_request_sent(&evt).await;
3695        let entry = log.first().await.unwrap();
3696        assert_eq!(entry.request.headers[0].value, "SGVsbG8=");
3697    }
3698
3699    #[tokio::test]
3700    async fn test_ingest_request_with_context() {
3701        let log = NetworkLog::new();
3702        let req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3703        let mut base = rustenium_bidi_definitions::network::types::BaseParameters::new(
3704            false, 0u64, req_data, 0u64,
3705        );
3706        base.context = Some(
3707            rustenium_bidi_definitions::browsing_context::types::BrowsingContext::new("ctx-1"),
3708        );
3709        let evt = BeforeRequestSent {
3710            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3711            params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3712                base_parameters: base,
3713                initiator: None,
3714            },
3715        };
3716        log.ingest_before_request_sent(&evt).await;
3717        let entry = log.first().await.unwrap();
3718        assert_eq!(entry.request.context, Some("ctx-1".into()));
3719    }
3720
3721    #[tokio::test]
3722    async fn test_ingest_request_with_initiator() {
3723        let log = NetworkLog::new();
3724        let req_data = make_bidi_request_data("req-1", "https://a.com", "GET");
3725        let evt = BeforeRequestSent {
3726            method: rustenium_bidi_definitions::network::events::BeforeRequestSentMethod::BeforeRequestSent,
3727            params: rustenium_bidi_definitions::network::events::BeforeRequestSentParams {
3728                base_parameters: rustenium_bidi_definitions::network::types::BaseParameters::new(false, 0u64, req_data, 0u64),
3729                initiator: Some(rustenium_bidi_definitions::network::types::Initiator {
3730                    column_number: None,
3731                    line_number: None,
3732                    request: None,
3733                    stack_trace: None,
3734                    r#type: Some(rustenium_bidi_definitions::network::types::InitiatorType::Script),
3735                }),
3736            },
3737        };
3738        log.ingest_before_request_sent(&evt).await;
3739        let entry = log.first().await.unwrap();
3740        assert_eq!(entry.request.initiator_type, Some("script".into()));
3741    }
3742
3743    #[tokio::test]
3744    async fn test_ingest_response_with_headers() {
3745        let log = NetworkLog::new();
3746        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3747            .await;
3748
3749        let mut resp_data = make_response_completed("req-1", "https://a.com", 200);
3750        resp_data.params.response.headers =
3751            vec![rustenium_bidi_definitions::network::types::Header {
3752                name: "X-Response".into(),
3753                value: rustenium_bidi_definitions::network::types::BytesValue::StringValue(
3754                    rustenium_bidi_definitions::network::types::StringValue::new(
3755                        rustenium_bidi_definitions::network::types::StringValueType::String,
3756                        "world",
3757                    ),
3758                ),
3759            }];
3760        log.ingest_response_completed(&resp_data).await;
3761
3762        let entry = log.first().await.unwrap();
3763        assert_eq!(entry.response.as_ref().unwrap().headers.len(), 1);
3764        assert_eq!(
3765            entry.response.as_ref().unwrap().headers[0].name,
3766            "X-Response"
3767        );
3768        assert_eq!(entry.response.as_ref().unwrap().headers[0].value, "world");
3769    }
3770
3771    #[tokio::test]
3772    async fn test_ingest_response_from_cache() {
3773        let log = NetworkLog::new();
3774        log.ingest_before_request_sent(&make_before_request_sent("req-1", "https://a.com", "GET"))
3775            .await;
3776        let mut resp = make_response_completed("req-1", "https://a.com", 200);
3777        resp.params.response.from_cache = true;
3778        log.ingest_response_completed(&resp).await;
3779        let entry = log.first().await.unwrap();
3780        assert!(entry.response.as_ref().unwrap().from_cache);
3781    }
3782
3783    // ---------- empty log query behavior ----------
3784
3785    #[tokio::test]
3786    async fn test_first_on_empty_log() {
3787        let log = NetworkLog::new();
3788        assert!(log.first().await.is_none());
3789    }
3790
3791    #[tokio::test]
3792    async fn test_last_on_empty_log() {
3793        let log = NetworkLog::new();
3794        assert!(log.last().await.is_none());
3795    }
3796
3797    #[tokio::test]
3798    async fn test_nth_on_empty_log() {
3799        let log = NetworkLog::new();
3800        assert!(log.nth(0).await.is_none());
3801    }
3802
3803    #[tokio::test]
3804    async fn test_find_by_url_on_empty_log() {
3805        let log = NetworkLog::new();
3806        assert!(log.find_by_url("anything").await.is_none());
3807    }
3808
3809    #[tokio::test]
3810    async fn test_filter_on_empty_log() {
3811        let log = NetworkLog::new();
3812        assert!(log.filter(Filter::new()).await.is_empty());
3813    }
3814
3815    #[tokio::test]
3816    async fn test_count_on_empty_log() {
3817        let log = NetworkLog::new();
3818        assert_eq!(log.count(Filter::new()).await, 0);
3819    }
3820
3821    #[tokio::test]
3822    async fn test_completed_on_empty_log() {
3823        let log = NetworkLog::new();
3824        assert!(log.completed().await.is_empty());
3825    }
3826
3827    #[tokio::test]
3828    async fn test_endpoints_on_empty_log() {
3829        let log = NetworkLog::new();
3830        assert!(log.endpoints().await.is_empty());
3831    }
3832
3833    #[tokio::test]
3834    async fn test_hostnames_on_empty_log() {
3835        let log = NetworkLog::new();
3836        assert!(log.hostnames().await.is_empty());
3837    }
3838
3839    #[tokio::test]
3840    async fn test_distinct_methods_on_empty_log() {
3841        let log = NetworkLog::new();
3842        assert!(log.distinct_methods().await.is_empty());
3843    }
3844
3845    #[tokio::test]
3846    async fn test_distinct_statuses_on_empty_log() {
3847        let log = NetworkLog::new();
3848        assert!(log.distinct_statuses().await.is_empty());
3849    }
3850
3851    #[tokio::test]
3852    async fn test_total_bytes_on_empty_log() {
3853        let log = NetworkLog::new();
3854        assert_eq!(log.total_bytes_in().await, 0);
3855        assert_eq!(log.total_bytes_out().await, 0);
3856    }
3857
3858    #[tokio::test]
3859    async fn test_contains_id_on_empty_log() {
3860        let log = NetworkLog::new();
3861        assert!(!log.contains_id("anything").await);
3862    }
3863
3864    #[tokio::test]
3865    async fn test_remove_by_id_on_empty_log() {
3866        let log = NetworkLog::new();
3867        assert!(log.remove_by_id("anything").await.is_none());
3868    }
3869
3870    #[tokio::test]
3871    async fn test_clear_on_empty_log() {
3872        let log = NetworkLog::new();
3873        log.clear().await;
3874        assert!(log.is_empty().await);
3875    }
3876
3877    #[tokio::test]
3878    async fn test_subscribe_on_empty_log() {
3879        let log = NetworkLog::new();
3880        let mut rx = log.subscribe().await;
3881        assert!(rx.try_recv().is_err());
3882    }
3883
3884    // ---------- remove_by_id edge cases ----------
3885
3886    #[tokio::test]
3887    async fn test_remove_by_id_twice() {
3888        let log = NetworkLog::new();
3889        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3890        assert!(log.remove_by_id("1").await.is_some());
3891        assert!(log.remove_by_id("1").await.is_none());
3892    }
3893
3894    #[tokio::test]
3895    async fn test_remove_by_id_only_entry() {
3896        let log = NetworkLog::new();
3897        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3898        log.remove_by_id("1").await;
3899        assert!(log.is_empty().await);
3900        assert!(!log.contains_id("1").await);
3901    }
3902
3903    #[tokio::test]
3904    async fn test_concurrent_remove_by_id() {
3905        let log = NetworkLog::new();
3906        for i in 0..100 {
3907            push_request(
3908                &log,
3909                make_request(&format!("{}", i), "GET", "https://a.com"),
3910            )
3911            .await;
3912        }
3913        let mut handles = Vec::new();
3914        for i in 0..100 {
3915            let log = log.clone();
3916            handles.push(tokio::spawn(async move {
3917                log.remove_by_id(&format!("{}", i)).await
3918            }));
3919        }
3920        for h in handles {
3921            h.await.unwrap();
3922        }
3923        assert!(log.is_empty().await);
3924    }
3925
3926    // ---------- wait_for edge cases ----------
3927
3928    #[tokio::test]
3929    async fn test_wait_for_url_empty_substring() {
3930        let log = NetworkLog::new();
3931        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3932        let found = log
3933            .wait_for_url("", std::time::Duration::from_secs(1))
3934            .await;
3935        assert!(found.is_some());
3936    }
3937
3938    #[tokio::test]
3939    async fn test_wait_for_response_nonexistent_id() {
3940        let log = NetworkLog::new();
3941        push_request(&log, make_request("1", "GET", "https://a.com")).await;
3942        let found = log
3943            .wait_for_response("does-not-exist", std::time::Duration::from_millis(50))
3944            .await;
3945        assert!(found.is_none());
3946    }
3947
3948    // ---------- json_body edge cases ----------
3949
3950    #[tokio::test]
3951    async fn test_json_body_array() {
3952        let mut req = make_request("1", "POST", "https://a.com");
3953        req.post_data = Some("[1, 2, 3]".into());
3954        assert_eq!(req.json_body(), Some(serde_json::json!([1, 2, 3])));
3955    }
3956
3957    #[tokio::test]
3958    async fn test_json_body_nested() {
3959        let mut req = make_request("1", "POST", "https://a.com");
3960        req.post_data = Some(r#"{"outer": {"inner": [true, false]}}"#.into());
3961        assert_eq!(
3962            req.json_body(),
3963            Some(serde_json::json!({"outer": {"inner": [true, false]}}))
3964        );
3965    }
3966
3967    // ---------- curl edge cases ----------
3968
3969    #[tokio::test]
3970    async fn test_to_curl_delete_method() {
3971        let req = make_request("1", "DELETE", "https://api.example.com/resource/1");
3972        let entry = NetworkEntry {
3973            request: req,
3974            response: None,
3975            error: None,
3976        };
3977        assert!(entry.to_curl().starts_with("curl -X DELETE"));
3978    }
3979
3980    #[tokio::test]
3981    async fn test_to_curl_url_with_single_quotes() {
3982        let req = make_request("1", "GET", "https://example.com?foo='bar'");
3983        let entry = NetworkEntry {
3984            request: req,
3985            response: None,
3986            error: None,
3987        };
3988        let curl = entry.to_curl();
3989        assert!(curl.contains("'https://example.com?foo='\\''bar'\\'''"));
3990    }
3991
3992    #[tokio::test]
3993    async fn test_to_curl_header_with_single_quotes() {
3994        let req = make_request_with_headers(
3995            "1",
3996            "GET",
3997            "https://example.com",
3998            vec![CapturedHeader {
3999                name: "X-Quote".into(),
4000                value: "it's working".into(),
4001            }],
4002        );
4003        let entry = NetworkEntry {
4004            request: req,
4005            response: None,
4006            error: None,
4007        };
4008        let curl = entry.to_curl();
4009        assert!(curl.contains("-H 'X-Quote: it'\\''s working'"));
4010    }
4011
4012    // ---------- serialization of public structs ----------
4013
4014    #[tokio::test]
4015    async fn test_network_metrics_roundtrip() {
4016        let m = NetworkMetrics {
4017            requests_received: 10,
4018            responses_received: 8,
4019            errors_received: 2,
4020            entries_evicted: 5,
4021            pending_responses_dropped: 1,
4022            pending_errors_dropped: 0,
4023            broadcast_drops: 3,
4024            duplicate_responses: 1,
4025            duplicate_errors: 0,
4026            max_entries: 100,
4027        };
4028        let json = serde_json::to_string(&m).unwrap();
4029        let de: NetworkMetrics = serde_json::from_str(&json).unwrap();
4030        assert_eq!(de, m);
4031    }
4032
4033    // ---------- unicode / IDN ----------
4034
4035    #[tokio::test]
4036    async fn test_find_by_url_unicode() {
4037        let log = NetworkLog::new();
4038        push_request(&log, make_request("1", "GET", "https://例子.com/path")).await;
4039        assert!(log.find_by_url("例子").await.is_some());
4040    }
4041
4042    #[tokio::test]
4043    async fn test_hostnames_idn() {
4044        let log = NetworkLog::new();
4045        push_request(
4046            &log,
4047            make_request("1", "GET", "https://xn--fsq092h.com/path"),
4048        )
4049        .await;
4050        let h = log.hostnames().await;
4051        assert_eq!(h.len(), 1);
4052        assert!(h.contains(&"xn--fsq092h.com".into()));
4053    }
4054
4055    // ---------- total_bytes_out multibyte ----------
4056
4057    #[tokio::test]
4058    async fn test_total_bytes_out_multibyte() {
4059        let log = NetworkLog::new();
4060        let mut req = make_request("1", "POST", "https://a.com");
4061        req.post_data = Some("日本語".into()); // 9 bytes in UTF-8
4062        push_entry(&log, req, None, None).await;
4063        assert_eq!(log.total_bytes_out().await, 9);
4064    }
4065
4066    // ---------- completed with only errors ----------
4067
4068    #[tokio::test]
4069    async fn test_completed_only_errors() {
4070        let log = NetworkLog::new();
4071        push_entry(
4072            &log,
4073            make_request("1", "GET", "https://a.com"),
4074            None,
4075            Some(make_error("1", "https://a.com", "err")),
4076        )
4077        .await;
4078        push_entry(
4079            &log,
4080            make_request("2", "GET", "https://b.com"),
4081            None,
4082            Some(make_error("2", "https://b.com", "err")),
4083        )
4084        .await;
4085        let c = log.completed().await;
4086        assert_eq!(c.len(), 2);
4087    }
4088
4089    // ---------- distinct with all same ----------
4090
4091    #[tokio::test]
4092    async fn test_distinct_statuses_all_same() {
4093        let log = NetworkLog::new();
4094        for i in 0..5 {
4095            push_entry(
4096                &log,
4097                make_request(&format!("{}", i), "GET", "https://a.com"),
4098                Some(make_response(&format!("{}", i), 200, "https://a.com")),
4099                None,
4100            )
4101            .await;
4102        }
4103        assert_eq!(log.distinct_statuses().await, vec![200]);
4104    }
4105
4106    #[tokio::test]
4107    async fn test_distinct_methods_all_same() {
4108        let log = NetworkLog::new();
4109        for i in 0..5 {
4110            push_request(
4111                &log,
4112                make_request(&format!("{}", i), "POST", "https://a.com"),
4113            )
4114            .await;
4115        }
4116        assert_eq!(log.distinct_methods().await, vec!["POST"]);
4117    }
4118
4119    // ---------- clear then subscribe then push ----------
4120
4121    #[tokio::test]
4122    async fn test_clear_subscribe_push() {
4123        let log = NetworkLog::new();
4124        log.clear().await;
4125        let mut rx = log.subscribe().await;
4126        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4127        assert_eq!(rx.recv().await.unwrap().request.id, "1");
4128    }
4129
4130    // ---------- from_cache does not affect byte count ----------
4131
4132    #[tokio::test]
4133    async fn test_total_bytes_in_from_cache() {
4134        let log = NetworkLog::new();
4135        let mut resp = make_response("1", 200, "https://a.com");
4136        resp.from_cache = true;
4137        resp.body_size = Some(500);
4138        push_entry(
4139            &log,
4140            make_request("1", "GET", "https://a.com"),
4141            Some(resp),
4142            None,
4143        )
4144        .await;
4145        assert_eq!(log.total_bytes_in().await, 500);
4146    }
4147
4148    // ---------- new fixes ----------
4149
4150    #[tokio::test]
4151    async fn test_to_curl_produces_valid_shell_with_quotes() {
4152        let req = CapturedRequest {
4153            id: "1".into(),
4154            context: None,
4155            method: "POST".into(),
4156            url: "https://example.com".into(),
4157            headers: vec![CapturedHeader {
4158                name: "X-Quote".into(),
4159                value: "it's".into(),
4160            }],
4161            post_data: Some("data='val'".into()),
4162            timestamp: 0,
4163            destination: "document".into(),
4164            initiator_type: None,
4165            timing: CapturedTiming::default(),
4166            cookies: vec![],
4167        };
4168        let entry = NetworkEntry {
4169            request: req,
4170            response: None,
4171            error: None,
4172        };
4173        let curl = entry.to_curl();
4174        // Every single quote should be escaped as '\''
4175        // So the output should NOT contain any bare single quotes inside quoted regions
4176        // We verify by checking the escaping pattern is present
4177        assert!(curl.contains("it'\\''s"));
4178        assert!(curl.contains("data='\\''val'\\'''"));
4179    }
4180
4181    #[tokio::test]
4182    async fn test_save_as_har_postdata_uses_content_type_header() {
4183        let log = NetworkLog::new();
4184        let mut req = make_request("1", "POST", "https://api.example.com");
4185        req.post_data = Some(r#"{"key":"value"}"#.into());
4186        req.headers = vec![CapturedHeader {
4187            name: "Content-Type".into(),
4188            value: "application/vnd.api+json".into(),
4189        }];
4190        push_entry(
4191            &log,
4192            req,
4193            Some(make_response("1", 201, "https://api.example.com")),
4194            None,
4195        )
4196        .await;
4197
4198        let path = std::path::Path::new("/tmp/foxdriver_har_mime.har");
4199        log.save_as_har(path, None).await.unwrap();
4200        let content = std::fs::read_to_string(path).unwrap();
4201        // The postData should use the request's Content-Type
4202        assert!(content.contains("\"mimeType\": \"application/vnd.api+json\""));
4203    }
4204
4205    #[tokio::test]
4206    async fn test_save_as_har_postdata_defaults_octet_stream() {
4207        let log = NetworkLog::new();
4208        let mut req = make_request("1", "POST", "https://api.example.com");
4209        req.post_data = Some("raw bytes".into());
4210        // No Content-Type header
4211        push_entry(
4212            &log,
4213            req,
4214            Some(make_response("1", 201, "https://api.example.com")),
4215            None,
4216        )
4217        .await;
4218
4219        let path = std::path::Path::new("/tmp/foxdriver_har_default_mime.har");
4220        log.save_as_har(path, None).await.unwrap();
4221        let content = std::fs::read_to_string(path).unwrap();
4222        assert!(content.contains("\"mimeType\": \"application/octet-stream\""));
4223    }
4224
4225    #[tokio::test]
4226    async fn test_concurrent_reads_with_rwlock() {
4227        let log = NetworkLog::new();
4228        for i in 0..100 {
4229            push_request(
4230                &log,
4231                make_request(&format!("{}", i), "GET", "https://example.com"),
4232            )
4233            .await;
4234        }
4235        let mut handles = Vec::new();
4236        for _ in 0..20 {
4237            let log = log.clone();
4238            handles.push(tokio::spawn(async move {
4239                for _ in 0..50 {
4240                    let _ = log.len().await;
4241                    let _ = log.entries().await;
4242                    let _ = log.distinct_methods().await;
4243                    let _ = log.total_bytes_in().await;
4244                }
4245            }));
4246        }
4247        for h in handles {
4248            h.await.unwrap();
4249        }
4250        // No deadlock means success
4251        assert_eq!(log.len().await, 100);
4252    }
4253
4254    // ---------- new bounty features ----------
4255
4256    // ---------- convenience queries ----------
4257
4258    #[tokio::test]
4259    async fn test_find_by_status() {
4260        let log = NetworkLog::new();
4261        push_entry(
4262            &log,
4263            make_request("1", "GET", "https://a.com"),
4264            Some(make_response("1", 200, "https://a.com")),
4265            None,
4266        )
4267        .await;
4268        push_entry(
4269            &log,
4270            make_request("2", "GET", "https://b.com"),
4271            Some(make_response("2", 404, "https://b.com")),
4272            None,
4273        )
4274        .await;
4275        push_entry(
4276            &log,
4277            make_request("3", "GET", "https://c.com"),
4278            None,
4279            Some(make_error("3", "https://c.com", "err")),
4280        )
4281        .await;
4282        assert_eq!(log.find_by_status(200).await.len(), 1);
4283        assert_eq!(log.find_by_status(404).await.len(), 1);
4284        assert_eq!(log.find_by_status(500).await.len(), 0);
4285    }
4286
4287    #[tokio::test]
4288    async fn test_entries_since() {
4289        let log = NetworkLog::new();
4290        let mut req1 = make_request("1", "GET", "https://a.com");
4291        req1.timestamp = 1000;
4292        let mut req2 = make_request("2", "GET", "https://b.com");
4293        req2.timestamp = 2000;
4294        let mut req3 = make_request("3", "GET", "https://c.com");
4295        req3.timestamp = 3000;
4296        push_request(&log, req1).await;
4297        push_request(&log, req2).await;
4298        push_request(&log, req3).await;
4299        assert_eq!(log.entries_since(0).await.len(), 3);
4300        assert_eq!(log.entries_since(2000).await.len(), 2);
4301        assert_eq!(log.entries_since(3001).await.len(), 0);
4302    }
4303
4304    #[tokio::test]
4305    async fn test_last_n() {
4306        let log = NetworkLog::new();
4307        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4308        push_request(&log, make_request("2", "GET", "https://b.com")).await;
4309        push_request(&log, make_request("3", "GET", "https://c.com")).await;
4310        let last = log.last_n(2).await;
4311        assert_eq!(last.len(), 2);
4312        assert_eq!(last[0].request.id, "2");
4313        assert_eq!(last[1].request.id, "3");
4314        assert_eq!(log.last_n(10).await.len(), 3);
4315        assert!(log.last_n(0).await.is_empty());
4316    }
4317
4318    // ---------- security analysis ----------
4319
4320    // ---------- additional bounty analysis ----------
4321
4322    #[tokio::test]
4323    async fn test_unique_urls() {
4324        let log = NetworkLog::new();
4325        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4326        push_request(&log, make_request("2", "GET", "https://b.com")).await;
4327        push_request(&log, make_request("3", "GET", "https://a.com")).await;
4328        let urls = log.unique_urls().await;
4329        assert_eq!(urls.len(), 2);
4330        assert!(urls.contains(&"https://a.com".to_string()));
4331        assert!(urls.contains(&"https://b.com".to_string()));
4332    }
4333
4334    #[tokio::test]
4335    async fn test_unique_urls_empty() {
4336        let log = NetworkLog::new();
4337        assert!(log.unique_urls().await.is_empty());
4338    }
4339
4340    // ---------- reliability ----------
4341
4342    #[tokio::test]
4343    async fn test_broadcast_drops_tracked_when_no_receivers() {
4344        let log = NetworkLog::new();
4345        // No subscribers active
4346        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4347        let m = log.metrics().await;
4348        assert_eq!(m.broadcast_drops, 1);
4349    }
4350
4351    #[tokio::test]
4352    async fn test_broadcast_no_drop_when_receiver_active() {
4353        let log = NetworkLog::new();
4354        let _rx = log.subscribe().await;
4355        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4356        let m = log.metrics().await;
4357        assert_eq!(m.broadcast_drops, 0);
4358    }
4359
4360    #[tokio::test]
4361    async fn test_pending_count_and_drain() {
4362        let log = NetworkLog::new();
4363        // Inject a response before its request (out-of-order)
4364        {
4365            let mut inner = log.inner.write().await;
4366            inner.pending_responses.insert(
4367                "orphan-1".into(),
4368                make_response("orphan-1", 200, "https://a.com"),
4369            );
4370            inner.pending_errors.insert(
4371                "orphan-2".into(),
4372                make_error("orphan-2", "https://b.com", "err"),
4373            );
4374        }
4375        assert_eq!(log.pending_count().await, 2);
4376
4377        let (responses, errors) = log.drain_pending().await;
4378        assert_eq!(responses.len(), 1);
4379        assert_eq!(errors.len(), 1);
4380        assert_eq!(log.pending_count().await, 0);
4381    }
4382
4383    #[tokio::test]
4384    async fn test_pending_count_empty() {
4385        let log = NetworkLog::new();
4386        assert_eq!(log.pending_count().await, 0);
4387        let (r, e) = log.drain_pending().await;
4388        assert!(r.is_empty() && e.is_empty());
4389    }
4390
4391    #[tokio::test]
4392    async fn test_retain_filters_entries() {
4393        let log = NetworkLog::new();
4394        push_request(&log, make_request("1", "GET", "https://keep.com")).await;
4395        push_request(&log, make_request("2", "GET", "https://drop.com")).await;
4396        push_request(&log, make_request("3", "GET", "https://keep.com/path")).await;
4397        log.retain(|e| e.request.url.contains("keep")).await;
4398        assert_eq!(log.len().await, 2);
4399        assert!(log.contains_id("1").await);
4400        assert!(!log.contains_id("2").await);
4401        assert!(log.contains_id("3").await);
4402    }
4403
4404    #[tokio::test]
4405    async fn test_retain_rebuilds_indices() {
4406        let log = NetworkLog::new();
4407        for i in 0..5 {
4408            push_request(
4409                &log,
4410                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4411            )
4412            .await;
4413        }
4414        log.retain(|e| e.request.id.parse::<i32>().unwrap() % 2 == 0)
4415            .await;
4416        assert_eq!(log.len().await, 3);
4417        assert!(log.contains_id("0").await);
4418        assert!(log.contains_id("2").await);
4419        assert!(log.contains_id("4").await);
4420    }
4421
4422    #[tokio::test]
4423    async fn test_deduplication_guard_ignores_duplicate_request_id() {
4424        let log = NetworkLog::new();
4425        push_request(&log, make_request("1", "GET", "https://first.com")).await;
4426        push_request(&log, make_request("1", "GET", "https://second.com")).await;
4427        assert_eq!(log.len().await, 1);
4428        assert_eq!(log.first().await.unwrap().request.url, "https://first.com");
4429    }
4430
4431    #[tokio::test]
4432    async fn test_duplicate_response_tracked_in_metrics() {
4433        let log = NetworkLog::new();
4434        push_entry(
4435            &log,
4436            make_request("1", "GET", "https://a.com"),
4437            Some(make_response("1", 200, "https://a.com")),
4438            None,
4439        )
4440        .await;
4441        // Simulate a second response for the same request
4442        {
4443            let mut inner = log.inner.write().await;
4444            if let Some(idx) = inner.by_id.get("1").copied() {
4445                let mut new_entry = (*inner.entries[idx]).clone();
4446                new_entry.response = Some(make_response("1", 201, "https://a.com"));
4447                inner.entries[idx] = Arc::new(new_entry);
4448            }
4449        }
4450        // We can't easily trigger ingest_response_completed without real BiDi events,
4451        // but we verify the metric field exists and is initialized to 0
4452        let m = log.metrics().await;
4453        assert_eq!(m.duplicate_responses, 0); // No duplicate through normal path yet
4454    }
4455
4456    #[tokio::test]
4457    async fn test_request_ids_returns_all_ids() {
4458        let log = NetworkLog::new();
4459        push_request(&log, make_request("a", "GET", "https://a.com")).await;
4460        push_request(&log, make_request("b", "GET", "https://b.com")).await;
4461        let ids = log.request_ids().await;
4462        assert_eq!(ids, vec!["a", "b"]);
4463    }
4464
4465    #[tokio::test]
4466    async fn test_has_response_and_has_error() {
4467        let log = NetworkLog::new();
4468        push_entry(
4469            &log,
4470            make_request("1", "GET", "https://a.com"),
4471            Some(make_response("1", 200, "https://a.com")),
4472            None,
4473        )
4474        .await;
4475        push_entry(
4476            &log,
4477            make_request("2", "GET", "https://b.com"),
4478            None,
4479            Some(make_error("2", "https://b.com", "err")),
4480        )
4481        .await;
4482        push_request(&log, make_request("3", "GET", "https://c.com")).await;
4483        assert!(log.has_response("1").await);
4484        assert!(!log.has_error("1").await);
4485        assert!(!log.has_response("2").await);
4486        assert!(log.has_error("2").await);
4487        assert!(!log.has_response("3").await);
4488        assert!(!log.has_error("3").await);
4489        assert!(!log.has_response("missing").await);
4490        assert!(!log.has_error("missing").await);
4491    }
4492
4493    // ---------- invariant tests ----------
4494
4495    #[tokio::test]
4496    async fn test_invariant_len_equals_entries_len() {
4497        let log = NetworkLog::new();
4498        for i in 0..50 {
4499            push_request(
4500                &log,
4501                make_request(&format!("{}", i), "GET", "https://x.com"),
4502            )
4503            .await;
4504            assert_eq!(log.len().await, log.entries().await.len());
4505        }
4506    }
4507
4508    #[tokio::test]
4509    async fn test_invariant_is_empty_iff_len_zero() {
4510        let log = NetworkLog::new();
4511        assert_eq!(log.is_empty().await, log.len().await == 0);
4512        push_request(&log, make_request("1", "GET", "https://x.com")).await;
4513        assert_eq!(log.is_empty().await, log.len().await == 0);
4514        log.clear().await;
4515        assert_eq!(log.is_empty().await, log.len().await == 0);
4516    }
4517
4518    #[tokio::test]
4519    async fn test_invariant_by_id_indices_are_valid() {
4520        let log = NetworkLog::new();
4521        for i in 0..100 {
4522            push_request(
4523                &log,
4524                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4525            )
4526            .await;
4527        }
4528        let inner = log.inner.read().await;
4529        for (id, idx) in &inner.by_id {
4530            assert!(
4531                *idx < inner.entries.len(),
4532                "id {} has invalid index {}",
4533                id,
4534                idx
4535            );
4536            assert_eq!(inner.entries[*idx].request.id, *id);
4537        }
4538    }
4539
4540    #[tokio::test]
4541    async fn test_invariant_no_duplicate_ids() {
4542        let log = NetworkLog::new();
4543        for i in 0..100 {
4544            push_request(
4545                &log,
4546                make_request(&format!("{}", i), "GET", "https://x.com"),
4547            )
4548            .await;
4549        }
4550        let inner = log.inner.read().await;
4551        let mut seen = std::collections::HashSet::new();
4552        for e in &inner.entries {
4553            assert!(seen.insert(&e.request.id), "duplicate id {}", e.request.id);
4554        }
4555    }
4556
4557    #[tokio::test]
4558    async fn test_invariant_first_and_last_consistent() {
4559        let log = NetworkLog::new();
4560        assert_eq!(log.first().await, None);
4561        assert_eq!(log.last().await, None);
4562        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4563        push_request(&log, make_request("2", "GET", "https://b.com")).await;
4564        let entries = log.entries().await;
4565        assert_eq!(
4566            log.first().await.as_deref(),
4567            entries.first().map(Arc::as_ref)
4568        );
4569        assert_eq!(log.last().await.as_deref(), entries.last().map(Arc::as_ref));
4570    }
4571
4572    #[tokio::test]
4573    async fn test_invariant_filter_count_consistent() {
4574        let log = NetworkLog::new();
4575        for i in 0..20 {
4576            let status = if i % 2 == 0 { 200 } else { 404 };
4577            push_entry(
4578                &log,
4579                make_request(&format!("{}", i), "GET", "https://x.com"),
4580                Some(make_response(&format!("{}", i), status, "https://x.com")),
4581                None,
4582            )
4583            .await;
4584        }
4585        let f = Filter::new().status_range(200..=200);
4586        let filtered = log.filter(f.clone()).await;
4587        let count = log.count(f).await;
4588        assert_eq!(filtered.len(), count);
4589    }
4590
4591    #[tokio::test]
4592    async fn test_invariant_metrics_monotonic() {
4593        let log = NetworkLog::new();
4594        let m0 = log.metrics().await;
4595        push_request(&log, make_request("1", "GET", "https://a.com")).await;
4596        let m1 = log.metrics().await;
4597        assert!(m1.requests_received >= m0.requests_received);
4598        assert!(m1.responses_received >= m0.responses_received);
4599        assert!(m1.errors_received >= m0.errors_received);
4600    }
4601
4602    #[tokio::test]
4603    async fn test_invariant_pending_count_bounded() {
4604        let log = NetworkLog::with_limits(100, 10);
4605        // Insert 20 orphaned pending responses directly (bypassing capping)
4606        {
4607            let mut inner = log.inner.write().await;
4608            for i in 0..20 {
4609                inner.pending_responses.insert(
4610                    format!("orphan-{}", i),
4611                    make_response(&format!("{}", i), 200, "https://x.com"),
4612                );
4613            }
4614        }
4615        // Direct insertion bypasses capping; verify drain_pending can clean them
4616        assert_eq!(log.pending_count().await, 20);
4617        let (r, _) = log.drain_pending().await;
4618        assert_eq!(r.len(), 20);
4619        assert_eq!(log.pending_count().await, 0);
4620    }
4621
4622    #[tokio::test]
4623    async fn test_invariant_after_clear_all_empty() {
4624        let log = NetworkLog::new();
4625        for i in 0..50 {
4626            push_request(
4627                &log,
4628                make_request(&format!("{}", i), "GET", "https://x.com"),
4629            )
4630            .await;
4631        }
4632        log.clear().await;
4633        assert!(log.is_empty().await);
4634        assert_eq!(log.pending_count().await, 0);
4635        assert!(!log.contains_id("1").await);
4636        let inner = log.inner.read().await;
4637        assert!(inner.by_id.is_empty());
4638    }
4639
4640    #[tokio::test]
4641    async fn test_invariant_after_remove_id_not_present() {
4642        let log = NetworkLog::new();
4643        for i in 0..50 {
4644            push_request(
4645                &log,
4646                make_request(&format!("{}", i), "GET", "https://x.com"),
4647            )
4648            .await;
4649        }
4650        log.remove_by_id("25").await;
4651        assert!(!log.contains_id("25").await);
4652        let inner = log.inner.read().await;
4653        assert!(!inner.by_id.contains_key("25"));
4654        assert_eq!(inner.entries.len(), 49);
4655        // Verify all remaining indices are valid
4656        for (id, idx) in &inner.by_id {
4657            assert!(inner.entries[*idx].request.id == *id);
4658        }
4659    }
4660
4661    #[tokio::test]
4662    async fn test_invariant_retain_all_satisfy_predicate() {
4663        let log = NetworkLog::new();
4664        for i in 0..50 {
4665            push_request(
4666                &log,
4667                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i % 3)),
4668            )
4669            .await;
4670        }
4671        log.retain(|e| e.request.url.contains("0.com") || e.request.url.contains("1.com"))
4672            .await;
4673        for e in log.entries().await {
4674            assert!(!e.request.url.contains("2.com"));
4675        }
4676        assert_eq!(log.len().await, 34); // 0,1 out of 0,1,2 = 2/3
4677    }
4678
4679    #[tokio::test]
4680    async fn test_invariant_nth_matches_entries_index() {
4681        let log = NetworkLog::new();
4682        for i in 0..20 {
4683            push_request(
4684                &log,
4685                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
4686            )
4687            .await;
4688        }
4689        let entries = log.entries().await;
4690        for (i, entry) in entries.iter().enumerate() {
4691            assert_eq!(log.nth(i).await.as_deref(), Some(entry.as_ref()));
4692        }
4693        assert!(log.nth(100).await.is_none());
4694    }
4695
4696    #[tokio::test]
4697    async fn test_invariant_last_n_order_preserved() {
4698        let log = NetworkLog::new();
4699        for i in 0..10 {
4700            push_request(
4701                &log,
4702                make_request(&format!("{}", i), "GET", "https://x.com"),
4703            )
4704            .await;
4705        }
4706        let last = log.last_n(3).await;
4707        assert_eq!(last.len(), 3);
4708        assert_eq!(last[0].request.id, "7");
4709        assert_eq!(last[1].request.id, "8");
4710        assert_eq!(last[2].request.id, "9");
4711    }
4712
4713    #[tokio::test]
4714    async fn test_invariant_entries_since_time_based() {
4715        let log = NetworkLog::new();
4716        for i in 0..10 {
4717            let mut req = make_request(&format!("{}", i), "GET", "https://x.com");
4718            req.timestamp = i as u64 * 1000;
4719            push_request(&log, req).await;
4720        }
4721        let since = log.entries_since(5000).await;
4722        assert_eq!(since.len(), 5); // 5,6,7,8,9
4723        for e in &since {
4724            assert!(e.request.timestamp >= 5000);
4725        }
4726    }
4727
4728    // ---------- concurrent stress tests ----------
4729
4730    #[tokio::test]
4731    async fn test_stress_concurrent_ingestion_and_query() {
4732        let log = NetworkLog::new();
4733        let mut handles = Vec::new();
4734
4735        // 10 ingestion tasks
4736        for t in 0..10 {
4737            let log = log.clone();
4738            handles.push(tokio::spawn(async move {
4739                for i in 0..100 {
4740                    let id = format!("t{}-{}", t, i);
4741                    push_request(&log, make_request(&id, "GET", "https://x.com")).await;
4742                }
4743            }));
4744        }
4745
4746        // 10 query tasks
4747        for _ in 0..10 {
4748            let log = log.clone();
4749            handles.push(tokio::spawn(async move {
4750                for _ in 0..100 {
4751                    let _ = log.len().await;
4752                    let _ = log.entries().await;
4753                    let _ = log.first().await;
4754                    let _ = log.last().await;
4755                    let _ = log.metrics().await;
4756                    tokio::task::yield_now().await;
4757                }
4758            }));
4759        }
4760
4761        for h in handles {
4762            h.await.unwrap();
4763        }
4764
4765        assert_eq!(log.len().await, 1000);
4766        let inner = log.inner.read().await;
4767        assert_eq!(inner.by_id.len(), 1000);
4768    }
4769
4770    #[tokio::test]
4771    async fn test_stress_concurrent_subscribers() {
4772        let log = NetworkLog::new();
4773        let mut rxs = Vec::new();
4774        for _ in 0..50 {
4775            rxs.push(log.subscribe().await);
4776        }
4777
4778        for i in 0..100 {
4779            push_request(
4780                &log,
4781                make_request(&format!("{}", i), "GET", "https://x.com"),
4782            )
4783            .await;
4784        }
4785
4786        // Each subscriber should have received at most 100 entries
4787        // (some may have been dropped if the channel filled)
4788        for mut rx in rxs {
4789            let mut count = 0;
4790            while rx.try_recv().is_ok() {
4791                count += 1;
4792            }
4793            assert!(count <= 100);
4794        }
4795    }
4796
4797    #[tokio::test]
4798    async fn test_stress_eviction_under_load() {
4799        let log = NetworkLog::with_limits(100, 10);
4800        for i in 0..500 {
4801            push_request(
4802                &log,
4803                make_request(&format!("{}", i), "GET", "https://x.com"),
4804            )
4805            .await;
4806        }
4807        assert_eq!(log.len().await, 100);
4808        let m = log.metrics().await;
4809        assert!(m.entries_evicted > 0);
4810    }
4811
4812    #[tokio::test]
4813    async fn test_stress_retain_under_load() {
4814        let log = NetworkLog::new();
4815        for i in 0..1000 {
4816            push_request(
4817                &log,
4818                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i % 10)),
4819            )
4820            .await;
4821        }
4822        log.retain(|e| e.request.url.contains("0.com") || e.request.url.contains("1.com"))
4823            .await;
4824        assert_eq!(log.len().await, 200);
4825        let inner = log.inner.read().await;
4826        assert_eq!(inner.by_id.len(), 200);
4827    }
4828
4829    #[tokio::test]
4830    async fn test_stress_memory_estimate_grows_with_entries() {
4831        let log = NetworkLog::new();
4832        let m0 = log.memory_estimate().await;
4833        for i in 0..100 {
4834            let mut req = make_request(&format!("{}", i), "POST", "https://x.com");
4835            req.post_data = Some("x".repeat(1000));
4836            push_request(&log, req).await;
4837        }
4838        let m1 = log.memory_estimate().await;
4839        assert!(m1 > m0);
4840    }
4841
4842    // ---------- HAR validation ----------
4843
4844    #[tokio::test]
4845    async fn test_har_is_valid_json() {
4846        let log = NetworkLog::new();
4847        push_entry(
4848            &log,
4849            make_request("1", "GET", "https://a.com"),
4850            Some(make_response("1", 200, "https://a.com")),
4851            None,
4852        )
4853        .await;
4854        let path = std::path::Path::new("/tmp/foxdriver_har_valid.json");
4855        log.save_as_har(path, Some("test")).await.unwrap();
4856        let content = std::fs::read_to_string(path).unwrap();
4857        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4858        assert_eq!(parsed["log"]["version"], "1.2");
4859        assert!(parsed["log"]["entries"].is_array());
4860        assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 1);
4861    }
4862
4863    #[tokio::test]
4864    async fn test_har_has_required_fields() {
4865        let log = NetworkLog::new();
4866        push_entry(
4867            &log,
4868            make_request("1", "POST", "https://api.example.com"),
4869            Some(make_response("1", 201, "https://api.example.com")),
4870            None,
4871        )
4872        .await;
4873        let path = std::path::Path::new("/tmp/foxdriver_har_fields.json");
4874        log.save_as_har(path, None).await.unwrap();
4875        let content = std::fs::read_to_string(path).unwrap();
4876        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4877        let entry = &parsed["log"]["entries"][0];
4878        assert!(entry["startedDateTime"].is_string());
4879        assert!(entry["request"]["method"].is_string());
4880        assert!(entry["request"]["url"].is_string());
4881        assert!(entry["response"]["status"].is_number());
4882        assert!(entry["response"]["statusText"].is_string());
4883        assert!(entry["timings"].is_object());
4884    }
4885
4886    #[tokio::test]
4887    async fn test_har_empty_log_has_zero_entries() {
4888        let log = NetworkLog::new();
4889        let path = std::path::Path::new("/tmp/foxdriver_har_empty_valid.json");
4890        log.save_as_har(path, None).await.unwrap();
4891        let content = std::fs::read_to_string(path).unwrap();
4892        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4893        assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 0);
4894    }
4895
4896    #[tokio::test]
4897    async fn test_har_post_data_present() {
4898        let log = NetworkLog::new();
4899        let mut req = make_request("1", "POST", "https://api.example.com");
4900        req.post_data = Some(r#"{"key":"value"}"#.into());
4901        req.headers = vec![CapturedHeader {
4902            name: "Content-Type".into(),
4903            value: "application/json".into(),
4904        }];
4905        push_entry(
4906            &log,
4907            req,
4908            Some(make_response("1", 200, "https://api.example.com")),
4909            None,
4910        )
4911        .await;
4912        let path = std::path::Path::new("/tmp/foxdriver_har_postdata.json");
4913        log.save_as_har(path, None).await.unwrap();
4914        let content = std::fs::read_to_string(path).unwrap();
4915        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
4916        let post_data = &parsed["log"]["entries"][0]["request"]["postData"];
4917        assert!(post_data["text"].is_string());
4918        assert!(post_data["mimeType"].is_string());
4919    }
4920
4921    // ---------- BiDi ingestion edge cases ----------
4922
4923    #[tokio::test]
4924    async fn test_ingest_response_for_unknown_request_stashes_pending() {
4925        let log = NetworkLog::new();
4926        // Directly stash a pending response
4927        {
4928            let mut inner = log.inner.write().await;
4929            inner.pending_responses.insert(
4930                "future-req".into(),
4931                make_response("future-req", 200, "https://a.com"),
4932            );
4933        }
4934        assert_eq!(log.pending_count().await, 1);
4935        // Manually reconcile: push_entry does not check pending maps
4936        {
4937            let mut inner = log.inner.write().await;
4938            let resp = inner.pending_responses.remove("future-req");
4939            let entry = Arc::new(NetworkEntry {
4940                request: make_request("future-req", "GET", "https://a.com"),
4941                response: resp,
4942                error: None,
4943            });
4944            inner.push_entry(entry);
4945        }
4946        assert_eq!(log.pending_count().await, 0);
4947        assert!(log.has_response("future-req").await);
4948    }
4949
4950    #[tokio::test]
4951    async fn test_ingest_error_for_unknown_request_stashes_pending() {
4952        let log = NetworkLog::new();
4953        {
4954            let mut inner = log.inner.write().await;
4955            inner.pending_errors.insert(
4956                "future-err".into(),
4957                make_error("future-err", "https://a.com", "timeout"),
4958            );
4959        }
4960        assert_eq!(log.pending_count().await, 1);
4961        // Manually reconcile
4962        {
4963            let mut inner = log.inner.write().await;
4964            let err = inner.pending_errors.remove("future-err");
4965            let entry = Arc::new(NetworkEntry {
4966                request: make_request("future-err", "GET", "https://a.com"),
4967                response: None,
4968                error: err,
4969            });
4970            inner.push_entry(entry);
4971        }
4972        assert_eq!(log.pending_count().await, 0);
4973        assert!(log.has_error("future-err").await);
4974    }
4975
4976    #[tokio::test]
4977    async fn test_ingest_duplicate_request_ignored() {
4978        let log = NetworkLog::new();
4979        push_request(&log, make_request("1", "GET", "https://first.com")).await;
4980        push_request(&log, make_request("1", "GET", "https://second.com")).await;
4981        assert_eq!(log.len().await, 1);
4982        assert_eq!(log.first().await.unwrap().request.url, "https://first.com");
4983    }
4984
4985    // ---------- boundary conditions ----------
4986
4987    #[tokio::test]
4988    async fn test_filter_empty_log() {
4989        let log = NetworkLog::new();
4990        let f = Filter::new().method("GET").status_range(200..=200);
4991        assert!(log.filter(f).await.is_empty());
4992        assert_eq!(log.count(Filter::new()).await, 0);
4993    }
4994
4995    #[tokio::test]
4996    async fn test_find_by_url_empty_log() {
4997        let log = NetworkLog::new();
4998        assert!(log.find_by_url("anything").await.is_none());
4999    }
5000
5001    #[tokio::test]
5002    async fn test_find_by_url_regex_empty_log() {
5003        let log = NetworkLog::new();
5004        let re = regex::Regex::new(".*").unwrap();
5005        assert!(log.find_by_url_regex(&re).await.is_none());
5006    }
5007
5008    #[tokio::test]
5009    async fn test_endpoints_empty_log() {
5010        let log = NetworkLog::new();
5011        assert!(log.endpoints().await.is_empty());
5012    }
5013
5014    #[tokio::test]
5015    async fn test_hostnames_empty_log() {
5016        let log = NetworkLog::new();
5017        assert!(log.hostnames().await.is_empty());
5018    }
5019
5020    #[tokio::test]
5021    async fn test_distinct_methods_empty_log_returns_empty() {
5022        let log = NetworkLog::new();
5023        assert!(log.distinct_methods().await.is_empty());
5024    }
5025
5026    #[tokio::test]
5027    async fn test_distinct_statuses_empty_log() {
5028        let log = NetworkLog::new();
5029        assert!(log.distinct_statuses().await.is_empty());
5030    }
5031
5032    #[tokio::test]
5033    async fn test_total_bytes_empty_log() {
5034        let log = NetworkLog::new();
5035        assert_eq!(log.total_bytes_in().await, 0);
5036        assert_eq!(log.total_bytes_out().await, 0);
5037    }
5038
5039    #[tokio::test]
5040    async fn test_nth_out_of_bounds() {
5041        let log = NetworkLog::new();
5042        assert!(log.nth(0).await.is_none());
5043        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5044        assert!(log.nth(1).await.is_none());
5045        assert!(log.nth(100).await.is_none());
5046    }
5047
5048    #[tokio::test]
5049    async fn test_last_n_greater_than_len() {
5050        let log = NetworkLog::new();
5051        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5052        assert_eq!(log.last_n(100).await.len(), 1);
5053    }
5054
5055    #[tokio::test]
5056    async fn test_find_by_status_no_matches() {
5057        let log = NetworkLog::new();
5058        push_entry(
5059            &log,
5060            make_request("1", "GET", "https://a.com"),
5061            Some(make_response("1", 200, "https://a.com")),
5062            None,
5063        )
5064        .await;
5065        assert!(log.find_by_status(404).await.is_empty());
5066    }
5067
5068    #[tokio::test]
5069    async fn test_entries_since_future_timestamp() {
5070        let log = NetworkLog::new();
5071        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5072        assert!(log.entries_since(u64::MAX).await.is_empty());
5073    }
5074
5075    #[tokio::test]
5076    async fn test_contains_id_false() {
5077        let log = NetworkLog::new();
5078        assert!(!log.contains_id("nonexistent").await);
5079    }
5080
5081    #[tokio::test]
5082    async fn test_remove_by_id_nonexistent() {
5083        let log = NetworkLog::new();
5084        assert!(log.remove_by_id("nonexistent").await.is_none());
5085    }
5086
5087    #[tokio::test]
5088    async fn test_metrics_on_empty_log() {
5089        let log = NetworkLog::new();
5090        let m = log.metrics().await;
5091        assert_eq!(m.requests_received, 0);
5092        assert_eq!(m.responses_received, 0);
5093        assert_eq!(m.errors_received, 0);
5094        assert_eq!(m.entries_evicted, 0);
5095        assert_eq!(m.broadcast_drops, 0);
5096        assert_eq!(m.duplicate_responses, 0);
5097        assert_eq!(m.duplicate_errors, 0);
5098    }
5099
5100    #[tokio::test]
5101    async fn test_memory_estimate_grows_with_entries() {
5102        let log = NetworkLog::new();
5103        let empty_est = log.memory_estimate().await;
5104        // The key invariant is that the estimate increases as entries are added.
5105        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5106        let grown_est = log.memory_estimate().await;
5107        assert!(
5108            grown_est > empty_est,
5109            "estimate must grow: empty={empty_est}, after one entry={grown_est}"
5110        );
5111    }
5112
5113    #[tokio::test]
5114    async fn test_with_limits_zero_zero() {
5115        let log = NetworkLog::with_limits(0, 0);
5116        for i in 0..100 {
5117            push_request(
5118                &log,
5119                make_request(&format!("{}", i), "GET", "https://x.com"),
5120            )
5121            .await;
5122        }
5123        assert_eq!(log.len().await, 100);
5124    }
5125
5126    #[tokio::test]
5127    async fn test_with_limits_small() {
5128        let log = NetworkLog::with_limits(5, 5);
5129        for i in 0..20 {
5130            push_request(
5131                &log,
5132                make_request(&format!("{}", i), "GET", "https://x.com"),
5133            )
5134            .await;
5135        }
5136        assert!(log.len().await <= 5);
5137    }
5138
5139    #[tokio::test]
5140    async fn test_wait_for_url_timeout_zero() {
5141        let log = NetworkLog::new();
5142        let result = log
5143            .wait_for_url("nonexistent", std::time::Duration::from_secs(0))
5144            .await;
5145        assert!(result.is_none());
5146    }
5147
5148    #[tokio::test]
5149    async fn test_wait_for_response_timeout_zero() {
5150        let log = NetworkLog::new();
5151        let result = log
5152            .wait_for_response("nonexistent", std::time::Duration::from_secs(0))
5153            .await;
5154        assert!(result.is_none());
5155    }
5156
5157    // ---------- filter edge cases ----------
5158
5159    #[tokio::test]
5160    async fn test_filter_header_case_insensitive() {
5161        let log = NetworkLog::new();
5162        let mut req = make_request("1", "GET", "https://a.com");
5163        req.headers = vec![CapturedHeader {
5164            name: "X-Custom".into(),
5165            value: "SecretValue".into(),
5166        }];
5167        push_request(&log, req).await;
5168        assert_eq!(
5169            log.filter(Filter::new().header("x-custom", "secretvalue"))
5170                .await
5171                .len(),
5172            1
5173        );
5174        assert_eq!(
5175            log.filter(Filter::new().header("X-CUSTOM", "SECRETVALUE"))
5176                .await
5177                .len(),
5178            1
5179        );
5180    }
5181
5182    #[tokio::test]
5183    async fn test_filter_combined_conditions() {
5184        let log = NetworkLog::new();
5185        push_entry(
5186            &log,
5187            make_request("1", "POST", "https://api.example.com"),
5188            Some(make_response("1", 201, "https://api.example.com")),
5189            None,
5190        )
5191        .await;
5192        push_entry(
5193            &log,
5194            make_request("2", "GET", "https://api.example.com"),
5195            Some(make_response("2", 200, "https://api.example.com")),
5196            None,
5197        )
5198        .await;
5199        push_entry(
5200            &log,
5201            make_request("3", "POST", "https://api.example.com"),
5202            Some(make_response("3", 500, "https://api.example.com")),
5203            None,
5204        )
5205        .await;
5206        let f = Filter::new().method("POST").status_range(200..=299);
5207        assert_eq!(log.filter(f).await.len(), 1);
5208    }
5209
5210    #[tokio::test]
5211    async fn test_filter_with_response_only() {
5212        let log = NetworkLog::new();
5213        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5214        push_entry(
5215            &log,
5216            make_request("2", "GET", "https://b.com"),
5217            Some(make_response("2", 200, "https://b.com")),
5218            None,
5219        )
5220        .await;
5221        assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
5222        assert_eq!(log.filter(Filter::new().without_response()).await.len(), 1);
5223    }
5224
5225    #[tokio::test]
5226    async fn test_filter_with_error_only() {
5227        let log = NetworkLog::new();
5228        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5229        push_entry(
5230            &log,
5231            make_request("2", "GET", "https://b.com"),
5232            None,
5233            Some(make_error("2", "https://b.com", "err")),
5234        )
5235        .await;
5236        assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
5237    }
5238
5239    // ---------- curl generation edge cases ----------
5240
5241    #[tokio::test]
5242    async fn test_to_curl_empty_body() {
5243        let req = make_request("1", "GET", "https://a.com");
5244        let entry = NetworkEntry {
5245            request: req,
5246            response: None,
5247            error: None,
5248        };
5249        let curl = entry.to_curl();
5250        assert!(!curl.contains("-d"));
5251        assert!(curl.contains("curl -X GET"));
5252    }
5253
5254    #[tokio::test]
5255    async fn test_to_curl_unicode_url() {
5256        let req = make_request("1", "GET", "https://例え.jp/テスト");
5257        let entry = NetworkEntry {
5258            request: req,
5259            response: None,
5260            error: None,
5261        };
5262        let curl = entry.to_curl();
5263        assert!(curl.contains("https://例え.jp/テスト"));
5264    }
5265
5266    #[tokio::test]
5267    async fn test_to_curl_strips_auto_headers() {
5268        let mut req = make_request("1", "GET", "https://a.com");
5269        req.headers = vec![
5270            CapturedHeader {
5271                name: "Host".into(),
5272                value: "a.com".into(),
5273            },
5274            CapturedHeader {
5275                name: "Accept-Encoding".into(),
5276                value: "gzip".into(),
5277            },
5278            CapturedHeader {
5279                name: "Connection".into(),
5280                value: "keep-alive".into(),
5281            },
5282            CapturedHeader {
5283                name: "X-Custom".into(),
5284                value: "value".into(),
5285            },
5286        ];
5287        let entry = NetworkEntry {
5288            request: req,
5289            response: None,
5290            error: None,
5291        };
5292        let curl = entry.to_curl();
5293        assert!(!curl.contains("Host:"));
5294        assert!(!curl.contains("Accept-Encoding:"));
5295        assert!(!curl.contains("Connection:"));
5296        assert!(curl.contains("X-Custom:"));
5297    }
5298
5299    // ---------- query params ----------
5300
5301    #[tokio::test]
5302    async fn test_query_params_multiple() {
5303        let req = make_request("1", "GET", "https://a.com?a=1&b=2&c=3");
5304        let params = req.query_params().unwrap();
5305        assert_eq!(params.len(), 3);
5306        assert!(params.contains(&("a".into(), "1".into())));
5307        assert!(params.contains(&("b".into(), "2".into())));
5308        assert!(params.contains(&("c".into(), "3".into())));
5309    }
5310
5311    #[tokio::test]
5312    async fn test_json_body_invalid() {
5313        let mut req = make_request("1", "POST", "https://a.com");
5314        req.post_data = Some("not json".into());
5315        assert!(req.json_body().is_none());
5316    }
5317
5318    #[tokio::test]
5319    async fn test_json_body_valid() {
5320        let mut req = make_request("1", "POST", "https://a.com");
5321        req.post_data = Some(r#"{"key":"value"}"#.into());
5322        let json = req.json_body().unwrap();
5323        assert_eq!(json["key"], "value");
5324    }
5325
5326    // ---------- request/response header lookup ----------
5327
5328    #[tokio::test]
5329    async fn test_request_header_case_insensitive() {
5330        let mut req = make_request("1", "GET", "https://a.com");
5331        req.headers = vec![CapturedHeader {
5332            name: "Content-Type".into(),
5333            value: "application/json".into(),
5334        }];
5335        assert_eq!(req.request_header("content-type"), Some("application/json"));
5336        assert_eq!(req.request_header("Content-Type"), Some("application/json"));
5337        assert_eq!(req.request_header("CONTENT-TYPE"), Some("application/json"));
5338    }
5339
5340    #[tokio::test]
5341    async fn test_request_header_missing() {
5342        let req = make_request("1", "GET", "https://a.com");
5343        assert_eq!(req.request_header("x-missing"), None);
5344    }
5345
5346    #[tokio::test]
5347    async fn test_response_header_case_insensitive() {
5348        let mut resp = make_response("1", 200, "https://a.com");
5349        resp.headers = vec![CapturedHeader {
5350            name: "X-Response-Header".into(),
5351            value: "val".into(),
5352        }];
5353        let entry = NetworkEntry {
5354            request: make_request("1", "GET", "https://a.com"),
5355            response: Some(resp),
5356            error: None,
5357        };
5358        assert_eq!(entry.response_header("x-response-header"), Some("val"));
5359        assert_eq!(entry.response_header("X-RESPONSE-HEADER"), Some("val"));
5360    }
5361
5362    // ---------- network entry helpers ----------
5363
5364    #[tokio::test]
5365    async fn test_network_entry_final_url() {
5366        let req = make_request("1", "GET", "https://a.com");
5367        let resp = make_response("1", 200, "https://b.com");
5368        let entry = NetworkEntry {
5369            request: req.clone(),
5370            response: Some(resp),
5371            error: None,
5372        };
5373        assert_eq!(entry.final_url(), "https://b.com");
5374        let entry2 = NetworkEntry {
5375            request: req,
5376            response: None,
5377            error: None,
5378        };
5379        assert_eq!(entry2.final_url(), "https://a.com");
5380    }
5381
5382    #[tokio::test]
5383    async fn test_network_entry_status() {
5384        let entry = NetworkEntry {
5385            request: make_request("1", "GET", "https://a.com"),
5386            response: Some(make_response("1", 404, "https://a.com")),
5387            error: None,
5388        };
5389        assert_eq!(entry.status(), Some(404));
5390        let entry2 = NetworkEntry {
5391            request: make_request("1", "GET", "https://a.com"),
5392            response: None,
5393            error: None,
5394        };
5395        assert_eq!(entry2.status(), None);
5396    }
5397
5398    // ---------- property-based tests (proptest) ----------
5399
5400    use proptest::prelude::*;
5401
5402    #[tokio::test]
5403    async fn test_proptest_filter_never_panics() {
5404        let log = NetworkLog::new();
5405        for i in 0..50 {
5406            let mut req = make_request(
5407                &format!("{}", i),
5408                ["GET", "POST", "PUT", "DELETE"][i % 4],
5409                &format!("https://{}.com", i),
5410            );
5411            req.headers = vec![CapturedHeader {
5412                name: "X-Id".into(),
5413                value: format!("{}", i),
5414            }];
5415            push_request(&log, req).await;
5416        }
5417
5418        // Test various filter combinations don't panic
5419        let _ = log.filter(Filter::new().method("GET")).await;
5420        let _ = log.filter(Filter::new().status_range(100..=599)).await;
5421        let _ = log.filter(Filter::new().url_contains("com")).await;
5422        let _ = log.filter(Filter::new().header("x-id", "25")).await;
5423        let _ = log.filter(Filter::new().with_response()).await;
5424        let _ = log.filter(Filter::new().without_response()).await;
5425        let _ = log.filter(Filter::new().with_error()).await;
5426        let _ = log
5427            .filter(
5428                Filter::new()
5429                    .method("GET")
5430                    .url_contains("com")
5431                    .with_response(),
5432            )
5433            .await;
5434    }
5435
5436    #[tokio::test]
5437    async fn test_proptest_concurrent_subscribe_and_push() {
5438        let log = NetworkLog::new();
5439        let mut handles = Vec::new();
5440
5441        for _ in 0..20 {
5442            let log = log.clone();
5443            handles.push(tokio::spawn(async move {
5444                let mut rx = log.subscribe().await;
5445                let mut count = 0;
5446                while count < 50 {
5447                    if rx.try_recv().is_ok() {
5448                        count += 1;
5449                    } else {
5450                        tokio::task::yield_now().await;
5451                    }
5452                }
5453            }));
5454        }
5455
5456        let log2 = log.clone();
5457        let pusher = tokio::spawn(async move {
5458            for i in 0..100 {
5459                push_request(
5460                    &log2,
5461                    make_request(&format!("{}", i), "GET", "https://x.com"),
5462                )
5463                .await;
5464                if i % 10 == 0 {
5465                    tokio::task::yield_now().await;
5466                }
5467            }
5468        });
5469
5470        pusher.await.unwrap();
5471        for h in handles {
5472            let _ = h.await;
5473        }
5474    }
5475
5476    #[tokio::test]
5477    async fn test_proptest_random_operations() {
5478        let log = NetworkLog::new();
5479        for i in 0..100 {
5480            push_request(
5481                &log,
5482                make_request(&format!("{}", i), "GET", &format!("https://{}.com", i)),
5483            )
5484            .await;
5485        }
5486
5487        // Perform random operations and verify invariants
5488        log.remove_by_id("50").await;
5489        log.retain(|e| e.request.id != "25").await;
5490        let _ = log.drain_pending().await;
5491        log.clear().await;
5492
5493        assert!(log.is_empty().await);
5494        assert_eq!(log.pending_count().await, 0);
5495    }
5496
5497    // ---------- proptest property-based tests ----------
5498
5499    proptest! {
5500        #![proptest_config(ProptestConfig::with_cases(256))]
5501
5502        #[test]
5503        fn prop_query_params_no_crash(url in r"https://[a-z0-9]+\.com(/[a-z]*)*(\?[a-z]+=[a-z0-9]*)*") {
5504            let req = CapturedRequest {
5505                id: "1".into(), context: None, url: url.clone(), method: "GET".into(),
5506                headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5507                destination: "document".into(), initiator_type: None,
5508                timing: CapturedTiming::default(),
5509            };
5510            let _ = req.query_params(); // must not panic
5511        }
5512
5513        #[test]
5514        fn prop_request_header_case_insensitive(
5515            name in "[A-Za-z_-]{1,20}",
5516            value in "[a-z0-9]{1,30}",
5517            lookup in "[A-Za-z_-]{1,20}"
5518        ) {
5519            let req = CapturedRequest {
5520                id: "1".into(), context: None, url: "https://a.com".into(), method: "GET".into(),
5521                headers: vec![CapturedHeader { name: name.clone(), value: value.clone() }],
5522                cookies: vec![], post_data: None, timestamp: 0,
5523                destination: "document".into(), initiator_type: None,
5524                timing: CapturedTiming::default(),
5525            };
5526            let result = req.request_header(&lookup);
5527            if name.to_lowercase() == lookup.to_lowercase() {
5528                prop_assert_eq!(result, Some(value.as_str()));
5529            }
5530        }
5531
5532        #[test]
5533        fn prop_json_body_valid_json_roundtrips(obj in "\\{.*\\}") {
5534            let req = CapturedRequest {
5535                id: "1".into(), context: None, url: "https://a.com".into(), method: "POST".into(),
5536                headers: vec![], cookies: vec![], post_data: Some(obj.clone()),
5537                timestamp: 0, destination: "document".into(), initiator_type: None,
5538                timing: CapturedTiming::default(),
5539            };
5540            // If it parses, the round-tripped JSON should equal the original parsed value.
5541            if let Some(val) = req.json_body() {
5542                let re_encoded = serde_json::to_string(&val).unwrap();
5543                let re_parsed: serde_json::Value = serde_json::from_str(&re_encoded).unwrap();
5544                prop_assert_eq!(val, re_parsed);
5545            }
5546        }
5547
5548        #[test]
5549        fn prop_filter_empty_matches_anything(
5550            method in "(GET|POST|PUT|DELETE|PATCH)",
5551            url in r"https://[a-z]+\.com",
5552            status in 100u16..599u16
5553        ) {
5554            let entry = NetworkEntry {
5555                request: CapturedRequest {
5556                    id: "1".into(), context: None, url: url.clone(), method,
5557                    headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5558                    destination: "document".into(), initiator_type: None,
5559                    timing: CapturedTiming::default(),
5560                },
5561                response: Some(CapturedResponse {
5562                    id: "1".into(), url, protocol: "h2".into(), status,
5563                    status_text: "OK".into(), headers: vec![], mime_type: "text/html".into(),
5564                    body_size: None, from_cache: false,
5565                }),
5566                error: None,
5567            };
5568            let filter = Filter::new();
5569            prop_assert!(filter.matches(&entry));
5570        }
5571
5572        #[test]
5573        fn prop_to_curl_always_contains_url(url in r"https://[a-z]+\.com(/[a-z]*)*") {
5574            let entry = NetworkEntry {
5575                request: CapturedRequest {
5576                    id: "1".into(), context: None, url: url.clone(), method: "GET".into(),
5577                    headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5578                    destination: "document".into(), initiator_type: None,
5579                    timing: CapturedTiming::default(),
5580                },
5581                response: None, error: None,
5582            };
5583            let curl = entry.to_curl();
5584            prop_assert!(curl.contains(&url), "curl missing URL: {}", curl);
5585        }
5586
5587        #[test]
5588        fn prop_to_curl_get_has_no_data_flag(url in r"https://[a-z]+\.com") {
5589            let entry = NetworkEntry {
5590                request: CapturedRequest {
5591                    id: "1".into(), context: None, url, method: "GET".into(),
5592                    headers: vec![], cookies: vec![], post_data: None, timestamp: 0,
5593                    destination: "document".into(), initiator_type: None,
5594                    timing: CapturedTiming::default(),
5595                },
5596                response: None, error: None,
5597            };
5598            let curl = entry.to_curl();
5599            prop_assert!(!curl.contains(" -d "), "GET curl should not have -d: {}", curl);
5600        }
5601    }
5602
5603    // ---------- deep boundary / edge cases ----------
5604
5605    #[tokio::test]
5606    async fn test_push_entry_with_very_long_url() {
5607        let log = NetworkLog::new();
5608        let long_url = format!("https://a.com/{}", "x".repeat(10_000));
5609        push_request(&log, make_request("1", "GET", &long_url)).await;
5610        assert_eq!(log.first().await.unwrap().request.url.len(), 14 + 10_000);
5611    }
5612
5613    #[tokio::test]
5614    async fn test_push_entry_with_unicode_url() {
5615        let log = NetworkLog::new();
5616        push_request(&log, make_request("1", "GET", "https://例え.jp/テスト")).await;
5617        assert_eq!(
5618            log.first().await.unwrap().request.url,
5619            "https://例え.jp/テスト"
5620        );
5621    }
5622
5623    #[tokio::test]
5624    async fn test_filter_url_regex_complex() {
5625        let log = NetworkLog::new();
5626        push_request(
5627            &log,
5628            make_request("1", "GET", "https://api.example.com/v1/users"),
5629        )
5630        .await;
5631        push_request(
5632            &log,
5633            make_request("2", "GET", "https://api.example.com/v2/items"),
5634        )
5635        .await;
5636        push_request(&log, make_request("3", "GET", "https://other.com/v1/users")).await;
5637        let re = regex::Regex::new(r"api\.example\.com/v\d+/users").unwrap();
5638        let found = log.find_by_url_regex(&re).await;
5639        assert_eq!(found.unwrap().request.id, "1");
5640    }
5641
5642    #[tokio::test]
5643    async fn test_filter_url_regex_no_match() {
5644        let log = NetworkLog::new();
5645        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5646        let re = regex::Regex::new(r"nomatch").unwrap();
5647        assert!(log.find_by_url_regex(&re).await.is_none());
5648    }
5649
5650    #[tokio::test]
5651    async fn test_hostnames_with_subdomains() {
5652        let log = NetworkLog::new();
5653        push_request(
5654            &log,
5655            make_request("1", "GET", "https://sub1.sub2.example.com/path"),
5656        )
5657        .await;
5658        push_request(&log, make_request("2", "GET", "https://other.org")).await;
5659        let hosts = log.hostnames().await;
5660        assert_eq!(hosts.len(), 2);
5661        assert!(hosts.contains(&"sub1.sub2.example.com".into()));
5662    }
5663
5664    #[tokio::test]
5665    async fn test_endpoints_deduplicates_full_url() {
5666        let log = NetworkLog::new();
5667        push_request(&log, make_request("1", "GET", "https://a.com/path?x=1")).await;
5668        push_request(&log, make_request("2", "GET", "https://a.com/path?y=2")).await;
5669        let endpoints = log.endpoints().await;
5670        // endpoints() deduplicates by full URL, so query strings make them distinct
5671        assert_eq!(endpoints.len(), 2);
5672    }
5673
5674    #[tokio::test]
5675    async fn test_endpoints_with_ports() {
5676        let log = NetworkLog::new();
5677        push_request(&log, make_request("1", "GET", "https://a.com:8080/path")).await;
5678        push_request(&log, make_request("2", "GET", "https://a.com:9090/path")).await;
5679        let endpoints = log.endpoints().await;
5680        assert_eq!(endpoints.len(), 2);
5681    }
5682
5683    #[tokio::test]
5684    async fn test_distinct_methods_sorted() {
5685        let log = NetworkLog::new();
5686        push_request(&log, make_request("1", "DELETE", "https://a.com")).await;
5687        push_request(&log, make_request("2", "GET", "https://a.com")).await;
5688        push_request(&log, make_request("3", "POST", "https://a.com")).await;
5689        push_request(&log, make_request("4", "GET", "https://a.com")).await;
5690        assert_eq!(log.distinct_methods().await, vec!["DELETE", "GET", "POST"]);
5691    }
5692
5693    #[tokio::test]
5694    async fn test_distinct_statuses_sorted() {
5695        let log = NetworkLog::new();
5696        push_entry(
5697            &log,
5698            make_request("1", "GET", "https://a.com"),
5699            Some(make_response("1", 500, "https://a.com")),
5700            None,
5701        )
5702        .await;
5703        push_entry(
5704            &log,
5705            make_request("2", "GET", "https://a.com"),
5706            Some(make_response("2", 200, "https://a.com")),
5707            None,
5708        )
5709        .await;
5710        push_entry(
5711            &log,
5712            make_request("3", "GET", "https://a.com"),
5713            Some(make_response("3", 404, "https://a.com")),
5714            None,
5715        )
5716        .await;
5717        push_entry(
5718            &log,
5719            make_request("4", "GET", "https://a.com"),
5720            Some(make_response("4", 200, "https://a.com")),
5721            None,
5722        )
5723        .await;
5724        assert_eq!(log.distinct_statuses().await, vec![200, 404, 500]);
5725    }
5726
5727    #[tokio::test]
5728    async fn test_total_bytes_in_with_none_body_size() {
5729        let log = NetworkLog::new();
5730        let mut resp = make_response("1", 200, "https://a.com");
5731        resp.body_size = None;
5732        push_entry(
5733            &log,
5734            make_request("1", "GET", "https://a.com"),
5735            Some(resp),
5736            None,
5737        )
5738        .await;
5739        assert_eq!(log.total_bytes_in().await, 0);
5740    }
5741
5742    #[tokio::test]
5743    async fn test_total_bytes_out_with_post_data() {
5744        let log = NetworkLog::new();
5745        let mut req = make_request("1", "POST", "https://a.com");
5746        req.post_data = Some("payload".into());
5747        push_request(&log, req).await;
5748        assert_eq!(log.total_bytes_out().await, 7);
5749    }
5750
5751    #[tokio::test]
5752    async fn test_total_bytes_out_empty() {
5753        let log = NetworkLog::new();
5754        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5755        assert_eq!(log.total_bytes_out().await, 0);
5756    }
5757
5758    #[tokio::test]
5759    async fn test_completed_returns_only_with_response_or_error() {
5760        let log = NetworkLog::new();
5761        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5762        push_entry(
5763            &log,
5764            make_request("2", "GET", "https://b.com"),
5765            Some(make_response("2", 200, "https://b.com")),
5766            None,
5767        )
5768        .await;
5769        push_entry(
5770            &log,
5771            make_request("3", "GET", "https://c.com"),
5772            None,
5773            Some(make_error("3", "https://c.com", "err")),
5774        )
5775        .await;
5776        let completed = log.completed().await;
5777        assert_eq!(completed.len(), 2);
5778    }
5779
5780    #[tokio::test]
5781    async fn test_count_with_no_filter() {
5782        let log = NetworkLog::new();
5783        for i in 0..50 {
5784            push_request(
5785                &log,
5786                make_request(&format!("{}", i), "GET", "https://x.com"),
5787            )
5788            .await;
5789        }
5790        assert_eq!(log.count(Filter::new()).await, 50);
5791    }
5792
5793    #[tokio::test]
5794    async fn test_find_by_url_partial_match() {
5795        let log = NetworkLog::new();
5796        push_request(
5797            &log,
5798            make_request("1", "GET", "https://api.example.com/users"),
5799        )
5800        .await;
5801        push_request(&log, make_request("2", "GET", "https://other.com/users")).await;
5802        assert_eq!(
5803            log.find_by_url("api.example").await.unwrap().request.id,
5804            "1"
5805        );
5806    }
5807
5808    #[tokio::test]
5809    async fn test_subscribe_receives_existing_and_new() {
5810        let log = NetworkLog::new();
5811        push_request(&log, make_request("1", "GET", "https://a.com")).await;
5812        let mut rx = log.subscribe().await;
5813        push_request(&log, make_request("2", "GET", "https://b.com")).await;
5814        // Should receive entry 2 (not 1, since subscription starts after)
5815        let received = rx.try_recv().unwrap();
5816        assert_eq!(received.request.id, "2");
5817        assert!(rx.try_recv().is_err());
5818    }
5819
5820    #[tokio::test]
5821    async fn test_metrics_after_eviction() {
5822        let log = NetworkLog::with_limits(10, 5);
5823        for i in 0..25 {
5824            push_request(
5825                &log,
5826                make_request(&format!("{}", i), "GET", "https://x.com"),
5827            )
5828            .await;
5829        }
5830        let m = log.metrics().await;
5831        assert!(m.entries_evicted > 0);
5832    }
5833
5834    #[tokio::test]
5835    async fn test_eviction_preserves_order_of_remaining() {
5836        let log = NetworkLog::with_limits(10, 5);
5837        for i in 0..20 {
5838            push_request(
5839                &log,
5840                make_request(&format!("{}", i), "GET", "https://x.com"),
5841            )
5842            .await;
5843        }
5844        let ids = log.request_ids().await;
5845        // After eviction of oldest half, remaining should still be in order
5846        for pair in ids.windows(2) {
5847            let a: u32 = pair[0].parse().unwrap();
5848            let b: u32 = pair[1].parse().unwrap();
5849            assert!(a < b, "order violated: {a} before {b}");
5850        }
5851    }
5852
5853    #[tokio::test]
5854    async fn test_captured_timing_defaults() {
5855        let timing = CapturedTiming::default();
5856        assert!(timing.dns_start_ms.is_none());
5857        assert!(timing.dns_end_ms.is_none());
5858        assert!(timing.connect_start_ms.is_none());
5859        assert!(timing.connect_end_ms.is_none());
5860        assert!(timing.tls_start_ms.is_none());
5861        assert!(timing.response_start_ms.is_none());
5862        assert!(timing.response_end_ms.is_none());
5863    }
5864
5865    #[tokio::test]
5866    async fn test_captured_request_header_missing() {
5867        let req = make_request("1", "GET", "https://a.com");
5868        assert_eq!(req.request_header("X-Missing"), None);
5869    }
5870
5871    #[tokio::test]
5872    async fn test_network_entry_is_error() {
5873        let entry = NetworkEntry {
5874            request: make_request("1", "GET", "https://a.com"),
5875            response: None,
5876            error: Some(make_error("1", "https://a.com", "err")),
5877        };
5878        assert!(entry.is_error());
5879        assert!(!entry.has_response());
5880    }
5881
5882    #[tokio::test]
5883    async fn test_network_entry_has_response() {
5884        let entry = NetworkEntry {
5885            request: make_request("1", "GET", "https://a.com"),
5886            response: Some(make_response("1", 200, "https://a.com")),
5887            error: None,
5888        };
5889        assert!(entry.has_response());
5890        assert!(!entry.is_error());
5891    }
5892
5893    #[tokio::test]
5894    async fn test_save_to_json_empty_log() {
5895        let log = NetworkLog::new();
5896        let path = std::path::Path::new("/tmp/foxdriver_json_empty.json");
5897        log.save_to_json(path).await.unwrap();
5898        let content = std::fs::read_to_string(path).unwrap();
5899        let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
5900        assert!(parsed.is_empty());
5901    }
5902
5903    #[tokio::test]
5904    async fn test_save_to_json_roundtrip_entries() {
5905        let log = NetworkLog::new();
5906        let mut req = make_request("1", "POST", "https://api.example.com");
5907        req.headers = vec![CapturedHeader {
5908            name: "Content-Type".into(),
5909            value: "application/json".into(),
5910        }];
5911        req.post_data = Some(r#"{"key":"value"}"#.into());
5912        push_entry(
5913            &log,
5914            req,
5915            Some(make_response("1", 201, "https://api.example.com")),
5916            None,
5917        )
5918        .await;
5919        let path = std::path::Path::new("/tmp/foxdriver_json_roundtrip.json");
5920        log.save_to_json(path).await.unwrap();
5921        let content = std::fs::read_to_string(path).unwrap();
5922        let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
5923        assert_eq!(parsed.len(), 1);
5924        assert_eq!(parsed[0].request.id, "1");
5925        assert_eq!(parsed[0].request.method, "POST");
5926        assert_eq!(parsed[0].response.as_ref().unwrap().status, 201);
5927    }
5928
5929    #[tokio::test]
5930    async fn test_har_with_multiple_entries() {
5931        let log = NetworkLog::new();
5932        push_entry(
5933            &log,
5934            make_request("1", "GET", "https://a.com"),
5935            Some(make_response("1", 200, "https://a.com")),
5936            None,
5937        )
5938        .await;
5939        push_entry(
5940            &log,
5941            make_request("2", "POST", "https://b.com"),
5942            Some(make_response("2", 201, "https://b.com")),
5943            None,
5944        )
5945        .await;
5946        let path = std::path::Path::new("/tmp/foxdriver_har_multi.json");
5947        log.save_as_har(path, Some("test page")).await.unwrap();
5948        let content = std::fs::read_to_string(path).unwrap();
5949        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
5950        assert_eq!(parsed["log"]["entries"].as_array().unwrap().len(), 2);
5951        assert_eq!(parsed["log"]["pages"][0]["title"], "test page");
5952    }
5953
5954    #[tokio::test]
5955    async fn test_har_error_entry_has_no_response() {
5956        let log = NetworkLog::new();
5957        push_entry(
5958            &log,
5959            make_request("1", "GET", "https://a.com"),
5960            None,
5961            Some(make_error("1", "https://a.com", "timeout")),
5962        )
5963        .await;
5964        let path = std::path::Path::new("/tmp/foxdriver_har_error.json");
5965        log.save_as_har(path, None).await.unwrap();
5966        let content = std::fs::read_to_string(path).unwrap();
5967        let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
5968        let entry = &parsed["log"]["entries"][0];
5969        assert!(entry["response"].is_null());
5970    }
5971
5972    // ---------- BiDi ingestion deep tests ----------
5973
5974    #[tokio::test]
5975    async fn test_metrics_requests_received_manual_increment() {
5976        let log = NetworkLog::new();
5977        let m0 = log.metrics().await;
5978        {
5979            let mut inner = log.inner.write().await;
5980            inner.metrics.requests_received += 5;
5981        }
5982        let m1 = log.metrics().await;
5983        assert_eq!(m1.requests_received, m0.requests_received + 5);
5984    }
5985
5986    #[tokio::test]
5987    async fn test_metrics_responses_received_manual_increment() {
5988        let log = NetworkLog::new();
5989        let m0 = log.metrics().await;
5990        {
5991            let mut inner = log.inner.write().await;
5992            inner.metrics.responses_received += 3;
5993        }
5994        let m1 = log.metrics().await;
5995        assert_eq!(m1.responses_received, m0.responses_received + 3);
5996    }
5997
5998    #[tokio::test]
5999    async fn test_metrics_errors_received_manual_increment() {
6000        let log = NetworkLog::new();
6001        let m0 = log.metrics().await;
6002        {
6003            let mut inner = log.inner.write().await;
6004            inner.metrics.errors_received += 2;
6005        }
6006        let m1 = log.metrics().await;
6007        assert_eq!(m1.errors_received, m0.errors_received + 2);
6008    }
6009
6010    #[tokio::test]
6011    async fn test_deduplication_does_not_affect_metrics() {
6012        let log = NetworkLog::new();
6013        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6014        let m1 = log.metrics().await;
6015        push_request(&log, make_request("1", "GET", "https://b.com")).await; // deduplicated
6016        let m2 = log.metrics().await;
6017        assert_eq!(m2.requests_received, m1.requests_received); // no change
6018        assert_eq!(log.len().await, 1); // still only one entry
6019    }
6020
6021    // ---------- filter deep tests ----------
6022
6023    #[tokio::test]
6024    async fn test_filter_status_range_boundary_inclusive() {
6025        let log = NetworkLog::new();
6026        push_entry(
6027            &log,
6028            make_request("1", "GET", "https://a.com"),
6029            Some(make_response("1", 200, "https://a.com")),
6030            None,
6031        )
6032        .await;
6033        push_entry(
6034            &log,
6035            make_request("2", "GET", "https://a.com"),
6036            Some(make_response("2", 299, "https://a.com")),
6037            None,
6038        )
6039        .await;
6040        push_entry(
6041            &log,
6042            make_request("3", "GET", "https://a.com"),
6043            Some(make_response("3", 300, "https://a.com")),
6044            None,
6045        )
6046        .await;
6047        assert_eq!(
6048            log.filter(Filter::new().status_range(200..=299))
6049                .await
6050                .len(),
6051            2
6052        );
6053    }
6054
6055    #[tokio::test]
6056    async fn test_filter_destination_deep() {
6057        let log = NetworkLog::new();
6058        let mut req1 = make_request("1", "GET", "https://a.com");
6059        req1.destination = "document".into();
6060        let mut req2 = make_request("2", "GET", "https://b.com");
6061        req2.destination = "image".into();
6062        push_request(&log, req1).await;
6063        push_request(&log, req2).await;
6064        assert_eq!(
6065            log.filter(Filter::new().destination("document"))
6066                .await
6067                .len(),
6068            1
6069        );
6070        assert_eq!(
6071            log.filter(Filter::new().destination("image")).await.len(),
6072            1
6073        );
6074        assert_eq!(
6075            log.filter(Filter::new().destination("script")).await.len(),
6076            0
6077        );
6078    }
6079
6080    #[tokio::test]
6081    async fn test_filter_combined_with_response_and_without_response() {
6082        let log = NetworkLog::new();
6083        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6084        push_entry(
6085            &log,
6086            make_request("2", "GET", "https://b.com"),
6087            Some(make_response("2", 200, "https://b.com")),
6088            None,
6089        )
6090        .await;
6091        push_entry(
6092            &log,
6093            make_request("3", "GET", "https://c.com"),
6094            None,
6095            Some(make_error("3", "https://c.com", "err")),
6096        )
6097        .await;
6098        assert_eq!(log.filter(Filter::new().with_response()).await.len(), 1);
6099        assert_eq!(log.filter(Filter::new().without_response()).await.len(), 2);
6100        assert_eq!(log.filter(Filter::new().with_error()).await.len(), 1);
6101    }
6102
6103    #[tokio::test]
6104    async fn test_filter_url_regex_complex_pattern() {
6105        let log = NetworkLog::new();
6106        push_request(
6107            &log,
6108            make_request("1", "GET", "https://api.example.com/v1/users/123"),
6109        )
6110        .await;
6111        push_request(
6112            &log,
6113            make_request("2", "GET", "https://api.example.com/v2/items/456"),
6114        )
6115        .await;
6116        push_request(
6117            &log,
6118            make_request("3", "GET", "https://other.com/v1/users/789"),
6119        )
6120        .await;
6121        let matches = log
6122            .filter(
6123                Filter::new()
6124                    .url_regex(r"api\.example\.com/v\d+/(users|items)/\d+")
6125                    .unwrap(),
6126            )
6127            .await;
6128        assert_eq!(matches.len(), 2);
6129    }
6130
6131    // ---------- serialization deep tests ----------
6132
6133    #[tokio::test]
6134    async fn test_captured_request_serde_roundtrip() {
6135        let req = make_request("1", "POST", "https://a.com");
6136        let json = serde_json::to_string(&req).unwrap();
6137        let de: CapturedRequest = serde_json::from_str(&json).unwrap();
6138        assert_eq!(de.id, req.id);
6139        assert_eq!(de.method, req.method);
6140        assert_eq!(de.url, req.url);
6141    }
6142
6143    #[tokio::test]
6144    async fn test_captured_response_serde_roundtrip() {
6145        let resp = make_response("1", 404, "https://a.com");
6146        let json = serde_json::to_string(&resp).unwrap();
6147        let de: CapturedResponse = serde_json::from_str(&json).unwrap();
6148        assert_eq!(de.status, 404);
6149        assert_eq!(de.url, "https://a.com");
6150    }
6151
6152    #[tokio::test]
6153    async fn test_captured_error_serde_roundtrip() {
6154        let err = make_error("1", "https://a.com", "timeout");
6155        let json = serde_json::to_string(&err).unwrap();
6156        let de: CapturedError = serde_json::from_str(&json).unwrap();
6157        assert_eq!(de.error_text, "timeout");
6158    }
6159
6160    #[tokio::test]
6161    async fn test_network_entry_serde_roundtrip() {
6162        let entry = NetworkEntry {
6163            request: make_request("1", "GET", "https://a.com"),
6164            response: Some(make_response("1", 200, "https://a.com")),
6165            error: None,
6166        };
6167        let json = serde_json::to_string(&entry).unwrap();
6168        let de: NetworkEntry = serde_json::from_str(&json).unwrap();
6169        assert_eq!(de.request.id, "1");
6170        assert!(de.response.is_some());
6171        assert!(de.error.is_none());
6172    }
6173
6174    // ---------- concurrent deep tests ----------
6175
6176    #[tokio::test]
6177    async fn test_concurrent_remove_and_query() {
6178        let log = NetworkLog::new();
6179        for i in 0..100 {
6180            push_request(
6181                &log,
6182                make_request(&format!("{}", i), "GET", "https://x.com"),
6183            )
6184            .await;
6185        }
6186        let mut handles = Vec::new();
6187        for _ in 0..10 {
6188            let log = log.clone();
6189            handles.push(tokio::spawn(async move {
6190                for _ in 0..10 {
6191                    let _ = log.remove_by_id("50").await;
6192                    let _ = log.len().await;
6193                    let _ = log.contains_id("50").await;
6194                }
6195            }));
6196        }
6197        for h in handles {
6198            h.await.unwrap();
6199        }
6200    }
6201
6202    #[tokio::test]
6203    async fn test_concurrent_retain_and_read() {
6204        let log = NetworkLog::new();
6205        for i in 0..100 {
6206            push_request(
6207                &log,
6208                make_request(&format!("{}", i), "GET", "https://x.com"),
6209            )
6210            .await;
6211        }
6212        let mut handles = Vec::new();
6213        for _ in 0..5 {
6214            let log = log.clone();
6215            handles.push(tokio::spawn(async move {
6216                log.retain(|e| e.request.id != "50").await;
6217            }));
6218        }
6219        for _ in 0..5 {
6220            let log = log.clone();
6221            handles.push(tokio::spawn(async move {
6222                for _ in 0..20 {
6223                    let _ = log.len().await;
6224                    tokio::task::yield_now().await;
6225                }
6226            }));
6227        }
6228        for h in handles {
6229            h.await.unwrap();
6230        }
6231    }
6232
6233    #[tokio::test]
6234    async fn test_concurrent_drain_pending_and_ingest() {
6235        let log = NetworkLog::new();
6236        let mut handles = Vec::new();
6237        for _ in 0..5 {
6238            let log = log.clone();
6239            handles.push(tokio::spawn(async move {
6240                for i in 0..20 {
6241                    push_request(
6242                        &log,
6243                        make_request(&format!("{}", i), "GET", "https://x.com"),
6244                    )
6245                    .await;
6246                }
6247            }));
6248        }
6249        for _ in 0..5 {
6250            let log = log.clone();
6251            handles.push(tokio::spawn(async move {
6252                for _ in 0..20 {
6253                    let _ = log.drain_pending().await;
6254                    tokio::task::yield_now().await;
6255                }
6256            }));
6257        }
6258        for h in handles {
6259            h.await.unwrap();
6260        }
6261    }
6262
6263    // ---------- wait helper deep tests ----------
6264
6265    #[tokio::test]
6266    async fn test_wait_for_url_receives_new_entry() {
6267        let log = NetworkLog::new();
6268        let log2 = log.clone();
6269        let waiter = tokio::spawn(async move {
6270            log2.wait_for_url("target", std::time::Duration::from_secs(1))
6271                .await
6272        });
6273        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6274        push_request(&log, make_request("1", "GET", "https://target.com")).await;
6275        let result = waiter.await.unwrap();
6276        assert!(result.is_some());
6277        assert_eq!(result.unwrap().request.url, "https://target.com");
6278    }
6279
6280    #[tokio::test]
6281    async fn test_wait_for_response_receives_update() {
6282        let log = NetworkLog::new();
6283        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6284        let log2 = log.clone();
6285        let waiter = tokio::spawn(async move {
6286            log2.wait_for_response("1", std::time::Duration::from_secs(1))
6287                .await
6288        });
6289        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
6290        {
6291            let mut inner = log.inner.write().await;
6292            let mut new_entry = (*inner.entries[0]).clone();
6293            new_entry.response = Some(make_response("1", 200, "https://a.com"));
6294            inner.entries[0] = Arc::new(new_entry);
6295            let _ = inner.tx.send(inner.entries[0].clone());
6296        }
6297        let result = waiter.await.unwrap();
6298        assert!(result.is_some());
6299        assert!(result.unwrap().has_response());
6300    }
6301
6302    #[tokio::test]
6303    async fn test_filter_combined_method_status_url() {
6304        let log = NetworkLog::new();
6305        push_request(
6306            &log,
6307            make_request("1", "POST", "https://api.example.com/v1"),
6308        )
6309        .await;
6310        push_request(&log, make_request("2", "GET", "https://api.example.com/v2")).await;
6311        {
6312            let mut inner = log.inner.write().await;
6313            let mut e = (*inner.entries[0]).clone();
6314            e.response = Some(make_response("1", 201, "https://api.example.com/v1"));
6315            inner.entries[0] = Arc::new(e);
6316        }
6317        let f = Filter::new()
6318            .method("POST")
6319            .status_range(200..=299)
6320            .url_contains("v1");
6321        let hits = log.filter(f).await;
6322        assert_eq!(hits.len(), 1);
6323        assert_eq!(hits[0].request.id, "1");
6324    }
6325
6326    #[tokio::test]
6327    async fn test_filter_has_error_true() {
6328        let log = NetworkLog::new();
6329        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6330        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6331        {
6332            let mut inner = log.inner.write().await;
6333            let mut e = (*inner.entries[1]).clone();
6334            e.error = Some(CapturedError {
6335                id: "2".into(),
6336                url: "https://b.com".into(),
6337                error_text: "net::ERR_FAILED".into(),
6338            });
6339            inner.entries[1] = Arc::new(e);
6340        }
6341        let hits = log.filter(Filter::new().with_error()).await;
6342        assert_eq!(hits.len(), 1);
6343        assert_eq!(hits[0].request.id, "2");
6344    }
6345
6346    #[tokio::test]
6347    async fn test_find_by_url_exact_vs_substring() {
6348        let log = NetworkLog::new();
6349        push_request(&log, make_request("1", "GET", "https://example.com/path")).await;
6350        let found = log.find_by_url("/path").await;
6351        assert!(found.is_some());
6352        let not_found = log.find_by_url("/other").await;
6353        assert!(not_found.is_none());
6354    }
6355
6356    #[tokio::test]
6357    async fn test_endpoints_domain_only_url() {
6358        let log = NetworkLog::new();
6359        push_request(&log, make_request("1", "GET", "https://example.com")).await;
6360        let eps = log.endpoints().await;
6361        assert_eq!(eps, vec!["https://example.com"]);
6362    }
6363
6364    #[tokio::test]
6365    async fn test_hostnames_idn_url() {
6366        let log = NetworkLog::new();
6367        push_request(&log, make_request("1", "GET", "https://münchen.de/shop")).await;
6368        let hosts = log.hostnames().await;
6369        // url::Url normalises IDN to punycode
6370        assert_eq!(hosts, vec!["xn--mnchen-3ya.de"]);
6371    }
6372
6373    #[tokio::test]
6374    async fn test_total_bytes_in_missing_body_size() {
6375        let log = NetworkLog::new();
6376        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6377        {
6378            let mut inner = log.inner.write().await;
6379            let mut e = (*inner.entries[0]).clone();
6380            e.response = Some(CapturedResponse {
6381                id: "1".into(),
6382                url: "https://a.com".into(),
6383                protocol: "h2".into(),
6384                status: 200,
6385                status_text: "OK".into(),
6386                headers: vec![],
6387                mime_type: "text/html".into(),
6388                body_size: None,
6389                from_cache: false,
6390            });
6391            inner.entries[0] = Arc::new(e);
6392        }
6393        assert_eq!(log.total_bytes_in().await, 0);
6394    }
6395
6396    #[tokio::test]
6397    async fn test_total_bytes_out_empty_post_data() {
6398        let log = NetworkLog::new();
6399        let mut req = make_request("1", "POST", "https://a.com");
6400        req.post_data = Some("".into());
6401        push_request(&log, req).await;
6402        assert_eq!(log.total_bytes_out().await, 0);
6403    }
6404
6405    #[test]
6406    fn test_to_curl_post_json() {
6407        let entry = NetworkEntry {
6408            request: CapturedRequest {
6409                id: "1".into(),
6410                context: None,
6411                url: "https://api.com".into(),
6412                method: "POST".into(),
6413                headers: vec![CapturedHeader {
6414                    name: "Content-Type".into(),
6415                    value: "application/json".into(),
6416                }],
6417                cookies: vec![],
6418                post_data: Some(r#"{"key":"val"}"#.into()),
6419                timestamp: 0,
6420                destination: "document".into(),
6421                initiator_type: None,
6422                timing: CapturedTiming::default(),
6423            },
6424            response: None,
6425            error: None,
6426        };
6427        let curl = entry.to_curl();
6428        assert!(curl.contains("-X POST"));
6429        assert!(curl.contains("-H 'Content-Type: application/json'"));
6430        assert!(curl.contains(r#"-d '{"key":"val"}'"#));
6431    }
6432
6433    #[test]
6434    fn test_to_curl_header_with_quote() {
6435        let entry = NetworkEntry {
6436            request: CapturedRequest {
6437                id: "1".into(),
6438                context: None,
6439                url: "https://a.com".into(),
6440                method: "GET".into(),
6441                headers: vec![CapturedHeader {
6442                    name: "X-Token".into(),
6443                    value: "it's ok".into(),
6444                }],
6445                cookies: vec![],
6446                post_data: None,
6447                timestamp: 0,
6448                destination: "document".into(),
6449                initiator_type: None,
6450                timing: CapturedTiming::default(),
6451            },
6452            response: None,
6453            error: None,
6454        };
6455        let curl = entry.to_curl();
6456        assert!(curl.contains("X-Token: it'\\''s ok"));
6457    }
6458
6459    #[tokio::test]
6460    async fn test_retain_keeps_some_removes_others() {
6461        let log = NetworkLog::new();
6462        push_request(&log, make_request("1", "GET", "https://keep.com")).await;
6463        push_request(&log, make_request("2", "GET", "https://drop.com")).await;
6464        push_request(&log, make_request("3", "GET", "https://keep.com/page")).await;
6465        log.retain(|e| e.request.url.contains("keep")).await;
6466        let ids = log.request_ids().await;
6467        assert_eq!(ids.len(), 2);
6468        assert!(ids.contains(&"1".into()));
6469        assert!(ids.contains(&"3".into()));
6470    }
6471
6472    #[tokio::test]
6473    async fn test_request_ids_ordering_after_eviction() {
6474        let log = NetworkLog::with_limits(4, 100);
6475        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6476        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6477        push_request(&log, make_request("3", "GET", "https://c.com")).await;
6478        push_request(&log, make_request("4", "GET", "https://d.com")).await;
6479        push_request(&log, make_request("5", "GET", "https://e.com")).await; // triggers eviction
6480        let ids = log.request_ids().await;
6481        // Incremental eviction removes only the oldest entry needed to stay under the cap.
6482        assert_eq!(ids.len(), 4);
6483        assert_eq!(
6484            ids,
6485            vec![
6486                "2".to_string(),
6487                "3".to_string(),
6488                "4".to_string(),
6489                "5".to_string()
6490            ]
6491        );
6492    }
6493
6494    #[tokio::test]
6495    async fn test_has_response_false_for_nonexistent() {
6496        let log = NetworkLog::new();
6497        assert!(!log.has_response("nope").await);
6498    }
6499
6500    #[tokio::test]
6501    async fn test_has_error_false_for_nonexistent() {
6502        let log = NetworkLog::new();
6503        assert!(!log.has_error("nope").await);
6504    }
6505
6506    #[tokio::test]
6507    async fn test_memory_estimate_increases_with_entries() {
6508        let log = NetworkLog::new();
6509        let empty = log.memory_estimate().await;
6510        push_request(
6511            &log,
6512            make_request("1", "GET", "https://example.com/some/long/path/here"),
6513        )
6514        .await;
6515        let with_one = log.memory_estimate().await;
6516        assert!(with_one > empty);
6517    }
6518
6519    #[tokio::test]
6520    async fn test_save_to_json_empty_log_roundtrip() {
6521        let log = NetworkLog::new();
6522        let tmp =
6523            std::env::temp_dir().join(format!("foxdriver_empty_json_{}.json", std::process::id()));
6524        log.save_to_json(&tmp).await.unwrap();
6525        let content = std::fs::read_to_string(&tmp).unwrap();
6526        assert!(content.contains("[]"));
6527        let _ = std::fs::remove_file(&tmp);
6528    }
6529
6530    #[tokio::test]
6531    async fn test_save_as_har_empty_log() {
6532        let log = NetworkLog::new();
6533        let tmp =
6534            std::env::temp_dir().join(format!("foxdriver_empty_har_{}.har", std::process::id()));
6535        log.save_as_har(&tmp, Some("empty")).await.unwrap();
6536        let content = std::fs::read_to_string(&tmp).unwrap();
6537        assert!(content.contains("\"entries\""));
6538        assert!(content.contains("\"pages\""));
6539        let _ = std::fs::remove_file(&tmp);
6540    }
6541
6542    #[tokio::test]
6543    async fn test_filter_destination_matching() {
6544        let log = NetworkLog::new();
6545        let mut req = make_request("1", "GET", "https://a.com");
6546        req.destination = "image".into();
6547        push_request(&log, req).await;
6548        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6549        let hits = log.filter(Filter::new().destination("image")).await;
6550        assert_eq!(hits.len(), 1);
6551        assert_eq!(hits[0].request.id, "1");
6552    }
6553
6554    #[tokio::test]
6555    async fn test_distinct_methods_empty_log() {
6556        let log = NetworkLog::new();
6557        assert!(log.distinct_methods().await.is_empty());
6558    }
6559
6560    #[tokio::test]
6561    async fn test_distinct_statuses_no_responses() {
6562        let log = NetworkLog::new();
6563        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6564        assert!(log.distinct_statuses().await.is_empty());
6565    }
6566
6567    #[tokio::test]
6568    async fn test_total_bytes_in_all_none() {
6569        let log = NetworkLog::new();
6570        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6571        {
6572            let mut inner = log.inner.write().await;
6573            let mut e = (*inner.entries[0]).clone();
6574            e.response = Some(CapturedResponse {
6575                id: "1".into(),
6576                url: "https://a.com".into(),
6577                protocol: "h2".into(),
6578                status: 200,
6579                status_text: "OK".into(),
6580                headers: vec![],
6581                mime_type: "text/html".into(),
6582                body_size: None,
6583                from_cache: false,
6584            });
6585            inner.entries[0] = Arc::new(e);
6586        }
6587        assert_eq!(log.total_bytes_in().await, 0);
6588    }
6589
6590    #[tokio::test]
6591    async fn test_total_bytes_out_no_post_data() {
6592        let log = NetworkLog::new();
6593        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6594        assert_eq!(log.total_bytes_out().await, 0);
6595    }
6596
6597    #[tokio::test]
6598    async fn test_save_to_json_roundtrip_with_response() {
6599        let log = NetworkLog::new();
6600        push_request(&log, make_request("1", "POST", "https://api.com")).await;
6601        {
6602            let mut inner = log.inner.write().await;
6603            let mut e = (*inner.entries[0]).clone();
6604            e.response = Some(make_response("1", 201, "https://api.com"));
6605            inner.entries[0] = Arc::new(e);
6606        }
6607        let tmp =
6608            std::env::temp_dir().join(format!("foxdriver_json_rt_{}.json", std::process::id()));
6609        log.save_to_json(&tmp).await.unwrap();
6610        let content = std::fs::read_to_string(&tmp).unwrap();
6611        let parsed: Vec<NetworkEntry> = serde_json::from_str(&content).unwrap();
6612        assert_eq!(parsed.len(), 1);
6613        assert_eq!(parsed[0].request.id, "1");
6614        assert_eq!(parsed[0].response.as_ref().unwrap().status, 201);
6615        let _ = std::fs::remove_file(&tmp);
6616    }
6617
6618    #[tokio::test]
6619    async fn test_save_as_har_with_response_entry() {
6620        let log = NetworkLog::new();
6621        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6622        {
6623            let mut inner = log.inner.write().await;
6624            let mut e = (*inner.entries[0]).clone();
6625            e.response = Some(make_response("1", 200, "https://a.com"));
6626            inner.entries[0] = Arc::new(e);
6627        }
6628        let tmp =
6629            std::env::temp_dir().join(format!("foxdriver_har_resp_{}.har", std::process::id()));
6630        log.save_as_har(&tmp, Some("testpage")).await.unwrap();
6631        let content = std::fs::read_to_string(&tmp).unwrap();
6632        assert!(content.contains("\"testpage\""));
6633        assert!(content.contains("200"));
6634        let _ = std::fs::remove_file(&tmp);
6635    }
6636
6637    #[tokio::test]
6638    async fn test_has_response_true_when_present() {
6639        let log = NetworkLog::new();
6640        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6641        assert!(!log.has_response("1").await);
6642        {
6643            let mut inner = log.inner.write().await;
6644            let mut e = (*inner.entries[0]).clone();
6645            e.response = Some(make_response("1", 200, "https://a.com"));
6646            inner.entries[0] = Arc::new(e);
6647        }
6648        assert!(log.has_response("1").await);
6649    }
6650
6651    #[tokio::test]
6652    async fn test_has_error_true_when_present() {
6653        let log = NetworkLog::new();
6654        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6655        assert!(!log.has_error("1").await);
6656        {
6657            let mut inner = log.inner.write().await;
6658            let mut e = (*inner.entries[0]).clone();
6659            e.error = Some(CapturedError {
6660                id: "1".into(),
6661                url: "https://a.com".into(),
6662                error_text: "fail".into(),
6663            });
6664            inner.entries[0] = Arc::new(e);
6665        }
6666        assert!(log.has_error("1").await);
6667    }
6668
6669    #[tokio::test]
6670    async fn test_memory_estimate_decreases_after_clear() {
6671        let log = NetworkLog::new();
6672        push_request(
6673            &log,
6674            make_request(
6675                "1",
6676                "GET",
6677                "https://example.com/very/long/path/here/for/bytes",
6678            ),
6679        )
6680        .await;
6681        let before = log.memory_estimate().await;
6682        log.clear().await;
6683        let after = log.memory_estimate().await;
6684        assert!(after < before);
6685    }
6686
6687    #[tokio::test]
6688    async fn test_memory_estimate_decreases_after_retain() {
6689        let log = NetworkLog::new();
6690        push_request(&log, make_request("1", "GET", "https://a.com/loooooong")).await;
6691        push_request(&log, make_request("2", "GET", "https://b.com/loooooong")).await;
6692        let before = log.memory_estimate().await;
6693        log.retain(|e| e.request.id == "1").await;
6694        let after = log.memory_estimate().await;
6695        assert!(after < before);
6696    }
6697
6698    #[tokio::test]
6699    async fn test_completed_excludes_in_flight() {
6700        let log = NetworkLog::new();
6701        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6702        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6703        {
6704            let mut inner = log.inner.write().await;
6705            let mut e = (*inner.entries[1]).clone();
6706            e.response = Some(make_response("2", 200, "https://b.com"));
6707            inner.entries[1] = Arc::new(e);
6708        }
6709        let completed = log.completed().await;
6710        assert_eq!(completed.len(), 1);
6711        assert_eq!(completed[0].request.id, "2");
6712    }
6713
6714    #[tokio::test]
6715    async fn test_completed_includes_errors() {
6716        let log = NetworkLog::new();
6717        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6718        {
6719            let mut inner = log.inner.write().await;
6720            let mut e = (*inner.entries[0]).clone();
6721            e.error = Some(CapturedError {
6722                id: "1".into(),
6723                url: "https://a.com".into(),
6724                error_text: "net::ERR_ABORTED".into(),
6725            });
6726            inner.entries[0] = Arc::new(e);
6727        }
6728        let completed = log.completed().await;
6729        assert_eq!(completed.len(), 1);
6730        assert!(completed[0].is_error());
6731    }
6732
6733    #[tokio::test]
6734    async fn test_find_by_url_regex_complex_pattern() {
6735        let log = NetworkLog::new();
6736        push_request(
6737            &log,
6738            make_request("1", "GET", "https://api.example.com/v1/users"),
6739        )
6740        .await;
6741        push_request(
6742            &log,
6743            make_request("2", "GET", "https://api.example.com/v2/items"),
6744        )
6745        .await;
6746        let re = regex::Regex::new(r"/v\d+/users").unwrap();
6747        let found = log.find_by_url_regex(&re).await;
6748        assert_eq!(found.unwrap().request.id, "1");
6749    }
6750
6751    #[tokio::test]
6752    async fn test_nth_out_of_bounds_returns_none() {
6753        let log = NetworkLog::new();
6754        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6755        assert!(log.nth(5).await.is_none());
6756    }
6757
6758    #[tokio::test]
6759    async fn test_first_last_both_none_on_empty() {
6760        let log = NetworkLog::new();
6761        assert!(log.first().await.is_none());
6762        assert!(log.last().await.is_none());
6763    }
6764
6765    #[tokio::test]
6766    async fn test_remove_by_id_missing_returns_none() {
6767        let log = NetworkLog::new();
6768        assert!(log.remove_by_id("nope").await.is_none());
6769    }
6770
6771    #[tokio::test]
6772    async fn test_contains_id_true_and_false() {
6773        let log = NetworkLog::new();
6774        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6775        assert!(log.contains_id("1").await);
6776        assert!(!log.contains_id("2").await);
6777    }
6778
6779    #[tokio::test]
6780    async fn test_endpoints_dedupes_by_full_url() {
6781        // endpoints() deduplicates by the full request URL (including query string).
6782        let log = NetworkLog::new();
6783        push_request(&log, make_request("1", "GET", "https://a.com/page?x=1")).await;
6784        push_request(&log, make_request("2", "GET", "https://a.com/page?x=2")).await;
6785        let eps = log.endpoints().await;
6786        assert_eq!(eps.len(), 2);
6787    }
6788
6789    #[tokio::test]
6790    async fn test_hostnames_dedupes() {
6791        let log = NetworkLog::new();
6792        push_request(&log, make_request("1", "GET", "https://a.com/x")).await;
6793        push_request(&log, make_request("2", "GET", "https://a.com/y")).await;
6794        let hosts = log.hostnames().await;
6795        assert_eq!(hosts.len(), 1);
6796        assert_eq!(hosts[0], "a.com");
6797    }
6798
6799    #[tokio::test]
6800    async fn test_drain_pending_empties_maps() {
6801        let log = NetworkLog::new();
6802        // Push a response out-of-order so it lands in pending
6803        {
6804            let mut inner = log.inner.write().await;
6805            inner
6806                .pending_responses
6807                .insert("1".into(), make_response("1", 200, "https://a.com"));
6808            inner.pending_errors.insert(
6809                "2".into(),
6810                CapturedError {
6811                    id: "2".into(),
6812                    url: "https://b.com".into(),
6813                    error_text: "err".into(),
6814                },
6815            );
6816        }
6817        assert_eq!(log.pending_count().await, 2);
6818        let (resps, errs) = log.drain_pending().await;
6819        assert_eq!(resps.len(), 1);
6820        assert_eq!(errs.len(), 1);
6821        assert_eq!(log.pending_count().await, 0);
6822    }
6823
6824    #[tokio::test]
6825    async fn test_metrics_zero_on_fresh_log() {
6826        let log = NetworkLog::new();
6827        let m = log.metrics().await;
6828        assert_eq!(m.requests_received, 0);
6829        assert_eq!(m.responses_received, 0);
6830        assert_eq!(m.errors_received, 0);
6831        assert_eq!(m.entries_evicted, 0);
6832        assert_eq!(m.broadcast_drops, 0);
6833        assert_eq!(m.duplicate_responses, 0);
6834        assert_eq!(m.duplicate_errors, 0);
6835    }
6836
6837    #[tokio::test]
6838    async fn test_metrics_track_entries() {
6839        let log = NetworkLog::new();
6840        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6841        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6842        // push_request does not increment metrics (those are BiDi handler counters).
6843        // Verify that default metrics are zero on a fresh log.
6844        let m = log.metrics().await;
6845        assert_eq!(m.max_entries, 50_000);
6846        assert_eq!(m.max_entries, log.inner.read().await.max_entries);
6847    }
6848
6849    #[tokio::test]
6850    async fn test_eviction_evicts_oldest_incrementally() {
6851        let log = NetworkLog::with_limits(3, 5);
6852        push_request(&log, make_request("1", "GET", "https://a.com")).await;
6853        push_request(&log, make_request("2", "GET", "https://b.com")).await;
6854        push_request(&log, make_request("3", "GET", "https://c.com")).await;
6855        push_request(&log, make_request("4", "GET", "https://d.com")).await;
6856        push_request(&log, make_request("5", "GET", "https://e.com")).await;
6857
6858        let entries = log.entries().await;
6859        assert_eq!(entries.len(), 3);
6860        assert_eq!(entries[0].request.id, "3");
6861        assert_eq!(entries[1].request.id, "4");
6862        assert_eq!(entries[2].request.id, "5");
6863
6864        let m = log.metrics().await;
6865        assert_eq!(m.entries_evicted, 2);
6866
6867        // Pushing one more evicts exactly one additional oldest entry.
6868        push_request(&log, make_request("6", "GET", "https://f.com")).await;
6869        let entries = log.entries().await;
6870        assert_eq!(entries.len(), 3);
6871        assert_eq!(entries[0].request.id, "4");
6872        let m = log.metrics().await;
6873        assert_eq!(m.entries_evicted, 3);
6874    }
6875
6876    #[tokio::test]
6877    async fn test_filter_status_range_ignores_error_without_status() {
6878        // A fetch error has no response and therefore no status. It must not
6879        // be fabricated as 0 and therefore must not match a range that
6880        // happens to include 0 (which HTTP status codes never do anyway).
6881        let log = NetworkLog::new();
6882        push_entry(
6883            &log,
6884            make_request("1", "GET", "https://a.com"),
6885            None,
6886            Some(make_error("1", "https://a.com", "net::ERR_FAILED")),
6887        )
6888        .await;
6889        // 0 is not a valid HTTP status, but this primarily guards against any
6890        // future code that fabricates 0 for missing statuses.
6891        assert!(log
6892            .filter(Filter::new().status_range(0..=0))
6893            .await
6894            .is_empty());
6895        assert!(log.filter(Filter::new().with_error()).await.len() == 1);
6896    }
6897}