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