Skip to main content

victauri_plugin/
introspection.rs

1//! Backend introspection and chaos engineering types.
2//!
3//! These types support Victauri's intervention capabilities — features that exploit
4//! the plugin's position inside the Rust process to provide insights and control
5//! that browser-external tools like CDP cannot access.
6
7use std::collections::{HashMap, VecDeque};
8use std::sync::RwLock;
9use std::sync::atomic::AtomicBool;
10use std::time::{Duration, Instant};
11
12use serde::Serialize;
13
14/// Per-command timing statistics aggregated from IPC invocations.
15#[derive(Debug, Clone, Serialize)]
16#[non_exhaustive]
17pub struct CommandTimingStats {
18    /// Command name.
19    pub command: String,
20    /// Number of invocations recorded.
21    pub count: u64,
22    /// Minimum execution time in milliseconds.
23    pub min_ms: f64,
24    /// Maximum execution time in milliseconds.
25    pub max_ms: f64,
26    /// Mean execution time in milliseconds.
27    pub avg_ms: f64,
28    /// 95th percentile execution time in milliseconds.
29    pub p95_ms: f64,
30    /// Total execution time across all invocations.
31    pub total_ms: f64,
32}
33
34/// Maximum recent samples retained per command for percentile estimation.
35/// `count`, `total`, `min`, and `max` are tracked as running aggregates, so they
36/// stay accurate over the command's full history; only the p95 estimate is
37/// windowed to this many of the most recent samples. This bounds memory under a
38/// long agent soak that hammers `invoke_command` (the prior `Vec` grew forever
39/// and was re-sorted in full on every `command_timings` read).
40const MAX_TIMING_SAMPLES: usize = 1024;
41
42/// Accumulated timing data for a single command — bounded memory.
43///
44/// Internal accumulator behind `CommandTimings` (crate-private — never part of the public
45/// API, so its layout can evolve freely without a semver break).
46#[derive(Debug, Default)]
47pub(crate) struct TimingSamples {
48    /// Most-recent durations (ring, capped at `MAX_TIMING_SAMPLES`) for p95.
49    recent: VecDeque<Duration>,
50    /// Total invocations recorded (all time).
51    count: u64,
52    /// Sum of all durations (all time) — for accurate mean/total.
53    total: Duration,
54    /// All-time minimum duration.
55    min: Option<Duration>,
56    /// All-time maximum duration.
57    max: Option<Duration>,
58}
59
60impl TimingSamples {
61    /// Add a timing sample.
62    pub fn record(&mut self, duration: Duration) {
63        self.count += 1;
64        self.total = self.total.saturating_add(duration);
65        self.min = Some(self.min.map_or(duration, |m| m.min(duration)));
66        self.max = Some(self.max.map_or(duration, |m| m.max(duration)));
67        if self.recent.len() == MAX_TIMING_SAMPLES {
68            self.recent.pop_front();
69        }
70        self.recent.push_back(duration);
71    }
72
73    /// Compute aggregate statistics. `count`, `min`, `max`, `avg`, and `total`
74    /// reflect the full history; `p95` is estimated over the most recent
75    /// `MAX_TIMING_SAMPLES` samples.
76    #[must_use]
77    pub fn stats(&self, command: &str) -> CommandTimingStats {
78        if self.count == 0 {
79            return CommandTimingStats {
80                command: command.to_string(),
81                count: 0,
82                min_ms: 0.0,
83                max_ms: 0.0,
84                avg_ms: 0.0,
85                p95_ms: 0.0,
86                total_ms: 0.0,
87            };
88        }
89        let to_ms = |d: Duration| d.as_secs_f64() * 1000.0;
90        let round2 = |v: f64| (v * 100.0).round() / 100.0;
91
92        let total_ms = to_ms(self.total);
93        let avg_ms = total_ms / self.count as f64;
94        let min_ms = self.min.map_or(0.0, to_ms);
95        let max_ms = self.max.map_or(0.0, to_ms);
96
97        // p95 over the recent window (bounded; representative of current behavior).
98        let mut sorted: Vec<f64> = self.recent.iter().copied().map(to_ms).collect();
99        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
100        let p95 = if sorted.is_empty() {
101            0.0
102        } else {
103            // Nearest-rank: the ceil(0.95·n)-th value, i.e. 0-based index ceil(0.95·n) − 1.
104            // Without the −1 this returned the MAX for every n ≤ 20.
105            let idx = (((sorted.len() as f64) * 0.95).ceil() as usize).saturating_sub(1);
106            sorted[idx.min(sorted.len() - 1)]
107        };
108
109        CommandTimingStats {
110            command: command.to_string(),
111            count: self.count,
112            min_ms: round2(min_ms),
113            max_ms: round2(max_ms),
114            avg_ms: round2(avg_ms),
115            p95_ms: round2(p95),
116            total_ms: round2(total_ms),
117        }
118    }
119}
120
121/// Maximum distinct commands tracked. The map is keyed by the caller-supplied command name, so
122/// without a cap an agent (or a loop) invoking ever-new names grows it forever. Once full, new
123/// names are not tracked; commands already tracked keep accumulating.
124const MAX_TIMED_COMMANDS: usize = 1024;
125
126/// Thread-safe store for per-command timing data.
127pub struct CommandTimings {
128    inner: RwLock<HashMap<String, TimingSamples>>,
129}
130
131impl CommandTimings {
132    /// Create a new empty store.
133    #[must_use]
134    pub fn new() -> Self {
135        Self {
136            inner: RwLock::new(HashMap::new()),
137        }
138    }
139
140    /// Record a timing sample for a command.
141    pub fn record(&self, command: &str, duration: Duration) {
142        let mut map = self
143            .inner
144            .write()
145            .unwrap_or_else(std::sync::PoisonError::into_inner);
146        if let Some(samples) = map.get_mut(command) {
147            samples.record(duration);
148        } else if map.len() < MAX_TIMED_COMMANDS {
149            map.entry(command.to_string()).or_default().record(duration);
150        }
151    }
152
153    /// Get stats for all commands, sorted by total time descending.
154    #[must_use]
155    pub fn all_stats(&self) -> Vec<CommandTimingStats> {
156        let map = self
157            .inner
158            .read()
159            .unwrap_or_else(std::sync::PoisonError::into_inner);
160        let mut stats: Vec<CommandTimingStats> =
161            map.iter().map(|(name, s)| s.stats(name)).collect();
162        stats.sort_by(|a, b| {
163            b.total_ms
164                .partial_cmp(&a.total_ms)
165                .unwrap_or(std::cmp::Ordering::Equal)
166        });
167        stats
168    }
169
170    /// Get stats for a single command.
171    #[must_use]
172    pub fn stats_for(&self, command: &str) -> Option<CommandTimingStats> {
173        let map = self
174            .inner
175            .read()
176            .unwrap_or_else(std::sync::PoisonError::into_inner);
177        map.get(command).map(|s| s.stats(command))
178    }
179
180    /// Clear all timing data.
181    pub fn clear(&self) {
182        let mut map = self
183            .inner
184            .write()
185            .unwrap_or_else(std::sync::PoisonError::into_inner);
186        map.clear();
187    }
188}
189
190impl Default for CommandTimings {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196// ── Fault Injection ─────────────────────────────────────────────────────────
197
198/// The type of fault to inject into a command.
199#[derive(Debug, Clone, Serialize)]
200#[non_exhaustive]
201pub enum FaultType {
202    /// Add artificial latency before command execution.
203    Delay {
204        /// Delay in milliseconds.
205        delay_ms: u64,
206    },
207    /// Return an error without executing the command.
208    Error {
209        /// Error message to return.
210        message: String,
211    },
212    /// Drop the response entirely (return empty/timeout-like response).
213    Drop,
214    /// Execute normally but corrupt the response (randomize field values).
215    Corrupt,
216}
217
218/// Configuration for a single fault injection rule.
219#[derive(Debug, Clone, Serialize)]
220#[non_exhaustive]
221pub struct FaultConfig {
222    /// Target command name.
223    pub command: String,
224    /// Type of fault to inject.
225    pub fault_type: FaultType,
226    /// Number of times this fault has been triggered.
227    pub trigger_count: u64,
228    /// Maximum number of times to trigger (0 = unlimited).
229    pub max_triggers: u64,
230    /// When this fault was created.
231    #[serde(skip)]
232    pub created_at: Instant,
233}
234
235/// Faults auto-expire this long after creation, so a forgotten fault cannot
236/// silently sabotage or mask a later test run (audit #34). Re-inject to refresh.
237pub const FAULT_TTL: Duration = Duration::from_secs(900); // 15 minutes
238
239impl FaultConfig {
240    /// Creates a fault rule for `command`, created now with zero triggers so far.
241    /// `max_triggers == 0` means unlimited (until [`FAULT_TTL`] expires it).
242    #[must_use]
243    pub fn new(command: impl Into<String>, fault_type: FaultType, max_triggers: u64) -> Self {
244        Self {
245            command: command.into(),
246            fault_type,
247            trigger_count: 0,
248            max_triggers,
249            created_at: Instant::now(),
250        }
251    }
252
253    /// Whether this fault should still trigger, evaluated at `now`. A fault is
254    /// inert once it is older than [`FAULT_TTL`] or has hit `max_triggers`.
255    #[must_use]
256    pub fn should_trigger_at(&self, now: Instant) -> bool {
257        if now.saturating_duration_since(self.created_at) >= FAULT_TTL {
258            return false;
259        }
260        self.max_triggers == 0 || self.trigger_count < self.max_triggers
261    }
262
263    /// Check if this fault should still trigger right now.
264    #[must_use]
265    pub fn should_trigger(&self) -> bool {
266        self.should_trigger_at(Instant::now())
267    }
268}
269
270/// Most fault rules held at once (G-10). Rules are keyed by the caller-chosen command name; at
271/// the cap, injecting a rule for a NEW command evicts the oldest-injected rule.
272pub(crate) const MAX_FAULT_RULES: usize = 256;
273
274/// Thread-safe registry of active fault injection rules.
275pub struct FaultRegistry {
276    inner: RwLock<FaultRules>,
277}
278
279#[derive(Default)]
280struct FaultRules {
281    /// Injection order, so eviction at the cap is deterministic (oldest first).
282    next_seq: u64,
283    map: HashMap<String, (u64, FaultConfig)>,
284}
285
286impl FaultRules {
287    /// Drop rules past [`FAULT_TTL`]: they can never trigger again, and were kept (and listed)
288    /// forever before (G-10).
289    fn evict_expired(&mut self, now: Instant) {
290        self.map
291            .retain(|_, (_, c)| now.saturating_duration_since(c.created_at) < FAULT_TTL);
292    }
293}
294
295impl FaultRegistry {
296    /// Create an empty registry.
297    #[must_use]
298    pub fn new() -> Self {
299        Self {
300            inner: RwLock::new(FaultRules::default()),
301        }
302    }
303
304    /// Register a fault for a command (replacing any rule for the same command). Expired rules
305    /// are evicted first; if the registry still holds its maximum (256) rules, the
306    /// oldest-injected rule is evicted to make room.
307    pub fn inject(&self, config: FaultConfig) {
308        let mut rules = self
309            .inner
310            .write()
311            .unwrap_or_else(std::sync::PoisonError::into_inner);
312        rules.evict_expired(Instant::now());
313        if !rules.map.contains_key(&config.command) && rules.map.len() >= MAX_FAULT_RULES {
314            let oldest = rules
315                .map
316                .iter()
317                .min_by_key(|(_, (seq, _))| *seq)
318                .map(|(k, _)| k.clone());
319            if let Some(oldest) = oldest {
320                rules.map.remove(&oldest);
321            }
322        }
323        let seq = rules.next_seq;
324        rules.next_seq += 1;
325        rules.map.insert(config.command.clone(), (seq, config));
326    }
327
328    /// Look up and optionally trigger a fault for a command.
329    /// Returns the fault type if one is active and should trigger.
330    pub fn check_and_trigger(&self, command: &str) -> Option<FaultType> {
331        let mut rules = self
332            .inner
333            .write()
334            .unwrap_or_else(std::sync::PoisonError::into_inner);
335        if let Some((_, config)) = rules.map.get_mut(command)
336            && config.should_trigger()
337        {
338            config.trigger_count += 1;
339            return Some(config.fault_type.clone());
340        }
341        None
342    }
343
344    /// List the fault rules that have not expired (expired ones are evicted here too).
345    #[must_use]
346    pub fn list(&self) -> Vec<FaultConfig> {
347        let mut rules = self
348            .inner
349            .write()
350            .unwrap_or_else(std::sync::PoisonError::into_inner);
351        rules.evict_expired(Instant::now());
352        rules.map.values().map(|(_, c)| c.clone()).collect()
353    }
354
355    /// Remove a fault rule for a command.
356    pub fn clear(&self, command: &str) -> bool {
357        let mut rules = self
358            .inner
359            .write()
360            .unwrap_or_else(std::sync::PoisonError::into_inner);
361        rules.map.remove(command).is_some()
362    }
363
364    /// Remove all fault rules.
365    pub fn clear_all(&self) -> usize {
366        let mut rules = self
367            .inner
368            .write()
369            .unwrap_or_else(std::sync::PoisonError::into_inner);
370        let count = rules.map.len();
371        rules.map.clear();
372        count
373    }
374}
375
376impl Default for FaultRegistry {
377    fn default() -> Self {
378        Self::new()
379    }
380}
381
382// ── IPC Contract Testing ────────────────────────────────────────────────────
383
384/// Describes the shape of a JSON value for contract comparison.
385#[derive(Debug, Clone, Serialize, PartialEq)]
386#[non_exhaustive]
387pub enum JsonShape {
388    /// null
389    Null,
390    /// boolean
391    Bool,
392    /// number (integer or float)
393    Number,
394    /// string
395    String,
396    /// array with element shape (from first element, or Null if empty)
397    Array(Box<Self>),
398    /// object with field names and their shapes
399    Object(HashMap<String, Self>),
400}
401
402impl JsonShape {
403    /// Extract the shape of a JSON value.
404    #[must_use]
405    pub fn from_value(value: &serde_json::Value) -> Self {
406        match value {
407            serde_json::Value::Null => Self::Null,
408            serde_json::Value::Bool(_) => Self::Bool,
409            serde_json::Value::Number(_) => Self::Number,
410            serde_json::Value::String(_) => Self::String,
411            serde_json::Value::Array(arr) => {
412                let elem = arr.first().map_or(Self::Null, Self::from_value);
413                Self::Array(Box::new(elem))
414            }
415            serde_json::Value::Object(obj) => {
416                let fields: HashMap<String, Self> = obj
417                    .iter()
418                    .map(|(k, v)| (k.clone(), Self::from_value(v)))
419                    .collect();
420                Self::Object(fields)
421            }
422        }
423    }
424
425    /// Human-readable type name.
426    #[must_use]
427    pub fn type_name(&self) -> &'static str {
428        match self {
429            Self::Null => "null",
430            Self::Bool => "bool",
431            Self::Number => "number",
432            Self::String => "string",
433            Self::Array(_) => "array",
434            Self::Object(_) => "object",
435        }
436    }
437}
438
439/// A recorded contract baseline for a command's response.
440#[derive(Debug, Clone, Serialize)]
441#[non_exhaustive]
442pub struct ContractBaseline {
443    /// Command name.
444    pub command: String,
445    /// Arguments used when recording.
446    pub args: serde_json::Value,
447    /// Shape of the response.
448    pub shape: JsonShape,
449    /// Raw sample response (first 4KB).
450    pub sample: String,
451    /// When this baseline was recorded.
452    pub recorded_at: String,
453}
454
455impl ContractBaseline {
456    /// Creates a contract baseline for `command`.
457    #[must_use]
458    pub fn new(
459        command: impl Into<String>,
460        args: serde_json::Value,
461        shape: JsonShape,
462        sample: impl Into<String>,
463        recorded_at: impl Into<String>,
464    ) -> Self {
465        Self {
466            command: command.into(),
467            args,
468            shape,
469            sample: sample.into(),
470            recorded_at: recorded_at.into(),
471        }
472    }
473}
474
475/// Differences found when checking a contract against baseline.
476#[derive(Debug, Clone, Serialize)]
477#[non_exhaustive]
478pub struct ContractDrift {
479    /// Command name.
480    pub command: String,
481    /// Fields present in current but not in baseline.
482    pub new_fields: Vec<String>,
483    /// Fields present in baseline but not in current.
484    pub removed_fields: Vec<String>,
485    /// Fields whose type changed.
486    pub type_changes: Vec<TypeChange>,
487    /// Whether the overall shape matches.
488    pub shape_matches: bool,
489}
490
491/// A single field type change between baseline and current.
492#[derive(Debug, Clone, Serialize)]
493#[non_exhaustive]
494pub struct TypeChange {
495    /// Dot-separated field path.
496    pub path: String,
497    /// Type in the baseline.
498    pub baseline_type: String,
499    /// Type in the current response.
500    pub current_type: String,
501}
502
503/// Compare two JSON shapes and report differences.
504#[must_use]
505pub fn diff_shapes(baseline: &JsonShape, current: &JsonShape, prefix: &str) -> ContractDrift {
506    let mut new_fields = Vec::new();
507    let mut removed_fields = Vec::new();
508    let mut type_changes = Vec::new();
509
510    diff_shapes_inner(
511        baseline,
512        current,
513        prefix,
514        &mut new_fields,
515        &mut removed_fields,
516        &mut type_changes,
517    );
518
519    let shape_matches =
520        new_fields.is_empty() && removed_fields.is_empty() && type_changes.is_empty();
521    ContractDrift {
522        command: prefix.to_string(),
523        new_fields,
524        removed_fields,
525        type_changes,
526        shape_matches,
527    }
528}
529
530fn diff_shapes_inner(
531    baseline: &JsonShape,
532    current: &JsonShape,
533    prefix: &str,
534    new_fields: &mut Vec<String>,
535    removed_fields: &mut Vec<String>,
536    type_changes: &mut Vec<TypeChange>,
537) {
538    match (baseline, current) {
539        (JsonShape::Object(b_fields), JsonShape::Object(c_fields)) => {
540            for (key, b_shape) in b_fields {
541                let path = if prefix.is_empty() {
542                    key.clone()
543                } else {
544                    format!("{prefix}.{key}")
545                };
546                if let Some(c_shape) = c_fields.get(key) {
547                    diff_shapes_inner(
548                        b_shape,
549                        c_shape,
550                        &path,
551                        new_fields,
552                        removed_fields,
553                        type_changes,
554                    );
555                } else {
556                    removed_fields.push(path);
557                }
558            }
559            for key in c_fields.keys() {
560                if !b_fields.contains_key(key) {
561                    let path = if prefix.is_empty() {
562                        key.clone()
563                    } else {
564                        format!("{prefix}.{key}")
565                    };
566                    new_fields.push(path);
567                }
568            }
569        }
570        (JsonShape::Array(b_elem), JsonShape::Array(c_elem)) => {
571            let path = format!("{prefix}[]");
572            diff_shapes_inner(
573                b_elem,
574                c_elem,
575                &path,
576                new_fields,
577                removed_fields,
578                type_changes,
579            );
580        }
581        (b, c) if b.type_name() != c.type_name() => {
582            type_changes.push(TypeChange {
583                path: prefix.to_string(),
584                baseline_type: b.type_name().to_string(),
585                current_type: c.type_name().to_string(),
586            });
587        }
588        _ => {}
589    }
590}
591
592/// Most contract baselines held at once (G-10). At the cap, recording a baseline for a NEW
593/// command evicts the least recently recorded one (the store never refuses a recording).
594pub(crate) const MAX_CONTRACT_BASELINES: usize = 1024;
595
596/// Thread-safe store for IPC contract baselines.
597pub struct ContractStore {
598    inner: RwLock<ContractBaselines>,
599}
600
601#[derive(Default)]
602struct ContractBaselines {
603    /// Recording order, so eviction at the cap is deterministic (least recently recorded).
604    next_seq: u64,
605    map: HashMap<String, (u64, ContractBaseline)>,
606}
607
608impl ContractStore {
609    /// Create an empty store.
610    #[must_use]
611    pub fn new() -> Self {
612        Self {
613            inner: RwLock::new(ContractBaselines::default()),
614        }
615    }
616
617    /// Record a baseline for a command, replacing (and refreshing) any earlier one for it. The
618    /// store holds at most 1024 baselines: at that cap, recording one for a new command evicts
619    /// the least recently recorded baseline, whose command is returned.
620    pub fn record(&self, baseline: ContractBaseline) -> Option<String> {
621        let mut store = self
622            .inner
623            .write()
624            .unwrap_or_else(std::sync::PoisonError::into_inner);
625        let mut evicted = None;
626        if !store.map.contains_key(&baseline.command) && store.map.len() >= MAX_CONTRACT_BASELINES {
627            evicted = store
628                .map
629                .iter()
630                .min_by_key(|(_, (seq, _))| *seq)
631                .map(|(k, _)| k.clone());
632            if let Some(oldest) = &evicted {
633                store.map.remove(oldest);
634            }
635        }
636        let seq = store.next_seq;
637        store.next_seq += 1;
638        store.map.insert(baseline.command.clone(), (seq, baseline));
639        evicted
640    }
641
642    /// Get the baseline for a command.
643    #[must_use]
644    pub fn get(&self, command: &str) -> Option<ContractBaseline> {
645        let store = self
646            .inner
647            .read()
648            .unwrap_or_else(std::sync::PoisonError::into_inner);
649        store.map.get(command).map(|(_, b)| b.clone())
650    }
651
652    /// Get all baselines.
653    #[must_use]
654    pub fn all(&self) -> Vec<ContractBaseline> {
655        let store = self
656            .inner
657            .read()
658            .unwrap_or_else(std::sync::PoisonError::into_inner);
659        store.map.values().map(|(_, b)| b.clone()).collect()
660    }
661
662    /// Clear all baselines.
663    pub fn clear(&self) -> usize {
664        let mut store = self
665            .inner
666            .write()
667            .unwrap_or_else(std::sync::PoisonError::into_inner);
668        let count = store.map.len();
669        store.map.clear();
670        count
671    }
672}
673
674impl Default for ContractStore {
675    fn default() -> Self {
676        Self::new()
677    }
678}
679
680// ── Startup Profiling ───────────────────────────────────────────────────────
681
682/// A single phase in the startup timeline.
683#[derive(Debug, Clone, Serialize)]
684#[non_exhaustive]
685pub struct StartupPhase {
686    /// Phase name.
687    pub name: String,
688    /// Duration of this phase in milliseconds.
689    pub duration_ms: f64,
690    /// Cumulative time from plugin init start.
691    pub cumulative_ms: f64,
692}
693
694/// Records timestamps at key phases during plugin initialization.
695pub struct StartupTimeline {
696    start: Instant,
697    phases: RwLock<Vec<(String, Instant)>>,
698}
699
700impl StartupTimeline {
701    /// Begin recording from now.
702    #[must_use]
703    pub fn new() -> Self {
704        Self {
705            start: Instant::now(),
706            phases: RwLock::new(Vec::new()),
707        }
708    }
709
710    /// Mark a phase as completed.
711    pub fn mark(&self, name: &str) {
712        let mut phases = self
713            .phases
714            .write()
715            .unwrap_or_else(std::sync::PoisonError::into_inner);
716        phases.push((name.to_string(), Instant::now()));
717    }
718
719    /// Get the timeline as a list of phases with durations.
720    #[must_use]
721    pub fn report(&self) -> Vec<StartupPhase> {
722        let phases = self
723            .phases
724            .read()
725            .unwrap_or_else(std::sync::PoisonError::into_inner);
726        let mut result = Vec::new();
727        let mut prev = self.start;
728
729        for (name, instant) in phases.iter() {
730            let duration = instant.duration_since(prev);
731            let cumulative = instant.duration_since(self.start);
732            result.push(StartupPhase {
733                name: name.clone(),
734                duration_ms: (duration.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
735                cumulative_ms: (cumulative.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
736            });
737            prev = *instant;
738        }
739        result
740    }
741
742    /// Total time from start to last recorded phase.
743    #[must_use]
744    pub fn total_ms(&self) -> f64 {
745        let phases = self
746            .phases
747            .read()
748            .unwrap_or_else(std::sync::PoisonError::into_inner);
749        if let Some((_, last)) = phases.last() {
750            (last.duration_since(self.start).as_secs_f64() * 1000.0 * 100.0).round() / 100.0
751        } else {
752            0.0
753        }
754    }
755}
756
757impl Default for StartupTimeline {
758    fn default() -> Self {
759        Self::new()
760    }
761}
762
763// ── Tauri Event Bus Monitor ─────────────────────────────────────────────
764
765/// A Tauri event captured from the application's native event bus.
766#[derive(Debug, Clone, Serialize)]
767#[non_exhaustive]
768pub struct CapturedTauriEvent {
769    /// Event name (e.g. "notification-added", `tauri://focus`).
770    pub name: String,
771    /// Serialized event payload.
772    pub payload: String,
773    /// ISO 8601 timestamp.
774    pub timestamp: String,
775}
776
777impl CapturedTauriEvent {
778    /// Creates a captured event record (`timestamp` is an ISO 8601 string).
779    #[must_use]
780    pub fn new(
781        name: impl Into<String>,
782        payload: impl Into<String>,
783        timestamp: impl Into<String>,
784    ) -> Self {
785        Self {
786            name: name.into(),
787            payload: payload.into(),
788            timestamp: timestamp.into(),
789        }
790    }
791}
792
793const DEFAULT_EVENT_BUS_CAPACITY: usize = 1000;
794
795/// Thread-safe ring buffer for captured Tauri events.
796#[derive(Clone)]
797pub struct EventBusMonitor {
798    inner: std::sync::Arc<RwLock<VecDeque<CapturedTauriEvent>>>,
799    capacity: usize,
800}
801
802impl EventBusMonitor {
803    /// Create a new monitor with the given capacity.
804    #[must_use]
805    pub fn new(capacity: usize) -> Self {
806        Self {
807            inner: std::sync::Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
808            capacity,
809        }
810    }
811
812    /// Record a captured event.
813    pub fn push(&self, event: CapturedTauriEvent) {
814        let mut buf = self
815            .inner
816            .write()
817            .unwrap_or_else(std::sync::PoisonError::into_inner);
818        if buf.len() >= self.capacity {
819            buf.pop_front();
820        }
821        buf.push_back(event);
822    }
823
824    /// Get all captured events.
825    #[must_use]
826    pub fn events(&self) -> Vec<CapturedTauriEvent> {
827        self.inner
828            .read()
829            .unwrap_or_else(std::sync::PoisonError::into_inner)
830            .iter()
831            .cloned()
832            .collect()
833    }
834
835    /// Get the number of captured events.
836    #[must_use]
837    pub fn len(&self) -> usize {
838        self.inner
839            .read()
840            .unwrap_or_else(std::sync::PoisonError::into_inner)
841            .len()
842    }
843
844    /// Returns true if no events have been captured.
845    #[must_use]
846    pub fn is_empty(&self) -> bool {
847        self.len() == 0
848    }
849
850    /// Clear all captured events, returning how many were removed.
851    pub fn clear(&self) -> usize {
852        let mut buf = self
853            .inner
854            .write()
855            .unwrap_or_else(std::sync::PoisonError::into_inner);
856        let count = buf.len();
857        buf.clear();
858        count
859    }
860}
861
862impl Default for EventBusMonitor {
863    fn default() -> Self {
864        Self::new(DEFAULT_EVENT_BUS_CAPACITY)
865    }
866}
867
868// ── Application State Probes ─────────────────────────────────────────────
869
870/// A named closure that returns a snapshot of application-specific backend state
871/// as JSON. Registered via [`VictauriBuilder::probe`](crate::VictauriBuilder::probe).
872pub type ProbeFn = dyn Fn() -> serde_json::Value + Send + Sync + 'static;
873
874/// Registry of application-defined state probes surfaced through the `app_state`
875/// MCP tool.
876///
877/// Probes give an agent first-class, discoverable access to domain state that
878/// would otherwise require `query_db` + log-grepping (e.g. a scoring pipeline's
879/// version, queue depth, or cache stats). Because a probe runs in the Rust
880/// process with direct access to whatever state the app captured into it, it
881/// reads backend state with **no IPC round-trip and no frontend involvement** —
882/// the kind of introspection a browser-external tool like CDP cannot do.
883#[derive(Clone, Default)]
884pub struct AppStateProbes {
885    inner: std::sync::Arc<RwLock<std::collections::BTreeMap<String, std::sync::Arc<ProbeFn>>>>,
886}
887
888impl AppStateProbes {
889    /// Register (or replace) a probe under `name`.
890    pub fn register(&self, name: impl Into<String>, probe: std::sync::Arc<ProbeFn>) {
891        self.inner
892            .write()
893            .unwrap_or_else(std::sync::PoisonError::into_inner)
894            .insert(name.into(), probe);
895    }
896
897    /// Sorted list of registered probe names.
898    #[must_use]
899    pub fn names(&self) -> Vec<String> {
900        self.inner
901            .read()
902            .unwrap_or_else(std::sync::PoisonError::into_inner)
903            .keys()
904            .cloned()
905            .collect()
906    }
907
908    /// Run the named probe and return its JSON snapshot, or `None` if no probe is
909    /// registered under that name.
910    #[must_use]
911    pub fn run(&self, name: &str) -> Option<serde_json::Value> {
912        self.get(name).map(|p| p())
913    }
914
915    /// The probe registered under `name`, to run somewhere other than the caller's thread
916    /// (the `app_state` tool runs it on the blocking pool with a timeout).
917    #[must_use]
918    pub fn get(&self, name: &str) -> Option<std::sync::Arc<ProbeFn>> {
919        self.inner
920            .read()
921            .unwrap_or_else(std::sync::PoisonError::into_inner)
922            .get(name)
923            .cloned()
924    }
925
926    /// Number of registered probes.
927    #[must_use]
928    pub fn len(&self) -> usize {
929        self.inner
930            .read()
931            .unwrap_or_else(std::sync::PoisonError::into_inner)
932            .len()
933    }
934
935    /// Returns true if no probes are registered.
936    #[must_use]
937    pub fn is_empty(&self) -> bool {
938        self.len() == 0
939    }
940}
941
942// ── Internal Task Tracker ──────────────────────────────────────────────
943
944/// Info about a tracked async task spawned by Victauri.
945#[derive(Debug, Clone, Serialize)]
946#[non_exhaustive]
947pub struct TrackedTaskInfo {
948    /// Human-readable task name.
949    pub name: String,
950    /// ISO 8601 timestamp when the task was spawned.
951    pub spawned_at: String,
952    /// Whether the task has finished (completed or errored).
953    pub is_finished: bool,
954    /// How long the task has been running in seconds.
955    pub uptime_secs: u64,
956}
957
958struct TrackedTaskEntry {
959    name: String,
960    spawned_at: Instant,
961    spawned_at_wall: String,
962    finished: std::sync::Arc<AtomicBool>,
963}
964
965/// Tracks Victauri's own spawned async tasks for observability.
966pub struct TaskTracker {
967    tasks: RwLock<Vec<TrackedTaskEntry>>,
968}
969
970impl TaskTracker {
971    /// Create a new empty tracker.
972    #[must_use]
973    pub fn new() -> Self {
974        Self {
975            tasks: RwLock::new(Vec::new()),
976        }
977    }
978
979    /// Register a new task. Returns a flag that the task should set to `true` when it finishes.
980    pub fn track(&self, name: &str) -> std::sync::Arc<AtomicBool> {
981        let finished = std::sync::Arc::new(AtomicBool::new(false));
982        let entry = TrackedTaskEntry {
983            name: name.to_string(),
984            spawned_at: Instant::now(),
985            spawned_at_wall: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
986            finished: finished.clone(),
987        };
988        self.tasks
989            .write()
990            .unwrap_or_else(std::sync::PoisonError::into_inner)
991            .push(entry);
992        finished
993    }
994
995    /// Register a new task and return a guard that marks it finished when DROPPED — at the end
996    /// of the task body, when the task panics (the drop runs during unwinding), or when its
997    /// future is dropped unfinished. Setting the flag by hand after the body left a panicked
998    /// task reported active forever (G-9).
999    pub(crate) fn track_guarded(&self, name: &str) -> TaskFinishedGuard {
1000        TaskFinishedGuard(self.track(name))
1001    }
1002
1003    /// List all tracked tasks with their current status.
1004    #[must_use]
1005    pub fn list(&self) -> Vec<TrackedTaskInfo> {
1006        let tasks = self
1007            .tasks
1008            .read()
1009            .unwrap_or_else(std::sync::PoisonError::into_inner);
1010        tasks
1011            .iter()
1012            .map(|t| TrackedTaskInfo {
1013                name: t.name.clone(),
1014                spawned_at: t.spawned_at_wall.clone(),
1015                is_finished: t.finished.load(std::sync::atomic::Ordering::Relaxed),
1016                uptime_secs: t.spawned_at.elapsed().as_secs(),
1017            })
1018            .collect()
1019    }
1020
1021    /// Count of active (non-finished) tasks.
1022    #[must_use]
1023    pub fn active_count(&self) -> usize {
1024        let tasks = self
1025            .tasks
1026            .read()
1027            .unwrap_or_else(std::sync::PoisonError::into_inner);
1028        tasks
1029            .iter()
1030            .filter(|t| !t.finished.load(std::sync::atomic::Ordering::Relaxed))
1031            .count()
1032    }
1033}
1034
1035impl Default for TaskTracker {
1036    fn default() -> Self {
1037        Self::new()
1038    }
1039}
1040
1041/// Marks a tracked task finished when dropped (see [`TaskTracker::track_guarded`]). Move it
1042/// into the spawned task and hold it for the task's whole body.
1043#[must_use = "the task is marked finished as soon as the guard is dropped"]
1044pub(crate) struct TaskFinishedGuard(std::sync::Arc<AtomicBool>);
1045
1046impl Drop for TaskFinishedGuard {
1047    fn drop(&mut self) {
1048        self.0.store(true, std::sync::atomic::Ordering::Relaxed);
1049    }
1050}
1051
1052// ── Child Process Enumeration ──────────────────────────────────────────
1053
1054/// Information about a child process of the Tauri application.
1055#[derive(Debug, Clone, Serialize)]
1056#[non_exhaustive]
1057pub struct ChildProcessInfo {
1058    /// Process ID.
1059    pub pid: u32,
1060    /// Parent process ID.
1061    pub ppid: u32,
1062    /// Executable name (not full path).
1063    pub name: String,
1064    /// Memory usage in bytes (working set / RSS), if available.
1065    pub memory_bytes: Option<u64>,
1066}
1067
1068/// Enumerate child processes of the current process.
1069///
1070/// Uses platform-native APIs:
1071/// - Windows: `CreateToolhelp32Snapshot` + `Process32First/Next`
1072/// - Linux: `/proc/` filesystem
1073/// - macOS: `proc_listpids` + `proc_pidinfo`
1074#[must_use]
1075pub fn enumerate_child_processes() -> Vec<ChildProcessInfo> {
1076    let my_pid = std::process::id();
1077
1078    #[cfg(windows)]
1079    {
1080        enumerate_children_windows(my_pid)
1081    }
1082
1083    #[cfg(target_os = "linux")]
1084    {
1085        enumerate_children_linux(my_pid)
1086    }
1087
1088    #[cfg(target_os = "macos")]
1089    {
1090        enumerate_children_macos(my_pid)
1091    }
1092
1093    #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
1094    {
1095        let _ = my_pid;
1096        Vec::new()
1097    }
1098}
1099
1100#[cfg(windows)]
1101#[allow(unsafe_code)]
1102fn enumerate_children_windows(parent_pid: u32) -> Vec<ChildProcessInfo> {
1103    use windows::Win32::Foundation::CloseHandle;
1104    use windows::Win32::System::Diagnostics::ToolHelp::{
1105        CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS,
1106    };
1107
1108    let mut children = Vec::new();
1109
1110    // SAFETY: `CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)` creates a
1111    // read-only snapshot of all running processes. The returned handle is
1112    // closed via `CloseHandle` when we're done. `Process32First/Next` iterate
1113    // the snapshot entries.
1114    unsafe {
1115        let Ok(snapshot) = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) else {
1116            return children;
1117        };
1118
1119        let mut entry: PROCESSENTRY32 = std::mem::zeroed();
1120        entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
1121
1122        if Process32First(snapshot, &mut entry).is_ok() {
1123            loop {
1124                if entry.th32ParentProcessID == parent_pid && entry.th32ProcessID != parent_pid {
1125                    let name_bytes: Vec<u8> = entry
1126                        .szExeFile
1127                        .iter()
1128                        .take_while(|&&b| b != 0)
1129                        .map(|&b| b as u8)
1130                        .collect();
1131                    let name = String::from_utf8_lossy(&name_bytes).to_string();
1132
1133                    let memory_bytes = get_process_memory_windows(entry.th32ProcessID);
1134
1135                    children.push(ChildProcessInfo {
1136                        pid: entry.th32ProcessID,
1137                        ppid: entry.th32ParentProcessID,
1138                        name,
1139                        memory_bytes,
1140                    });
1141                }
1142
1143                if Process32Next(snapshot, &mut entry).is_err() {
1144                    break;
1145                }
1146            }
1147        }
1148
1149        let _ = CloseHandle(snapshot);
1150    }
1151
1152    children
1153}
1154
1155#[cfg(windows)]
1156#[allow(unsafe_code)]
1157fn get_process_memory_windows(pid: u32) -> Option<u64> {
1158    use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
1159    use windows::Win32::System::Threading::{
1160        OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ,
1161    };
1162
1163    // SAFETY: `OpenProcess` with `PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ`
1164    // opens a limited handle for reading memory stats. The process handle is closed
1165    // automatically when dropped (windows crate handles this).
1166    unsafe {
1167        let process = OpenProcess(
1168            PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
1169            false,
1170            pid,
1171        )
1172        .ok()?;
1173
1174        let mut counters: PROCESS_MEMORY_COUNTERS = std::mem::zeroed();
1175        counters.cb = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
1176
1177        if GetProcessMemoryInfo(process, &mut counters, counters.cb).is_ok() {
1178            Some(counters.WorkingSetSize as u64)
1179        } else {
1180            None
1181        }
1182    }
1183}
1184
1185#[cfg(target_os = "linux")]
1186fn enumerate_children_linux(parent_pid: u32) -> Vec<ChildProcessInfo> {
1187    let mut children = Vec::new();
1188    let Ok(entries) = std::fs::read_dir("/proc") else {
1189        return children;
1190    };
1191
1192    for entry in entries.flatten() {
1193        let file_name = entry.file_name();
1194        let Some(pid_str) = file_name.to_str() else {
1195            continue;
1196        };
1197        let Ok(pid) = pid_str.parse::<u32>() else {
1198            continue;
1199        };
1200
1201        let status_path = format!("/proc/{pid}/status");
1202        let Ok(status) = std::fs::read_to_string(&status_path) else {
1203            continue;
1204        };
1205
1206        let mut ppid: Option<u32> = None;
1207        let mut name = String::new();
1208        let mut vm_rss_kb: u64 = 0;
1209
1210        for line in status.lines() {
1211            if let Some(v) = line.strip_prefix("PPid:\t") {
1212                ppid = v.trim().parse().ok();
1213            } else if let Some(v) = line.strip_prefix("Name:\t") {
1214                name = v.trim().to_string();
1215            } else if let Some(v) = line.strip_prefix("VmRSS:") {
1216                vm_rss_kb = v
1217                    .split_whitespace()
1218                    .next()
1219                    .and_then(|n| n.parse().ok())
1220                    .unwrap_or(0);
1221            }
1222        }
1223
1224        if ppid == Some(parent_pid) {
1225            children.push(ChildProcessInfo {
1226                pid,
1227                ppid: parent_pid,
1228                name,
1229                memory_bytes: if vm_rss_kb > 0 {
1230                    Some(vm_rss_kb * 1024)
1231                } else {
1232                    None
1233                },
1234            });
1235        }
1236    }
1237
1238    children
1239}
1240
1241#[cfg(target_os = "macos")]
1242#[allow(unsafe_code)]
1243fn enumerate_children_macos(parent_pid: u32) -> Vec<ChildProcessInfo> {
1244    use std::mem;
1245
1246    unsafe extern "C" {
1247        fn proc_listchildpids(ppid: i32, buffer: *mut i32, buffersize: i32) -> i32;
1248        fn proc_pidinfo(pid: i32, flavor: i32, arg: u64, buffer: *mut u8, buffersize: i32) -> i32;
1249        fn proc_name(pid: i32, buffer: *mut u8, buffersize: u32) -> i32;
1250    }
1251
1252    const PROC_PIDTASKINFO: i32 = 4;
1253
1254    #[repr(C)]
1255    struct ProcTaskInfo {
1256        pti_virtual_size: u64,
1257        pti_resident_size: u64,
1258        pti_total_user: u64,
1259        pti_total_system: u64,
1260        pti_threads_user: u64,
1261        pti_threads_system: u64,
1262        pti_policy: i32,
1263        pti_faults: i32,
1264        pti_pageins: i32,
1265        pti_cow_faults: i32,
1266        pti_messages_sent: i32,
1267        pti_messages_received: i32,
1268        pti_syscalls_mach: i32,
1269        pti_syscalls_unix: i32,
1270        pti_csw: i32,
1271        pti_threadnum: i32,
1272        pti_numrunning: i32,
1273        pti_priority: i32,
1274    }
1275
1276    let mut children = Vec::new();
1277
1278    // SAFETY: `proc_listchildpids` populates a buffer of child PIDs for the given
1279    // parent PID. We first call with a zero buffer to get the count, then allocate
1280    // and call again. `proc_name` and `proc_pidinfo` read metadata for a given PID.
1281    unsafe {
1282        let ppid = parent_pid as i32;
1283        // `proc_listchildpids` returns the COUNT of child PIDs written, not bytes —
1284        // verified empirically on macOS 26 / arm64 (1 child → returns 1, pids[0] is
1285        // the child). The old code divided the return by `size_of::<i32>()`, so a
1286        // single child (1/4 = 0) always enumerated as zero. Allocate a generous
1287        // buffer and call directly; if the returned count meets our capacity the list
1288        // may be truncated, so grow and retry.
1289        let mut cap = 256usize;
1290        let (pids, n) = loop {
1291            let mut pids = vec![0i32; cap];
1292            let buf_size = (cap * mem::size_of::<i32>()) as i32;
1293            let actual = proc_listchildpids(ppid, pids.as_mut_ptr(), buf_size);
1294            if actual <= 0 {
1295                return children;
1296            }
1297            let count = actual as usize;
1298            // `count` is clamped to `cap` for the slice below (stays in bounds even if
1299            // the syscall ever reports a total larger than the buffer it filled).
1300            if count < cap || cap >= 65536 {
1301                break (pids, count.min(cap));
1302            }
1303            cap = (count + 16).max(cap * 2);
1304        };
1305        for &pid in &pids[..n] {
1306            if pid <= 0 {
1307                continue;
1308            }
1309
1310            let mut name_buf = [0u8; 256];
1311            let name_len = proc_name(pid, name_buf.as_mut_ptr(), 256);
1312            let name = if name_len > 0 {
1313                String::from_utf8_lossy(&name_buf[..name_len as usize]).to_string()
1314            } else {
1315                String::from("<unknown>")
1316            };
1317
1318            let mut task_info: ProcTaskInfo = mem::zeroed();
1319            let info_size = mem::size_of::<ProcTaskInfo>() as i32;
1320            let ret = proc_pidinfo(
1321                pid,
1322                PROC_PIDTASKINFO,
1323                0,
1324                &mut task_info as *mut _ as *mut u8,
1325                info_size,
1326            );
1327
1328            let memory_bytes = if ret == info_size {
1329                Some(task_info.pti_resident_size)
1330            } else {
1331                None
1332            };
1333
1334            children.push(ChildProcessInfo {
1335                pid: pid as u32,
1336                ppid: parent_pid,
1337                name,
1338                memory_bytes,
1339            });
1340        }
1341    }
1342
1343    children
1344}
1345
1346// ── Page loads ─────────────────────────────────────────────────────────────
1347
1348/// Each window's latest bridge ready signal — sent whenever its JS bridge (re)initializes, i.e.
1349/// on every page load or reload — with the nonce identifying that page load. An eval running in
1350/// a page that has since been replaced can never deliver its result, so a ready signal from a
1351/// DIFFERENT page lets the caller fail fast ("the page reloaded while the call was in flight")
1352/// instead of waiting out the full timeout.
1353#[derive(Default)]
1354pub(crate) struct PageLoads {
1355    last_load: std::sync::Mutex<HashMap<String, PageLoad>>,
1356    seq: std::sync::atomic::AtomicU64,
1357    changed: tokio::sync::Notify,
1358}
1359
1360/// One recorded ready signal.
1361#[derive(Debug, Clone, PartialEq, Eq)]
1362pub(crate) struct PageLoad {
1363    /// Increases with every ready signal (from any window), so a caller can tell "a new signal
1364    /// since I looked" from "the same one".
1365    pub seq: u64,
1366    /// The page-load nonce the signal carried (`None` from a bridge that sends none).
1367    pub nonce: Option<String>,
1368}
1369
1370/// Longest nonce kept from a ready signal (the bridge's is a UUID; the signal is page-callable).
1371const MAX_PAGE_NONCE_LEN: usize = 128;
1372
1373/// Most windows whose latest page load is remembered (G-10: an entry per window label ever seen
1374/// was kept forever). At the cap, the window that loaded least recently is forgotten.
1375const MAX_PAGE_LOAD_WINDOWS: usize = 256;
1376
1377impl PageLoads {
1378    /// Record that window `label` sent a ready signal for the page identified by `nonce`.
1379    pub fn record_load(&self, label: &str, nonce: Option<&str>) {
1380        let nonce = nonce.map(|n| n.chars().take(MAX_PAGE_NONCE_LEN).collect());
1381        {
1382            let mut loads = self
1383                .last_load
1384                .lock()
1385                .unwrap_or_else(std::sync::PoisonError::into_inner);
1386            // Numbered under the lock, so a window's stored seq only ever grows.
1387            let seq = self.seq.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
1388            if !loads.contains_key(label) && loads.len() >= MAX_PAGE_LOAD_WINDOWS {
1389                let oldest = loads
1390                    .iter()
1391                    .min_by_key(|(_, load)| load.seq)
1392                    .map(|(k, _)| k.clone());
1393                if let Some(oldest) = oldest {
1394                    loads.remove(&oldest);
1395                }
1396            }
1397            loads.insert(label.to_string(), PageLoad { seq, nonce });
1398        }
1399        self.changed.notify_waiters();
1400    }
1401
1402    /// Window `label`'s latest ready signal, if any.
1403    #[must_use]
1404    pub fn latest(&self, label: &str) -> Option<PageLoad> {
1405        self.last_load
1406            .lock()
1407            .unwrap_or_else(std::sync::PoisonError::into_inner)
1408            .get(label)
1409            .cloned()
1410    }
1411
1412    /// The sequence number of the most recent ready signal from any window (0 if none yet).
1413    #[must_use]
1414    pub fn current_seq(&self) -> u64 {
1415        self.seq.load(std::sync::atomic::Ordering::Relaxed)
1416    }
1417
1418    /// Resolves the next time any window records a page load.
1419    pub fn changed(&self) -> tokio::sync::futures::Notified<'_> {
1420        self.changed.notified()
1421    }
1422}
1423// ── Recording drain watermarks ─────────────────────────────────────────────
1424
1425/// Per-window drain positions for pulling the JS bridge's event stream into a recording,
1426/// shared by the background drain loop and `recording flush`.
1427///
1428/// Each kept its own watermark before, so a flush re-recorded what the drain had captured
1429/// (and vice versa). A position is the bridge's per-page event SEQUENCE (plus the page
1430/// instance it belongs to), not a wall-clock timestamp: a timestamp watermark clamped to the
1431/// Rust clock re-read an event stamped ahead of it on every drain, and lost events pushed in
1432/// the same millisecond after a read. Positions are reset whenever a recording starts or is
1433/// imported — the new epoch is the recorder generation, and a window's first read in it skips
1434/// the page's pre-recording history by the `floor_ms` timestamp. A per-window async lock
1435/// serializes the two readers for the same window.
1436#[derive(Default)]
1437pub(crate) struct DrainWatermarks {
1438    inner: std::sync::Mutex<WatermarkState>,
1439}
1440
1441#[derive(Default)]
1442struct WatermarkState {
1443    floor_ms: f64,
1444    epoch: u64,
1445    per_label: HashMap<String, DrainMark>,
1446    locks: HashMap<String, std::sync::Arc<tokio::sync::Mutex<()>>>,
1447}
1448
1449/// How far a window's event stream has been drained: the bridge's page `instance` and the
1450/// last sequence number read from it.
1451#[derive(Debug, Clone, PartialEq, Eq)]
1452pub(crate) struct DrainMark {
1453    /// Identifies one page load of the window (the sequence restarts on reload).
1454    pub instance: String,
1455    /// The last sequence number read from that page.
1456    pub seq: u64,
1457}
1458
1459/// Where the next drain of a window starts, captured in one step.
1460#[derive(Debug, Clone)]
1461pub(crate) struct DrainCursor {
1462    /// The recording epoch (recorder generation) this position belongs to.
1463    pub epoch: u64,
1464    /// Entries completed at or before this time predate the recording (first read only).
1465    pub floor_ms: f64,
1466    /// The window's position, or `None` if it has not been read in this epoch.
1467    pub mark: Option<DrainMark>,
1468}
1469
1470impl DrainWatermarks {
1471    fn state(&self) -> std::sync::MutexGuard<'_, WatermarkState> {
1472        self.inner
1473            .lock()
1474            .unwrap_or_else(std::sync::PoisonError::into_inner)
1475    }
1476
1477    /// Where to read `label`'s stream from.
1478    #[must_use]
1479    pub fn cursor(&self, label: &str) -> DrainCursor {
1480        let s = self.state();
1481        DrainCursor {
1482            epoch: s.epoch,
1483            floor_ms: s.floor_ms,
1484            mark: s.per_label.get(label).cloned(),
1485        }
1486    }
1487
1488    /// Record that `label` was read up to `mark`, if `epoch` is still the current one (a read
1489    /// that straddled a reset must not plant an old position in the new epoch). Returns
1490    /// whether the position was stored.
1491    pub fn advance(&self, label: &str, epoch: u64, mark: DrainMark) -> bool {
1492        let mut s = self.state();
1493        if s.epoch != epoch {
1494            return false;
1495        }
1496        s.per_label.insert(label.to_string(), mark);
1497        true
1498    }
1499
1500    /// Start recording epoch `epoch`: every window is read afresh, skipping entries at or
1501    /// before `floor_ms`. Monotonic: epochs are recorder generations (strictly increasing), so a
1502    /// reset for an epoch at or below the current one is a stale caller — a `start` whose
1503    /// handler ran late — and is ignored (R4-RACE1).
1504    pub fn reset(&self, floor_ms: f64, epoch: u64) {
1505        let mut s = self.state();
1506        if epoch <= s.epoch {
1507            return;
1508        }
1509        s.floor_ms = floor_ms;
1510        s.epoch = epoch;
1511        s.per_label.clear();
1512    }
1513
1514    /// Forget windows that no longer exist (bounds the maps across ephemeral windows).
1515    pub fn retain(&self, labels: &[String]) {
1516        let mut s = self.state();
1517        s.per_label.retain(|l, _| labels.contains(l));
1518        s.locks.retain(|l, _| labels.contains(l));
1519    }
1520
1521    /// The lock that serializes readers of `label`'s stream.
1522    #[must_use]
1523    pub fn lock_for(&self, label: &str) -> std::sync::Arc<tokio::sync::Mutex<()>> {
1524        std::sync::Arc::clone(self.state().locks.entry(label.to_string()).or_default())
1525    }
1526}
1527#[cfg(test)]
1528mod tests {
1529    use super::*;
1530
1531    /// G-10: expired fault rules were never evicted, and nothing bounded the number of rules.
1532    #[test]
1533    fn fault_registry_evicts_expired_rules_and_is_bounded() {
1534        let reg = FaultRegistry::new();
1535        let mut expired = FaultConfig::new("old", FaultType::Drop, 0);
1536        expired.created_at = Instant::now()
1537            .checked_sub(FAULT_TTL + Duration::from_secs(1))
1538            .expect("clock far enough from boot");
1539        reg.inject(expired);
1540        reg.inject(FaultConfig::new("fresh", FaultType::Drop, 0));
1541        let listed: Vec<String> = reg.list().into_iter().map(|f| f.command).collect();
1542        assert_eq!(
1543            listed,
1544            vec!["fresh".to_string()],
1545            "expired rule still listed"
1546        );
1547        assert_eq!(
1548            reg.inner.read().unwrap().map.len(),
1549            1,
1550            "expired rule still stored"
1551        );
1552
1553        for i in 0..300 {
1554            reg.inject(FaultConfig::new(format!("cmd-{i}"), FaultType::Drop, 0));
1555        }
1556        let stored = reg.inner.read().unwrap().map.len();
1557        assert_eq!(stored, 256, "fault rules must be capped");
1558        // The newest rule is kept; the oldest ones were evicted first.
1559        assert!(reg.check_and_trigger("cmd-299").is_some());
1560        assert!(reg.check_and_trigger("fresh").is_none());
1561        // Re-injecting an existing command at the cap replaces it without evicting another.
1562        reg.inject(FaultConfig::new("cmd-299", FaultType::Drop, 5));
1563        assert_eq!(reg.inner.read().unwrap().map.len(), 256);
1564    }
1565
1566    /// G-10: the contract store had no cap. At the cap the OLDEST baseline is evicted (and
1567    /// named), rather than refusing the new one.
1568    #[test]
1569    fn contract_store_is_bounded_evicting_the_oldest() {
1570        let store = ContractStore::new();
1571        let baseline =
1572            |c: &str| ContractBaseline::new(c, serde_json::Value::Null, JsonShape::Null, "", "t");
1573        for i in 0..1030 {
1574            store.record(baseline(&format!("cmd-{i}")));
1575        }
1576        assert_eq!(store.all().len(), 1024);
1577        assert!(
1578            store.get("cmd-0").is_none(),
1579            "the oldest baseline goes first"
1580        );
1581        assert!(store.get("cmd-5").is_none());
1582        assert!(store.get("cmd-6").is_some());
1583        assert!(store.get("cmd-1029").is_some());
1584        // Re-recording an existing command refreshes it in place: nothing is evicted.
1585        store.record(baseline("cmd-6"));
1586        assert_eq!(store.all().len(), 1024);
1587        assert!(store.get("cmd-7").is_some());
1588        // The refreshed baseline is now the newest, so the next eviction takes cmd-7.
1589        store.record(baseline("cmd-new"));
1590        assert!(store.get("cmd-6").is_some());
1591        assert!(store.get("cmd-7").is_none());
1592    }
1593
1594    /// G-10: `PageLoads` kept an entry for every window label ever seen.
1595    #[test]
1596    fn page_loads_are_bounded_keeping_the_most_recent_windows() {
1597        let loads = PageLoads::default();
1598        for i in 0..300 {
1599            loads.record_load(&format!("win-{i}"), Some("n"));
1600        }
1601        assert_eq!(loads.last_load.lock().unwrap().len(), 256);
1602        assert!(loads.latest("win-299").is_some());
1603        assert!(
1604            loads.latest("win-0").is_none(),
1605            "the least recently loaded window goes first"
1606        );
1607        // A window that loads again becomes the most recent and survives the next eviction.
1608        loads.record_load("win-44", Some("n2"));
1609        loads.record_load("win-new", None);
1610        assert!(loads.latest("win-44").is_some());
1611        assert!(loads.latest("win-45").is_none());
1612    }
1613
1614    /// R4-RACE1: a delayed reset for an OLDER recording generation moved the epoch backwards,
1615    /// so the drain for the current recording (which captured the newer epoch) had every
1616    /// `advance` refused and the recording silently captured no webview events.
1617    #[test]
1618    fn drain_watermark_reset_is_monotonic() {
1619        let w = DrainWatermarks::default();
1620        w.reset(200.0, 2);
1621        let mark = DrainMark {
1622            instance: "page-a".to_string(),
1623            seq: 7,
1624        };
1625        assert!(w.advance("main", 2, mark.clone()));
1626        // The reset for generation 1 (an earlier `start` whose handler was delayed) lands late.
1627        w.reset(100.0, 1);
1628        let c = w.cursor("main");
1629        assert_eq!(
1630            c.epoch, 2,
1631            "a stale reset must not move the epoch backwards"
1632        );
1633        assert!((c.floor_ms - 200.0).abs() < f64::EPSILON);
1634        assert_eq!(c.mark, Some(mark));
1635        // The current recording's drain keeps advancing.
1636        assert!(w.advance(
1637            "main",
1638            2,
1639            DrainMark {
1640                instance: "page-a".to_string(),
1641                seq: 9,
1642            }
1643        ));
1644        // A newer generation still resets.
1645        w.reset(300.0, 3);
1646        let c = w.cursor("main");
1647        assert_eq!((c.epoch, c.mark), (3, None));
1648    }
1649
1650    // C15e: the map is keyed by the caller-chosen command name, so it must be bounded.
1651    #[test]
1652    fn command_timings_bound_distinct_commands() {
1653        let t = CommandTimings::new();
1654        for i in 0..(MAX_TIMED_COMMANDS + 100) {
1655            t.record(&format!("cmd-{i}"), Duration::from_millis(1));
1656        }
1657        assert_eq!(t.all_stats().len(), MAX_TIMED_COMMANDS);
1658        // Commands already tracked keep accumulating once the cap is reached.
1659        t.record("cmd-0", Duration::from_millis(1));
1660        assert_eq!(t.stats_for("cmd-0").unwrap().count, 2);
1661    }
1662
1663    #[test]
1664    fn p95_is_nearest_rank_not_max_for_small_samples() {
1665        let t = CommandTimings::new();
1666        for ms in 1..=20 {
1667            t.record("cmd", Duration::from_millis(ms));
1668        }
1669        let s = t.stats_for("cmd").unwrap();
1670        assert!(
1671            (s.p95_ms - 19.0).abs() < 1e-9,
1672            "p95 of 1..=20 is 19, got {}",
1673            s.p95_ms
1674        );
1675        assert!((s.max_ms - 20.0).abs() < 1e-9);
1676    }
1677
1678    #[test]
1679    fn app_state_probes_register_run_list() {
1680        let probes = AppStateProbes::default();
1681        assert!(probes.is_empty());
1682
1683        probes.register(
1684            "scoring",
1685            std::sync::Arc::new(|| serde_json::json!({ "pipeline_version": 5 })),
1686        );
1687        probes.register("queue", std::sync::Arc::new(|| serde_json::json!(0)));
1688
1689        // names() is sorted (BTreeMap).
1690        assert_eq!(
1691            probes.names(),
1692            vec!["queue".to_string(), "scoring".to_string()]
1693        );
1694        assert_eq!(probes.len(), 2);
1695
1696        let snapshot = probes.run("scoring").expect("probe runs");
1697        assert_eq!(snapshot["pipeline_version"], 5);
1698        assert!(probes.run("missing").is_none());
1699    }
1700
1701    #[test]
1702    fn app_state_probe_reflects_live_state() {
1703        // A probe closes over shared state and reflects mutations at call time —
1704        // proving it reads live backend state, not a registration-time snapshot.
1705        let counter = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
1706        let probe_counter = std::sync::Arc::clone(&counter);
1707        let probes = AppStateProbes::default();
1708        probes.register(
1709            "counter",
1710            std::sync::Arc::new(move || {
1711                serde_json::json!(probe_counter.load(std::sync::atomic::Ordering::SeqCst))
1712            }),
1713        );
1714
1715        assert_eq!(probes.run("counter").unwrap(), serde_json::json!(0));
1716        counter.store(42, std::sync::atomic::Ordering::SeqCst);
1717        assert_eq!(probes.run("counter").unwrap(), serde_json::json!(42));
1718    }
1719
1720    #[test]
1721    fn event_bus_push_and_read() {
1722        let bus = EventBusMonitor::new(3);
1723        assert!(bus.is_empty());
1724        bus.push(CapturedTauriEvent {
1725            name: "test".to_string(),
1726            payload: "{}".to_string(),
1727            timestamp: "2026-01-01T00:00:00Z".to_string(),
1728        });
1729        assert_eq!(bus.len(), 1);
1730        assert_eq!(bus.events()[0].name, "test");
1731    }
1732
1733    #[test]
1734    fn event_bus_ring_buffer_eviction() {
1735        let bus = EventBusMonitor::new(2);
1736        for i in 0..5 {
1737            bus.push(CapturedTauriEvent {
1738                name: format!("event_{i}"),
1739                payload: String::new(),
1740                timestamp: String::new(),
1741            });
1742        }
1743        assert_eq!(bus.len(), 2);
1744        assert_eq!(bus.events()[0].name, "event_3");
1745        assert_eq!(bus.events()[1].name, "event_4");
1746    }
1747
1748    #[test]
1749    fn event_bus_clear() {
1750        let bus = EventBusMonitor::new(10);
1751        bus.push(CapturedTauriEvent {
1752            name: "a".to_string(),
1753            payload: String::new(),
1754            timestamp: String::new(),
1755        });
1756        assert_eq!(bus.clear(), 1);
1757        assert!(bus.is_empty());
1758    }
1759
1760    /// G-9: the finished flag was only set when a task body returned normally, so a panicked
1761    /// task was reported active forever. The guard sets it on unwind (and on a dropped future).
1762    #[test]
1763    fn a_panicking_tracked_task_is_reported_finished() {
1764        let tracker = std::sync::Arc::new(TaskTracker::new());
1765        let guard = tracker.track_guarded("event_drain_loop");
1766        let r = std::thread::spawn(move || {
1767            let _finished = guard;
1768            panic!("task blew up");
1769        })
1770        .join();
1771        assert!(r.is_err());
1772        assert_eq!(
1773            tracker.active_count(),
1774            0,
1775            "a panicked task is still reported active"
1776        );
1777        assert!(tracker.list()[0].is_finished);
1778
1779        // A task that is still running stays active until its guard goes.
1780        let running = tracker.track_guarded("mcp_server");
1781        assert_eq!(tracker.active_count(), 1);
1782        drop(running);
1783        assert_eq!(tracker.active_count(), 0);
1784    }
1785
1786    #[test]
1787    fn task_tracker_lifecycle() {
1788        let tracker = TaskTracker::new();
1789        let flag = tracker.track("mcp_server");
1790        let tasks = tracker.list();
1791        assert_eq!(tasks.len(), 1);
1792        assert_eq!(tasks[0].name, "mcp_server");
1793        assert!(!tasks[0].is_finished);
1794        assert_eq!(tracker.active_count(), 1);
1795
1796        flag.store(true, std::sync::atomic::Ordering::Relaxed);
1797        let tasks = tracker.list();
1798        assert!(tasks[0].is_finished);
1799        assert_eq!(tracker.active_count(), 0);
1800    }
1801
1802    #[test]
1803    fn timing_samples_basic() {
1804        let mut samples = TimingSamples::default();
1805        samples.record(Duration::from_millis(10));
1806        samples.record(Duration::from_millis(20));
1807        samples.record(Duration::from_millis(30));
1808        let stats = samples.stats("test_cmd");
1809        assert_eq!(stats.count, 3);
1810        assert!((stats.min_ms - 10.0).abs() < 1.0);
1811        assert!((stats.max_ms - 30.0).abs() < 1.0);
1812        assert!((stats.avg_ms - 20.0).abs() < 1.0);
1813    }
1814
1815    #[test]
1816    fn timing_samples_empty() {
1817        let samples = TimingSamples::default();
1818        let stats = samples.stats("empty");
1819        assert_eq!(stats.count, 0);
1820        assert_eq!(stats.min_ms, 0.0);
1821    }
1822
1823    #[test]
1824    fn timing_samples_bounded_but_count_accurate() {
1825        // Record far more than the ring capacity. Memory must stay bounded
1826        // (recent window <= MAX_TIMING_SAMPLES) while count/min/max/total remain
1827        // accurate over the FULL history — not just the retained window. This test
1828        // fails if the cap is removed (unbounded growth) or if the running
1829        // aggregates are dropped in favor of windowed-only stats.
1830        let mut samples = TimingSamples::default();
1831        let n = MAX_TIMING_SAMPLES * 3;
1832        for i in 0..n {
1833            // Durations 1..=n ms; the smallest (1ms) and largest (n ms) fall
1834            // outside the retained recent window, proving min/max are all-time.
1835            samples.record(Duration::from_millis((i + 1) as u64));
1836        }
1837        assert!(
1838            samples.recent.len() <= MAX_TIMING_SAMPLES,
1839            "recent window must stay bounded, got {}",
1840            samples.recent.len()
1841        );
1842        let stats = samples.stats("soak");
1843        assert_eq!(stats.count, n as u64, "count must reflect full history");
1844        assert!(
1845            (stats.min_ms - 1.0).abs() < 0.5,
1846            "all-time min lost: {}",
1847            stats.min_ms
1848        );
1849        assert!(
1850            (stats.max_ms - n as f64).abs() < 0.5,
1851            "all-time max lost: {}",
1852            stats.max_ms
1853        );
1854    }
1855
1856    #[test]
1857    fn command_timings_thread_safe() {
1858        let timings = CommandTimings::new();
1859        timings.record("cmd_a", Duration::from_millis(5));
1860        timings.record("cmd_a", Duration::from_millis(15));
1861        timings.record("cmd_b", Duration::from_millis(100));
1862
1863        let all = timings.all_stats();
1864        assert_eq!(all.len(), 2);
1865        assert_eq!(all[0].command, "cmd_b");
1866
1867        let a = timings.stats_for("cmd_a").unwrap();
1868        assert_eq!(a.count, 2);
1869    }
1870
1871    #[test]
1872    fn fault_registry_lifecycle() {
1873        let registry = FaultRegistry::new();
1874        registry.inject(FaultConfig {
1875            command: "slow_cmd".to_string(),
1876            fault_type: FaultType::Delay { delay_ms: 500 },
1877            trigger_count: 0,
1878            max_triggers: 2,
1879            created_at: Instant::now(),
1880        });
1881
1882        assert!(registry.check_and_trigger("slow_cmd").is_some());
1883        assert!(registry.check_and_trigger("slow_cmd").is_some());
1884        assert!(registry.check_and_trigger("slow_cmd").is_none());
1885
1886        assert_eq!(registry.list().len(), 1);
1887        assert!(registry.clear("slow_cmd"));
1888        assert_eq!(registry.list().len(), 0);
1889    }
1890
1891    #[test]
1892    fn fault_registry_unlimited() {
1893        let registry = FaultRegistry::new();
1894        registry.inject(FaultConfig {
1895            command: "always_fail".to_string(),
1896            fault_type: FaultType::Error {
1897                message: "injected".to_string(),
1898            },
1899            trigger_count: 0,
1900            max_triggers: 0,
1901            created_at: Instant::now(),
1902        });
1903
1904        for _ in 0..100 {
1905            assert!(registry.check_and_trigger("always_fail").is_some());
1906        }
1907    }
1908
1909    #[test]
1910    fn fault_expires_after_ttl() {
1911        let cfg = FaultConfig {
1912            command: "x".to_string(),
1913            fault_type: FaultType::Error {
1914                message: "e".to_string(),
1915            },
1916            trigger_count: 0,
1917            max_triggers: 0, // unlimited by count...
1918            created_at: Instant::now(),
1919        };
1920        // ...but still inert once older than the TTL (audit #34).
1921        assert!(cfg.should_trigger_at(cfg.created_at));
1922        assert!(!cfg.should_trigger_at(cfg.created_at + FAULT_TTL + Duration::from_secs(1)));
1923    }
1924
1925    #[test]
1926    fn json_shape_extraction() {
1927        let value = serde_json::json!({
1928            "name": "test",
1929            "count": 42,
1930            "active": true,
1931            "items": [{"id": 1}],
1932            "meta": null
1933        });
1934        let shape = JsonShape::from_value(&value);
1935        match &shape {
1936            JsonShape::Object(fields) => {
1937                assert_eq!(fields.len(), 5);
1938                assert_eq!(*fields.get("name").unwrap(), JsonShape::String);
1939                assert_eq!(*fields.get("count").unwrap(), JsonShape::Number);
1940                assert_eq!(*fields.get("active").unwrap(), JsonShape::Bool);
1941                assert_eq!(*fields.get("meta").unwrap(), JsonShape::Null);
1942            }
1943            _ => panic!("expected object"),
1944        }
1945    }
1946
1947    #[test]
1948    fn contract_diff_detects_changes() {
1949        let baseline = serde_json::json!({"name": "old", "count": 1});
1950        let current = serde_json::json!({"name": "new", "count": "not_a_number", "extra": true});
1951
1952        let b_shape = JsonShape::from_value(&baseline);
1953        let c_shape = JsonShape::from_value(&current);
1954        let drift = diff_shapes(&b_shape, &c_shape, "test_cmd");
1955
1956        assert!(!drift.shape_matches);
1957        assert_eq!(drift.new_fields, vec!["test_cmd.extra"]);
1958        assert_eq!(drift.type_changes.len(), 1);
1959        assert_eq!(drift.type_changes[0].path, "test_cmd.count");
1960    }
1961
1962    #[test]
1963    fn contract_store_crud() {
1964        let store = ContractStore::new();
1965        let baseline = ContractBaseline {
1966            command: "get_user".to_string(),
1967            args: serde_json::json!({}),
1968            shape: JsonShape::Object(HashMap::new()),
1969            sample: "{}".to_string(),
1970            recorded_at: "2026-05-26".to_string(),
1971        };
1972        store.record(baseline);
1973        assert!(store.get("get_user").is_some());
1974        assert_eq!(store.all().len(), 1);
1975        assert_eq!(store.clear(), 1);
1976        assert!(store.get("get_user").is_none());
1977    }
1978
1979    #[test]
1980    fn startup_timeline_records_phases() {
1981        let timeline = StartupTimeline::new();
1982        std::thread::sleep(Duration::from_millis(5));
1983        timeline.mark("phase_1");
1984        std::thread::sleep(Duration::from_millis(5));
1985        timeline.mark("phase_2");
1986
1987        let report = timeline.report();
1988        assert_eq!(report.len(), 2);
1989        assert_eq!(report[0].name, "phase_1");
1990        assert!(report[1].cumulative_ms >= report[0].cumulative_ms);
1991        assert!(timeline.total_ms() > 0.0);
1992    }
1993
1994    #[test]
1995    fn enumerate_child_processes_returns_vec() {
1996        let children = enumerate_child_processes();
1997        // The test process itself may or may not have children, but the
1998        // function must not panic and must return a well-formed Vec.
1999        for child in &children {
2000            assert_ne!(child.pid, 0, "child PID should be non-zero");
2001            assert_eq!(
2002                child.ppid,
2003                std::process::id(),
2004                "parent PID should match current process"
2005            );
2006            assert!(!child.name.is_empty(), "child name should not be empty");
2007        }
2008    }
2009
2010    #[test]
2011    fn enumerate_child_processes_with_spawned_child() {
2012        // Spawn a short-lived child process and verify we can enumerate it.
2013        let child = std::process::Command::new(if cfg!(windows) { "cmd.exe" } else { "sleep" })
2014            .args(if cfg!(windows) {
2015                &["/c", "timeout /t 10 /nobreak >nul"][..]
2016            } else {
2017                &["10"][..]
2018            })
2019            .spawn();
2020
2021        if let Ok(mut child_proc) = child {
2022            let children = enumerate_child_processes();
2023            assert!(
2024                !children.is_empty(),
2025                "should find at least one child process"
2026            );
2027
2028            let found = children.iter().any(|c| c.pid == child_proc.id());
2029            assert!(
2030                found,
2031                "spawned child (PID {}) should appear in enumeration",
2032                child_proc.id()
2033            );
2034
2035            let _ = child_proc.kill();
2036            let _ = child_proc.wait();
2037        }
2038    }
2039
2040    #[test]
2041    fn child_process_info_serializes() {
2042        let info = ChildProcessInfo {
2043            pid: 1234,
2044            ppid: 5678,
2045            name: "test-sidecar".to_string(),
2046            memory_bytes: Some(1_048_576),
2047        };
2048        let json = serde_json::to_value(&info).unwrap();
2049        assert_eq!(json["pid"], 1234);
2050        assert_eq!(json["ppid"], 5678);
2051        assert_eq!(json["name"], "test-sidecar");
2052        assert_eq!(json["memory_bytes"], 1_048_576);
2053    }
2054
2055    #[test]
2056    fn child_process_info_serializes_no_memory() {
2057        let info = ChildProcessInfo {
2058            pid: 42,
2059            ppid: 1,
2060            name: "zombie".to_string(),
2061            memory_bytes: None,
2062        };
2063        let json = serde_json::to_value(&info).unwrap();
2064        assert!(json["memory_bytes"].is_null());
2065    }
2066}