use std::collections::{HashMap, VecDeque};
use std::sync::RwLock;
use std::sync::atomic::AtomicBool;
use std::time::{Duration, Instant};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct CommandTimingStats {
pub command: String,
pub count: u64,
pub min_ms: f64,
pub max_ms: f64,
pub avg_ms: f64,
pub p95_ms: f64,
pub total_ms: f64,
}
#[derive(Debug, Default)]
pub struct TimingSamples {
pub samples: Vec<Duration>,
}
impl TimingSamples {
pub fn record(&mut self, duration: Duration) {
self.samples.push(duration);
}
#[must_use]
pub fn stats(&self, command: &str) -> CommandTimingStats {
if self.samples.is_empty() {
return CommandTimingStats {
command: command.to_string(),
count: 0,
min_ms: 0.0,
max_ms: 0.0,
avg_ms: 0.0,
p95_ms: 0.0,
total_ms: 0.0,
};
}
let mut sorted: Vec<f64> = self
.samples
.iter()
.map(|d| d.as_secs_f64() * 1000.0)
.collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let count = sorted.len() as u64;
let total: f64 = sorted.iter().sum();
let min = sorted[0];
let max = sorted[sorted.len() - 1];
let avg = total / sorted.len() as f64;
let p95_idx = ((sorted.len() as f64) * 0.95).ceil() as usize;
let p95 = sorted[p95_idx.min(sorted.len() - 1)];
CommandTimingStats {
command: command.to_string(),
count,
min_ms: (min * 100.0).round() / 100.0,
max_ms: (max * 100.0).round() / 100.0,
avg_ms: (avg * 100.0).round() / 100.0,
p95_ms: (p95 * 100.0).round() / 100.0,
total_ms: (total * 100.0).round() / 100.0,
}
}
}
pub struct CommandTimings {
inner: RwLock<HashMap<String, TimingSamples>>,
}
impl CommandTimings {
#[must_use]
pub fn new() -> Self {
Self {
inner: RwLock::new(HashMap::new()),
}
}
pub fn record(&self, command: &str, duration: Duration) {
let mut map = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.entry(command.to_string()).or_default().record(duration);
}
#[must_use]
pub fn all_stats(&self) -> Vec<CommandTimingStats> {
let map = self
.inner
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut stats: Vec<CommandTimingStats> =
map.iter().map(|(name, s)| s.stats(name)).collect();
stats.sort_by(|a, b| {
b.total_ms
.partial_cmp(&a.total_ms)
.unwrap_or(std::cmp::Ordering::Equal)
});
stats
}
#[must_use]
pub fn stats_for(&self, command: &str) -> Option<CommandTimingStats> {
let map = self
.inner
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.get(command).map(|s| s.stats(command))
}
pub fn clear(&self) {
let mut map = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.clear();
}
}
impl Default for CommandTimings {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize)]
pub enum FaultType {
Delay {
delay_ms: u64,
},
Error {
message: String,
},
Drop,
Corrupt,
}
#[derive(Debug, Clone, Serialize)]
pub struct FaultConfig {
pub command: String,
pub fault_type: FaultType,
pub trigger_count: u64,
pub max_triggers: u64,
#[serde(skip)]
pub created_at: Instant,
}
impl FaultConfig {
#[must_use]
pub fn should_trigger(&self) -> bool {
self.max_triggers == 0 || self.trigger_count < self.max_triggers
}
}
pub struct FaultRegistry {
inner: RwLock<HashMap<String, FaultConfig>>,
}
impl FaultRegistry {
#[must_use]
pub fn new() -> Self {
Self {
inner: RwLock::new(HashMap::new()),
}
}
pub fn inject(&self, config: FaultConfig) {
let mut map = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.insert(config.command.clone(), config);
}
pub fn check_and_trigger(&self, command: &str) -> Option<FaultType> {
let mut map = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(config) = map.get_mut(command)
&& config.should_trigger()
{
config.trigger_count += 1;
return Some(config.fault_type.clone());
}
None
}
#[must_use]
pub fn list(&self) -> Vec<FaultConfig> {
let map = self
.inner
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.values().cloned().collect()
}
pub fn clear(&self, command: &str) -> bool {
let mut map = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.remove(command).is_some()
}
pub fn clear_all(&self) -> usize {
let mut map = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let count = map.len();
map.clear();
count
}
}
impl Default for FaultRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum JsonShape {
Null,
Bool,
Number,
String,
Array(Box<Self>),
Object(HashMap<String, Self>),
}
impl JsonShape {
#[must_use]
pub fn from_value(value: &serde_json::Value) -> Self {
match value {
serde_json::Value::Null => Self::Null,
serde_json::Value::Bool(_) => Self::Bool,
serde_json::Value::Number(_) => Self::Number,
serde_json::Value::String(_) => Self::String,
serde_json::Value::Array(arr) => {
let elem = arr.first().map_or(Self::Null, Self::from_value);
Self::Array(Box::new(elem))
}
serde_json::Value::Object(obj) => {
let fields: HashMap<String, Self> = obj
.iter()
.map(|(k, v)| (k.clone(), Self::from_value(v)))
.collect();
Self::Object(fields)
}
}
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::Null => "null",
Self::Bool => "bool",
Self::Number => "number",
Self::String => "string",
Self::Array(_) => "array",
Self::Object(_) => "object",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ContractBaseline {
pub command: String,
pub args: serde_json::Value,
pub shape: JsonShape,
pub sample: String,
pub recorded_at: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ContractDrift {
pub command: String,
pub new_fields: Vec<String>,
pub removed_fields: Vec<String>,
pub type_changes: Vec<TypeChange>,
pub shape_matches: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct TypeChange {
pub path: String,
pub baseline_type: String,
pub current_type: String,
}
#[must_use]
pub fn diff_shapes(baseline: &JsonShape, current: &JsonShape, prefix: &str) -> ContractDrift {
let mut new_fields = Vec::new();
let mut removed_fields = Vec::new();
let mut type_changes = Vec::new();
diff_shapes_inner(
baseline,
current,
prefix,
&mut new_fields,
&mut removed_fields,
&mut type_changes,
);
let shape_matches =
new_fields.is_empty() && removed_fields.is_empty() && type_changes.is_empty();
ContractDrift {
command: prefix.to_string(),
new_fields,
removed_fields,
type_changes,
shape_matches,
}
}
fn diff_shapes_inner(
baseline: &JsonShape,
current: &JsonShape,
prefix: &str,
new_fields: &mut Vec<String>,
removed_fields: &mut Vec<String>,
type_changes: &mut Vec<TypeChange>,
) {
match (baseline, current) {
(JsonShape::Object(b_fields), JsonShape::Object(c_fields)) => {
for (key, b_shape) in b_fields {
let path = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
if let Some(c_shape) = c_fields.get(key) {
diff_shapes_inner(
b_shape,
c_shape,
&path,
new_fields,
removed_fields,
type_changes,
);
} else {
removed_fields.push(path);
}
}
for key in c_fields.keys() {
if !b_fields.contains_key(key) {
let path = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
new_fields.push(path);
}
}
}
(JsonShape::Array(b_elem), JsonShape::Array(c_elem)) => {
let path = format!("{prefix}[]");
diff_shapes_inner(
b_elem,
c_elem,
&path,
new_fields,
removed_fields,
type_changes,
);
}
(b, c) if b.type_name() != c.type_name() => {
type_changes.push(TypeChange {
path: prefix.to_string(),
baseline_type: b.type_name().to_string(),
current_type: c.type_name().to_string(),
});
}
_ => {}
}
}
pub struct ContractStore {
inner: RwLock<HashMap<String, ContractBaseline>>,
}
impl ContractStore {
#[must_use]
pub fn new() -> Self {
Self {
inner: RwLock::new(HashMap::new()),
}
}
pub fn record(&self, baseline: ContractBaseline) {
let mut map = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.insert(baseline.command.clone(), baseline);
}
#[must_use]
pub fn get(&self, command: &str) -> Option<ContractBaseline> {
let map = self
.inner
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.get(command).cloned()
}
#[must_use]
pub fn all(&self) -> Vec<ContractBaseline> {
let map = self
.inner
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.values().cloned().collect()
}
pub fn clear(&self) -> usize {
let mut map = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let count = map.len();
map.clear();
count
}
}
impl Default for ContractStore {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct StartupPhase {
pub name: String,
pub duration_ms: f64,
pub cumulative_ms: f64,
}
pub struct StartupTimeline {
start: Instant,
phases: RwLock<Vec<(String, Instant)>>,
}
impl StartupTimeline {
#[must_use]
pub fn new() -> Self {
Self {
start: Instant::now(),
phases: RwLock::new(Vec::new()),
}
}
pub fn mark(&self, name: &str) {
let mut phases = self
.phases
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
phases.push((name.to_string(), Instant::now()));
}
#[must_use]
pub fn report(&self) -> Vec<StartupPhase> {
let phases = self
.phases
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut result = Vec::new();
let mut prev = self.start;
for (name, instant) in phases.iter() {
let duration = instant.duration_since(prev);
let cumulative = instant.duration_since(self.start);
result.push(StartupPhase {
name: name.clone(),
duration_ms: (duration.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
cumulative_ms: (cumulative.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
});
prev = *instant;
}
result
}
#[must_use]
pub fn total_ms(&self) -> f64 {
let phases = self
.phases
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some((_, last)) = phases.last() {
(last.duration_since(self.start).as_secs_f64() * 1000.0 * 100.0).round() / 100.0
} else {
0.0
}
}
}
impl Default for StartupTimeline {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CapturedTauriEvent {
pub name: String,
pub payload: String,
pub timestamp: String,
}
const DEFAULT_EVENT_BUS_CAPACITY: usize = 1000;
#[derive(Clone)]
pub struct EventBusMonitor {
inner: std::sync::Arc<RwLock<VecDeque<CapturedTauriEvent>>>,
capacity: usize,
}
impl EventBusMonitor {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
inner: std::sync::Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
capacity,
}
}
pub fn push(&self, event: CapturedTauriEvent) {
let mut buf = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if buf.len() >= self.capacity {
buf.pop_front();
}
buf.push_back(event);
}
#[must_use]
pub fn events(&self) -> Vec<CapturedTauriEvent> {
self.inner
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.cloned()
.collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.inner
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) -> usize {
let mut buf = self
.inner
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let count = buf.len();
buf.clear();
count
}
}
impl Default for EventBusMonitor {
fn default() -> Self {
Self::new(DEFAULT_EVENT_BUS_CAPACITY)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TrackedTaskInfo {
pub name: String,
pub spawned_at: String,
pub is_finished: bool,
pub uptime_secs: u64,
}
struct TrackedTaskEntry {
name: String,
spawned_at: Instant,
spawned_at_wall: String,
finished: std::sync::Arc<AtomicBool>,
}
pub struct TaskTracker {
tasks: RwLock<Vec<TrackedTaskEntry>>,
}
impl TaskTracker {
#[must_use]
pub fn new() -> Self {
Self {
tasks: RwLock::new(Vec::new()),
}
}
pub fn track(&self, name: &str) -> std::sync::Arc<AtomicBool> {
let finished = std::sync::Arc::new(AtomicBool::new(false));
let entry = TrackedTaskEntry {
name: name.to_string(),
spawned_at: Instant::now(),
spawned_at_wall: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
finished: finished.clone(),
};
self.tasks
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(entry);
finished
}
#[must_use]
pub fn list(&self) -> Vec<TrackedTaskInfo> {
let tasks = self
.tasks
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
tasks
.iter()
.map(|t| TrackedTaskInfo {
name: t.name.clone(),
spawned_at: t.spawned_at_wall.clone(),
is_finished: t.finished.load(std::sync::atomic::Ordering::Relaxed),
uptime_secs: t.spawned_at.elapsed().as_secs(),
})
.collect()
}
#[must_use]
pub fn active_count(&self) -> usize {
let tasks = self
.tasks
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
tasks
.iter()
.filter(|t| !t.finished.load(std::sync::atomic::Ordering::Relaxed))
.count()
}
}
impl Default for TaskTracker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ChildProcessInfo {
pub pid: u32,
pub ppid: u32,
pub name: String,
pub memory_bytes: Option<u64>,
}
#[must_use]
pub fn enumerate_child_processes() -> Vec<ChildProcessInfo> {
let my_pid = std::process::id();
#[cfg(windows)]
{
enumerate_children_windows(my_pid)
}
#[cfg(target_os = "linux")]
{
enumerate_children_linux(my_pid)
}
#[cfg(target_os = "macos")]
{
enumerate_children_macos(my_pid)
}
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
{
let _ = my_pid;
Vec::new()
}
}
#[cfg(windows)]
#[allow(unsafe_code)]
fn enumerate_children_windows(parent_pid: u32) -> Vec<ChildProcessInfo> {
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS,
};
let mut children = Vec::new();
unsafe {
let Ok(snapshot) = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) else {
return children;
};
let mut entry: PROCESSENTRY32 = std::mem::zeroed();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
if Process32First(snapshot, &mut entry).is_ok() {
loop {
if entry.th32ParentProcessID == parent_pid && entry.th32ProcessID != parent_pid {
let name_bytes: Vec<u8> = entry
.szExeFile
.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as u8)
.collect();
let name = String::from_utf8_lossy(&name_bytes).to_string();
let memory_bytes = get_process_memory_windows(entry.th32ProcessID);
children.push(ChildProcessInfo {
pid: entry.th32ProcessID,
ppid: entry.th32ParentProcessID,
name,
memory_bytes,
});
}
if Process32Next(snapshot, &mut entry).is_err() {
break;
}
}
}
let _ = CloseHandle(snapshot);
}
children
}
#[cfg(windows)]
#[allow(unsafe_code)]
fn get_process_memory_windows(pid: u32) -> Option<u64> {
use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ,
};
unsafe {
let process = OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
false,
pid,
)
.ok()?;
let mut counters: PROCESS_MEMORY_COUNTERS = std::mem::zeroed();
counters.cb = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
if GetProcessMemoryInfo(process, &mut counters, counters.cb).is_ok() {
Some(counters.WorkingSetSize as u64)
} else {
None
}
}
}
#[cfg(target_os = "linux")]
fn enumerate_children_linux(parent_pid: u32) -> Vec<ChildProcessInfo> {
let mut children = Vec::new();
let Ok(entries) = std::fs::read_dir("/proc") else {
return children;
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let Some(pid_str) = file_name.to_str() else {
continue;
};
let Ok(pid) = pid_str.parse::<u32>() else {
continue;
};
let status_path = format!("/proc/{pid}/status");
let Ok(status) = std::fs::read_to_string(&status_path) else {
continue;
};
let mut ppid: Option<u32> = None;
let mut name = String::new();
let mut vm_rss_kb: u64 = 0;
for line in status.lines() {
if let Some(v) = line.strip_prefix("PPid:\t") {
ppid = v.trim().parse().ok();
} else if let Some(v) = line.strip_prefix("Name:\t") {
name = v.trim().to_string();
} else if let Some(v) = line.strip_prefix("VmRSS:") {
vm_rss_kb = v
.split_whitespace()
.next()
.and_then(|n| n.parse().ok())
.unwrap_or(0);
}
}
if ppid == Some(parent_pid) {
children.push(ChildProcessInfo {
pid,
ppid: parent_pid,
name,
memory_bytes: if vm_rss_kb > 0 {
Some(vm_rss_kb * 1024)
} else {
None
},
});
}
}
children
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn enumerate_children_macos(parent_pid: u32) -> Vec<ChildProcessInfo> {
use std::mem;
unsafe extern "C" {
fn proc_listchildpids(ppid: i32, buffer: *mut i32, buffersize: i32) -> i32;
fn proc_pidinfo(pid: i32, flavor: i32, arg: u64, buffer: *mut u8, buffersize: i32) -> i32;
fn proc_name(pid: i32, buffer: *mut u8, buffersize: u32) -> i32;
}
const PROC_PIDTASKINFO: i32 = 4;
#[repr(C)]
struct ProcTaskInfo {
pti_virtual_size: u64,
pti_resident_size: u64,
pti_total_user: u64,
pti_total_system: u64,
pti_threads_user: u64,
pti_threads_system: u64,
pti_policy: i32,
pti_faults: i32,
pti_pageins: i32,
pti_cow_faults: i32,
pti_messages_sent: i32,
pti_messages_received: i32,
pti_syscalls_mach: i32,
pti_syscalls_unix: i32,
pti_csw: i32,
pti_threadnum: i32,
pti_numrunning: i32,
pti_priority: i32,
}
let mut children = Vec::new();
unsafe {
let ppid = parent_pid as i32;
let mut cap = 256usize;
let (pids, n) = loop {
let mut pids = vec![0i32; cap];
let buf_size = (cap * mem::size_of::<i32>()) as i32;
let actual = proc_listchildpids(ppid, pids.as_mut_ptr(), buf_size);
if actual <= 0 {
return children;
}
let count = actual as usize;
if count < cap || cap >= 65536 {
break (pids, count.min(cap));
}
cap = (count + 16).max(cap * 2);
};
for &pid in &pids[..n] {
if pid <= 0 {
continue;
}
let mut name_buf = [0u8; 256];
let name_len = proc_name(pid, name_buf.as_mut_ptr(), 256);
let name = if name_len > 0 {
String::from_utf8_lossy(&name_buf[..name_len as usize]).to_string()
} else {
String::from("<unknown>")
};
let mut task_info: ProcTaskInfo = mem::zeroed();
let info_size = mem::size_of::<ProcTaskInfo>() as i32;
let ret = proc_pidinfo(
pid,
PROC_PIDTASKINFO,
0,
&mut task_info as *mut _ as *mut u8,
info_size,
);
let memory_bytes = if ret == info_size {
Some(task_info.pti_resident_size)
} else {
None
};
children.push(ChildProcessInfo {
pid: pid as u32,
ppid: parent_pid,
name,
memory_bytes,
});
}
}
children
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_bus_push_and_read() {
let bus = EventBusMonitor::new(3);
assert!(bus.is_empty());
bus.push(CapturedTauriEvent {
name: "test".to_string(),
payload: "{}".to_string(),
timestamp: "2026-01-01T00:00:00Z".to_string(),
});
assert_eq!(bus.len(), 1);
assert_eq!(bus.events()[0].name, "test");
}
#[test]
fn event_bus_ring_buffer_eviction() {
let bus = EventBusMonitor::new(2);
for i in 0..5 {
bus.push(CapturedTauriEvent {
name: format!("event_{i}"),
payload: String::new(),
timestamp: String::new(),
});
}
assert_eq!(bus.len(), 2);
assert_eq!(bus.events()[0].name, "event_3");
assert_eq!(bus.events()[1].name, "event_4");
}
#[test]
fn event_bus_clear() {
let bus = EventBusMonitor::new(10);
bus.push(CapturedTauriEvent {
name: "a".to_string(),
payload: String::new(),
timestamp: String::new(),
});
assert_eq!(bus.clear(), 1);
assert!(bus.is_empty());
}
#[test]
fn task_tracker_lifecycle() {
let tracker = TaskTracker::new();
let flag = tracker.track("mcp_server");
let tasks = tracker.list();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].name, "mcp_server");
assert!(!tasks[0].is_finished);
assert_eq!(tracker.active_count(), 1);
flag.store(true, std::sync::atomic::Ordering::Relaxed);
let tasks = tracker.list();
assert!(tasks[0].is_finished);
assert_eq!(tracker.active_count(), 0);
}
#[test]
fn timing_samples_basic() {
let mut samples = TimingSamples::default();
samples.record(Duration::from_millis(10));
samples.record(Duration::from_millis(20));
samples.record(Duration::from_millis(30));
let stats = samples.stats("test_cmd");
assert_eq!(stats.count, 3);
assert!((stats.min_ms - 10.0).abs() < 1.0);
assert!((stats.max_ms - 30.0).abs() < 1.0);
assert!((stats.avg_ms - 20.0).abs() < 1.0);
}
#[test]
fn timing_samples_empty() {
let samples = TimingSamples::default();
let stats = samples.stats("empty");
assert_eq!(stats.count, 0);
assert_eq!(stats.min_ms, 0.0);
}
#[test]
fn command_timings_thread_safe() {
let timings = CommandTimings::new();
timings.record("cmd_a", Duration::from_millis(5));
timings.record("cmd_a", Duration::from_millis(15));
timings.record("cmd_b", Duration::from_millis(100));
let all = timings.all_stats();
assert_eq!(all.len(), 2);
assert_eq!(all[0].command, "cmd_b");
let a = timings.stats_for("cmd_a").unwrap();
assert_eq!(a.count, 2);
}
#[test]
fn fault_registry_lifecycle() {
let registry = FaultRegistry::new();
registry.inject(FaultConfig {
command: "slow_cmd".to_string(),
fault_type: FaultType::Delay { delay_ms: 500 },
trigger_count: 0,
max_triggers: 2,
created_at: Instant::now(),
});
assert!(registry.check_and_trigger("slow_cmd").is_some());
assert!(registry.check_and_trigger("slow_cmd").is_some());
assert!(registry.check_and_trigger("slow_cmd").is_none());
assert_eq!(registry.list().len(), 1);
assert!(registry.clear("slow_cmd"));
assert_eq!(registry.list().len(), 0);
}
#[test]
fn fault_registry_unlimited() {
let registry = FaultRegistry::new();
registry.inject(FaultConfig {
command: "always_fail".to_string(),
fault_type: FaultType::Error {
message: "injected".to_string(),
},
trigger_count: 0,
max_triggers: 0,
created_at: Instant::now(),
});
for _ in 0..100 {
assert!(registry.check_and_trigger("always_fail").is_some());
}
}
#[test]
fn json_shape_extraction() {
let value = serde_json::json!({
"name": "test",
"count": 42,
"active": true,
"items": [{"id": 1}],
"meta": null
});
let shape = JsonShape::from_value(&value);
match &shape {
JsonShape::Object(fields) => {
assert_eq!(fields.len(), 5);
assert_eq!(*fields.get("name").unwrap(), JsonShape::String);
assert_eq!(*fields.get("count").unwrap(), JsonShape::Number);
assert_eq!(*fields.get("active").unwrap(), JsonShape::Bool);
assert_eq!(*fields.get("meta").unwrap(), JsonShape::Null);
}
_ => panic!("expected object"),
}
}
#[test]
fn contract_diff_detects_changes() {
let baseline = serde_json::json!({"name": "old", "count": 1});
let current = serde_json::json!({"name": "new", "count": "not_a_number", "extra": true});
let b_shape = JsonShape::from_value(&baseline);
let c_shape = JsonShape::from_value(¤t);
let drift = diff_shapes(&b_shape, &c_shape, "test_cmd");
assert!(!drift.shape_matches);
assert_eq!(drift.new_fields, vec!["test_cmd.extra"]);
assert_eq!(drift.type_changes.len(), 1);
assert_eq!(drift.type_changes[0].path, "test_cmd.count");
}
#[test]
fn contract_store_crud() {
let store = ContractStore::new();
let baseline = ContractBaseline {
command: "get_user".to_string(),
args: serde_json::json!({}),
shape: JsonShape::Object(HashMap::new()),
sample: "{}".to_string(),
recorded_at: "2026-05-26".to_string(),
};
store.record(baseline);
assert!(store.get("get_user").is_some());
assert_eq!(store.all().len(), 1);
assert_eq!(store.clear(), 1);
assert!(store.get("get_user").is_none());
}
#[test]
fn startup_timeline_records_phases() {
let timeline = StartupTimeline::new();
std::thread::sleep(Duration::from_millis(5));
timeline.mark("phase_1");
std::thread::sleep(Duration::from_millis(5));
timeline.mark("phase_2");
let report = timeline.report();
assert_eq!(report.len(), 2);
assert_eq!(report[0].name, "phase_1");
assert!(report[1].cumulative_ms >= report[0].cumulative_ms);
assert!(timeline.total_ms() > 0.0);
}
#[test]
fn enumerate_child_processes_returns_vec() {
let children = enumerate_child_processes();
for child in &children {
assert_ne!(child.pid, 0, "child PID should be non-zero");
assert_eq!(
child.ppid,
std::process::id(),
"parent PID should match current process"
);
assert!(!child.name.is_empty(), "child name should not be empty");
}
}
#[test]
fn enumerate_child_processes_with_spawned_child() {
let child = std::process::Command::new(if cfg!(windows) { "cmd.exe" } else { "sleep" })
.args(if cfg!(windows) {
&["/c", "timeout /t 10 /nobreak >nul"][..]
} else {
&["10"][..]
})
.spawn();
if let Ok(mut child_proc) = child {
let children = enumerate_child_processes();
assert!(
!children.is_empty(),
"should find at least one child process"
);
let found = children.iter().any(|c| c.pid == child_proc.id());
assert!(
found,
"spawned child (PID {}) should appear in enumeration",
child_proc.id()
);
let _ = child_proc.kill();
let _ = child_proc.wait();
}
}
#[test]
fn child_process_info_serializes() {
let info = ChildProcessInfo {
pid: 1234,
ppid: 5678,
name: "test-sidecar".to_string(),
memory_bytes: Some(1_048_576),
};
let json = serde_json::to_value(&info).unwrap();
assert_eq!(json["pid"], 1234);
assert_eq!(json["ppid"], 5678);
assert_eq!(json["name"], "test-sidecar");
assert_eq!(json["memory_bytes"], 1_048_576);
}
#[test]
fn child_process_info_serializes_no_memory() {
let info = ChildProcessInfo {
pid: 42,
ppid: 1,
name: "zombie".to_string(),
memory_bytes: None,
};
let json = serde_json::to_value(&info).unwrap();
assert!(json["memory_bytes"].is_null());
}
}