Skip to main content

claude_codex/monitor/
mock.rs

1use std::{
2    collections::{HashMap, VecDeque},
3    path::PathBuf,
4    time::{Duration, Instant, SystemTime},
5};
6
7use super::{
8    ActiveRequest, CompletedRequest, EndpointKind, MonitorState, RequestStatus, SessionUsage,
9    session_summaries,
10};
11
12const TICK_MILLIS: u64 = 250;
13const REQUEST_TICKS: u64 = 24;
14const COMPLETED_PHASE: u64 = 20;
15
16#[derive(Debug)]
17pub struct MockMonitor {
18    started_at: SystemTime,
19    output_buckets: HashMap<Option<String>, Vec<(u64, u64)>>,
20    tick: u64,
21}
22
23impl MockMonitor {
24    pub fn new() -> Self {
25        let now = SystemTime::now();
26        Self {
27            started_at: now - Duration::from_secs(3_723),
28            output_buckets: initial_output_buckets(now),
29            tick: 0,
30        }
31    }
32
33    pub fn snapshot(&mut self) -> MonitorState {
34        let now = SystemTime::now();
35        advance_output_buckets(&mut self.output_buckets, now, self.tick);
36        let state = mock_state_for_tick(
37            self.started_at,
38            now,
39            Instant::now(),
40            self.tick,
41            &self.output_buckets,
42        );
43        self.tick = self.tick.wrapping_add(1);
44        state
45    }
46}
47
48impl Default for MockMonitor {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54pub fn mock_state() -> MonitorState {
55    let now = SystemTime::now();
56    let mut output_buckets = initial_output_buckets(now);
57    advance_output_buckets(&mut output_buckets, now, 0);
58    mock_state_for_tick(
59        now - Duration::from_secs(3_723),
60        now,
61        Instant::now(),
62        0,
63        &output_buckets,
64    )
65}
66
67fn mock_state_for_tick(
68    started_at: SystemTime,
69    now: SystemTime,
70    instant_now: Instant,
71    tick: u64,
72    output_buckets: &HashMap<Option<String>, Vec<(u64, u64)>>,
73) -> MonitorState {
74    let mut streaming = active_request(
75        now,
76        instant_now,
77        "req-active-codex",
78        Some("57c7c914-ada4-4f40-9672-985f950fbb66"),
79        Some(12),
80        EndpointKind::Messages,
81        Duration::from_secs(14),
82        RequestStatus::Streaming,
83    );
84    streaming.project = Some("claude-codex".to_string());
85    streaming.provider = Some("codex".to_string());
86    streaming.model = Some("claude-sonnet-4-6 → gpt-5.6-sol".to_string());
87    streaming.effort = Some("high".to_string());
88    streaming.generation_started_at = Some(now - Duration::from_secs(10));
89    streaming.generation_started_instant = Some(instant_now - Duration::from_secs(10));
90    streaming.generation_initial_output_tokens = 20;
91    streaming.generation_finished_at = Some(now - Duration::from_secs(2));
92    streaming.generation_duration = Some(Duration::from_secs(8));
93    streaming.streamed_bytes = 18_432;
94    streaming.stream_chunks = 96;
95    streaming.input_tokens = Some(12_480);
96    streaming.output_tokens = Some(420);
97    streaming.traffic_capture_path = Some(PathBuf::from(
98        "/tmp/claude-codex-demo/traffic/req-active-codex",
99    ));
100    let simulated_elapsed = Duration::from_millis(tick.saturating_mul(TICK_MILLIS));
101    streaming.started_at = now - Duration::from_secs(14) - simulated_elapsed;
102    streaming.started_instant = instant_now - Duration::from_secs(14) - simulated_elapsed;
103    streaming.generation_started_at = Some(now - Duration::from_secs(10) - simulated_elapsed);
104    streaming.generation_started_instant =
105        Some(instant_now - Duration::from_secs(10) - simulated_elapsed);
106    streaming.output_tokens = Some(420_u64.saturating_add(simulated_output_tokens(tick)));
107    streaming.input_tokens = Some(12_480_u64.saturating_add(tick / 4));
108    streaming.streamed_bytes = 18_432_u64.saturating_add(tick.saturating_mul(384));
109    streaming.stream_chunks = 96_u64.saturating_add(tick.saturating_mul(3));
110    streaming.generation_duration = Some(Duration::from_secs(4));
111    streaming.generation_initial_output_tokens = streaming
112        .output_tokens
113        .unwrap_or(0)
114        .saturating_sub(160 + (tick % 12) * 9);
115
116    let mut upstream = active_request(
117        now,
118        instant_now,
119        "req-active-kimi",
120        Some("terminal-refactor"),
121        Some(4),
122        EndpointKind::Messages,
123        Duration::from_secs(6),
124        RequestStatus::Upstream,
125    );
126    upstream.project = Some("terminal-dashboard".to_string());
127    upstream.provider = Some("kimi".to_string());
128    upstream.model = Some("kimi-k2.6".to_string());
129    upstream.effort = Some("medium".to_string());
130    upstream.input_tokens = Some(3_200);
131
132    let mut selected = active_request(
133        now,
134        instant_now,
135        "req-active-grok",
136        Some("mobile-client"),
137        Some(9),
138        EndpointKind::Messages,
139        Duration::from_secs(3),
140        RequestStatus::ProviderSelected,
141    );
142    selected.project = Some("companion-app".to_string());
143    selected.provider = Some("grok".to_string());
144    selected.model = Some("grok-composer-2.5-fast".to_string());
145    selected.effort = Some("low".to_string());
146
147    let started = active_request(
148        now,
149        instant_now,
150        "req-active-count",
151        None,
152        None,
153        EndpointKind::CountTokens,
154        Duration::from_secs(1),
155        RequestStatus::Started,
156    );
157
158    let mut byte_stream = active_request(
159        now,
160        instant_now,
161        "req-active-cursor",
162        Some("cursor-session"),
163        Some(2),
164        EndpointKind::Messages,
165        Duration::from_secs(22),
166        RequestStatus::Streaming,
167    );
168    byte_stream.project = Some("responsive-layout-lab".to_string());
169    byte_stream.provider = Some("cursor".to_string());
170    byte_stream.model = Some("cursor:claude-4.6-opus-high-thinking".to_string());
171    byte_stream.generation_started_at = Some(now - Duration::from_secs(20));
172    byte_stream.generation_started_instant = Some(instant_now - Duration::from_secs(20));
173    byte_stream.generation_finished_at = Some(now - Duration::from_secs(4));
174    byte_stream.streamed_bytes = 32_768_u64.saturating_add(tick.saturating_mul(640));
175    byte_stream.stream_chunks = 128_u64.saturating_add(tick.saturating_mul(4));
176    byte_stream.generation_duration = Some(Duration::from_millis(8_000 + (tick % 16) * 500));
177
178    let mut active = vec![streaming, upstream, selected, started, byte_stream];
179    let mut recent = VecDeque::new();
180
181    let mut success = completed_request(
182        now,
183        "req-complete-codex",
184        Some("57c7c914-ada4-4f40-9672-985f950fbb66"),
185        Some(11),
186        EndpointKind::Messages,
187        Duration::from_secs(18),
188        Duration::from_millis(4_820),
189        RequestStatus::Completed,
190        Some(200),
191    );
192    success.project = Some("claude-codex".to_string());
193    success.provider = Some("codex".to_string());
194    success.model = Some("claude-sonnet-4-6 → gpt-5.6-terra".to_string());
195    success.effort = Some("xhigh".to_string());
196    success.generation_duration = Some(Duration::from_secs(4));
197    success.generation_initial_output_tokens = 32;
198    success.streamed_bytes = 24_576;
199    success.stream_chunks = 142;
200    success.input_tokens = Some(125_600);
201    success.output_tokens = Some(832);
202    success.traffic_capture_path = Some(PathBuf::from(
203        "/tmp/claude-codex-demo/traffic/req-complete-codex",
204    ));
205    recent.push_back(success);
206
207    let mut unavailable = completed_request(
208        now,
209        "req-failed-kimi",
210        Some("terminal-refactor"),
211        Some(3),
212        EndpointKind::Messages,
213        Duration::from_secs(41),
214        Duration::from_millis(735),
215        RequestStatus::Failed,
216        Some(502),
217    );
218    unavailable.project = Some("terminal-dashboard".to_string());
219    unavailable.provider = Some("kimi".to_string());
220    unavailable.model = Some("kimi-for-coding".to_string());
221    unavailable.effort = Some("high".to_string());
222    unavailable.input_tokens = Some(8_900);
223    unavailable.error = Some("upstream connection closed before response headers".to_string());
224    unavailable.traffic_capture_path = Some(PathBuf::from(
225        "/tmp/claude-codex-demo/errors/req-failed-kimi.json",
226    ));
227    recent.push_back(unavailable);
228
229    let mut rate_limited = completed_request(
230        now,
231        "req-failed-cursor",
232        Some("cursor-session"),
233        Some(1),
234        EndpointKind::Messages,
235        Duration::from_secs(66),
236        Duration::from_secs(2),
237        RequestStatus::Failed,
238        Some(429),
239    );
240    rate_limited.project = Some("responsive-layout-lab".to_string());
241    rate_limited.provider = Some("cursor".to_string());
242    rate_limited.model = Some("cursor:claude-4.6-opus-high-thinking".to_string());
243    rate_limited.error = Some("provider rate limit reached; retry after 30 seconds".to_string());
244    recent.push_back(rate_limited);
245
246    let mut bytes = completed_request(
247        now,
248        "req-complete-grok",
249        Some("mobile-client"),
250        Some(8),
251        EndpointKind::Messages,
252        Duration::from_secs(93),
253        Duration::from_secs(3),
254        RequestStatus::Completed,
255        Some(201),
256    );
257    bytes.project = Some("companion-app".to_string());
258    bytes.provider = Some("grok".to_string());
259    bytes.model = Some("grok-4.5".to_string());
260    bytes.generation_duration = Some(Duration::from_secs(2));
261    bytes.streamed_bytes = 8_192;
262    bytes.stream_chunks = 64;
263    recent.push_back(bytes);
264
265    let mut events = completed_request(
266        now,
267        "req-complete-events",
268        Some("event-stream"),
269        Some(5),
270        EndpointKind::Messages,
271        Duration::from_secs(125),
272        Duration::from_secs(5),
273        RequestStatus::Completed,
274        Some(204),
275    );
276    events.project = Some("provider-playground".to_string());
277    events.provider = Some("codex".to_string());
278    events.model = Some("gpt-5.6-luna".to_string());
279    events.generation_duration = Some(Duration::from_secs(4));
280    events.stream_chunks = 48;
281    recent.push_back(events);
282
283    let mut bad_model = completed_request(
284        now,
285        "req-failed-model",
286        None,
287        None,
288        EndpointKind::Messages,
289        Duration::from_secs(162),
290        Duration::from_millis(12),
291        RequestStatus::Failed,
292        Some(400),
293    );
294    bad_model.model = Some("unknown-model".to_string());
295    bad_model.error = Some("unknown model; choose a registered provider model".to_string());
296    recent.push_back(bad_model);
297
298    let mut counted = completed_request(
299        now,
300        "req-complete-count",
301        Some("terminal-refactor"),
302        Some(2),
303        EndpointKind::CountTokens,
304        Duration::from_secs(214),
305        Duration::from_millis(84),
306        RequestStatus::Completed,
307        Some(200),
308    );
309    counted.project = Some("terminal-dashboard".to_string());
310    counted.provider = Some("kimi".to_string());
311    counted.model = Some("kimi-k2.6".to_string());
312    counted.input_tokens = Some(2_048);
313    recent.push_back(counted);
314
315    let mut server_error = completed_request(
316        now,
317        "req-failed-internal",
318        Some("event-stream"),
319        Some(4),
320        EndpointKind::Messages,
321        Duration::from_secs(278),
322        Duration::from_millis(420),
323        RequestStatus::Failed,
324        Some(500),
325    );
326    server_error.project = Some("provider-playground".to_string());
327    server_error.provider = Some("codex".to_string());
328    server_error.model = Some("gpt-5.5".to_string());
329    server_error.effort = Some("medium".to_string());
330    server_error.error =
331        Some("response translation failed: missing message stop event".to_string());
332    recent.push_back(server_error);
333
334    let mut no_status = completed_request(
335        now,
336        "req-abandoned",
337        Some("background-agent"),
338        Some(1),
339        EndpointKind::Messages,
340        Duration::from_secs(340),
341        Duration::from_secs(7),
342        RequestStatus::Failed,
343        None,
344    );
345    no_status.project = Some("automation-sandbox".to_string());
346    no_status.provider = Some("codex".to_string());
347    no_status.model = Some("gpt-5.4-mini".to_string());
348    no_status.error = Some("request future ended before completion".to_string());
349    recent.push_back(no_status);
350
351    add_simulated_requests(now, instant_now, tick, &mut active, &mut recent);
352    let mut session_usage = HashMap::<Option<String>, SessionUsage>::new();
353    for request in &recent {
354        let usage = session_usage.entry(request.session_id.clone()).or_default();
355        usage.input_tokens = usage
356            .input_tokens
357            .saturating_add(request.input_tokens.unwrap_or(0));
358        usage.output_tokens = usage
359            .output_tokens
360            .saturating_add(request.output_tokens.unwrap_or(0));
361    }
362    for request in &active {
363        let usage = session_usage.entry(request.session_id.clone()).or_default();
364        usage.input_tokens = usage
365            .input_tokens
366            .saturating_add(request.input_tokens.unwrap_or(0));
367        usage.output_tokens = usage
368            .output_tokens
369            .saturating_add(request.output_tokens.unwrap_or(0));
370    }
371    let sessions = session_summaries(&active, &recent, &session_usage, output_buckets);
372    MonitorState {
373        started_at,
374        sessions,
375        active,
376        recent: recent.into_iter().collect(),
377    }
378}
379
380fn initial_output_buckets(now: SystemTime) -> HashMap<Option<String>, Vec<(u64, u64)>> {
381    const HISTORIES: [(&str, [u64; 12]); 3] = [
382        (
383            "57c7c914-ada4-4f40-9672-985f950fbb66",
384            [0, 320, 780, 0, 1_600, 2_900, 850, 0, 440, 3_600, 2_200, 0],
385        ),
386        (
387            "terminal-refactor",
388            [180, 0, 420, 960, 0, 0, 1_300, 740, 2_100, 0, 350, 0],
389        ),
390        (
391            "cursor-session",
392            [0, 640, 1_200, 2_400, 3_800, 1_900, 0, 0, 820, 1_500, 0, 0],
393        ),
394    ];
395
396    let current_bucket = super::session_token_bucket(now);
397    let mut buckets = HashMap::<Option<String>, Vec<(u64, u64)>>::new();
398    for (session_id, history) in HISTORIES {
399        for (index, tokens) in history.iter().copied().enumerate() {
400            if tokens == 0 {
401                continue;
402            }
403            let offset = (history.len() - 1 - index) as u64;
404            record_output_bucket(
405                &mut buckets,
406                Some(session_id.to_string()),
407                current_bucket.saturating_sub(offset),
408                tokens,
409            );
410        }
411    }
412    buckets
413}
414
415fn advance_output_buckets(
416    buckets: &mut HashMap<Option<String>, Vec<(u64, u64)>>,
417    now: SystemTime,
418    tick: u64,
419) {
420    let current_bucket = super::session_token_bucket(now);
421    record_output_bucket(
422        buckets,
423        Some("57c7c914-ada4-4f40-9672-985f950fbb66".to_string()),
424        current_bucket,
425        if tick.is_multiple_of(2) { 10 } else { 25 },
426    );
427    if tick.is_multiple_of(4) {
428        record_output_bucket(
429            buckets,
430            Some("terminal-refactor".to_string()),
431            current_bucket,
432            120,
433        );
434    }
435    if tick.is_multiple_of(5) {
436        record_output_bucket(
437            buckets,
438            Some("cursor-session".to_string()),
439            current_bucket,
440            220,
441        );
442    }
443    if tick % REQUEST_TICKS == COMPLETED_PHASE {
444        let cycle = tick / REQUEST_TICKS;
445        if cycle % 4 != 3 {
446            record_output_bucket(
447                buckets,
448                Some(format!("demo-session-{cycle:03}")),
449                current_bucket,
450                128 + cycle.saturating_mul(11),
451            );
452        }
453    }
454    for samples in buckets.values_mut() {
455        samples.sort_by_key(|(bucket, _)| *bucket);
456    }
457}
458
459fn record_output_bucket(
460    buckets: &mut HashMap<Option<String>, Vec<(u64, u64)>>,
461    session_id: Option<String>,
462    bucket: u64,
463    tokens: u64,
464) {
465    let samples = buckets.entry(session_id).or_default();
466    if let Some((_, existing)) = samples
467        .iter_mut()
468        .find(|(existing_bucket, _)| *existing_bucket == bucket)
469    {
470        *existing = existing.saturating_add(tokens);
471    } else {
472        samples.push((bucket, tokens));
473    }
474}
475
476fn simulated_output_tokens(tick: u64) -> u64 {
477    let pairs = tick / 2;
478    pairs
479        .saturating_mul(35)
480        .saturating_add(if tick.is_multiple_of(2) { 0 } else { 10 })
481}
482
483fn add_simulated_requests(
484    now: SystemTime,
485    instant_now: Instant,
486    tick: u64,
487    active: &mut Vec<ActiveRequest>,
488    recent: &mut VecDeque<CompletedRequest>,
489) {
490    let cycle = tick / REQUEST_TICKS;
491    let phase = tick % REQUEST_TICKS;
492    let completed_cycles = cycle + u64::from(phase >= COMPLETED_PHASE);
493    let first_cycle = completed_cycles.saturating_sub(4);
494
495    for completed_cycle in first_cycle..completed_cycles {
496        let completion_tick = completed_cycle
497            .saturating_mul(REQUEST_TICKS)
498            .saturating_add(COMPLETED_PHASE);
499        let finished_ago = Duration::from_millis(
500            tick.saturating_sub(completion_tick)
501                .saturating_mul(TICK_MILLIS),
502        );
503        recent.push_front(simulated_completed_request(
504            now,
505            completed_cycle,
506            finished_ago,
507        ));
508    }
509
510    if phase < COMPLETED_PHASE {
511        active.insert(0, simulated_active_request(now, instant_now, cycle, phase));
512    }
513}
514
515fn simulated_active_request(
516    now: SystemTime,
517    instant_now: Instant,
518    cycle: u64,
519    phase: u64,
520) -> ActiveRequest {
521    let profile = simulation_profile(cycle);
522    let elapsed = Duration::from_millis(phase.saturating_mul(TICK_MILLIS));
523    let status = match phase {
524        0..=2 => RequestStatus::Started,
525        3..=5 => RequestStatus::ProviderSelected,
526        6..=9 => RequestStatus::Upstream,
527        _ => RequestStatus::Streaming,
528    };
529    let request_id = format!("req-simulated-{cycle:03}");
530    let session_id = format!("demo-session-{cycle:03}");
531    let mut request = active_request(
532        now,
533        instant_now,
534        &request_id,
535        Some(&session_id),
536        Some(cycle + 1),
537        EndpointKind::Messages,
538        elapsed,
539        status,
540    );
541    if phase >= 2 {
542        request.project = Some(profile.project.to_string());
543    }
544    if phase >= 3 {
545        request.provider = Some(profile.provider.to_string());
546        request.model = Some(profile.model.to_string());
547        request.effort = profile.effort.map(str::to_string);
548    }
549    if phase >= 10 {
550        let generation_ticks = phase - 9;
551        let generation_duration =
552            Duration::from_millis(generation_ticks.saturating_mul(TICK_MILLIS));
553        let output_tokens = generation_ticks
554            .saturating_mul(9 + cycle % 5)
555            .saturating_add(generation_ticks.saturating_mul(generation_ticks) / 3);
556        request.generation_started_at = Some(now - generation_duration);
557        request.generation_started_instant = Some(instant_now - generation_duration);
558        request.generation_finished_at = Some(now);
559        request.generation_duration = Some(generation_duration);
560        request.streamed_bytes = output_tokens.saturating_mul(24);
561        request.stream_chunks = generation_ticks.saturating_mul(3);
562        request.input_tokens = Some(1_800 + cycle.saturating_mul(137));
563        request.output_tokens = Some(output_tokens);
564    }
565    request
566}
567
568fn simulated_completed_request(
569    now: SystemTime,
570    cycle: u64,
571    finished_ago: Duration,
572) -> CompletedRequest {
573    let profile = simulation_profile(cycle);
574    let failed = cycle % 4 == 3;
575    let request_id = format!("req-simulated-{cycle:03}");
576    let session_id = format!("demo-session-{cycle:03}");
577    let latency = Duration::from_secs(5);
578    let mut request = completed_request(
579        now,
580        &request_id,
581        Some(&session_id),
582        Some(cycle + 1),
583        EndpointKind::Messages,
584        finished_ago,
585        latency,
586        if failed {
587            RequestStatus::Failed
588        } else {
589            RequestStatus::Completed
590        },
591        Some(if failed { 503 } else { 200 }),
592    );
593    request.project = Some(profile.project.to_string());
594    request.provider = Some(profile.provider.to_string());
595    request.model = Some(profile.model.to_string());
596    request.effort = profile.effort.map(str::to_string);
597    request.input_tokens = Some(1_800 + cycle.saturating_mul(137));
598    if failed {
599        request.error = Some("simulated upstream overload after streaming began".to_string());
600    } else {
601        request.generation_duration = Some(Duration::from_millis(2_750));
602        request.stream_chunks = 33 + cycle % 9;
603        request.output_tokens = Some(128 + cycle.saturating_mul(11));
604        request.streamed_bytes = request.output_tokens.unwrap_or(0).saturating_mul(24);
605    }
606    request
607}
608
609#[derive(Clone, Copy)]
610struct SimulationProfile {
611    project: &'static str,
612    provider: &'static str,
613    model: &'static str,
614    effort: Option<&'static str>,
615}
616
617fn simulation_profile(cycle: u64) -> SimulationProfile {
618    const PROFILES: [SimulationProfile; 4] = [
619        SimulationProfile {
620            project: "live-dashboard",
621            provider: "codex",
622            model: "gpt-5.6-sol",
623            effort: Some("high"),
624        },
625        SimulationProfile {
626            project: "api-client",
627            provider: "kimi",
628            model: "kimi-k2.6",
629            effort: Some("medium"),
630        },
631        SimulationProfile {
632            project: "editor-extension",
633            provider: "cursor",
634            model: "cursor:composer-2.5-fast",
635            effort: None,
636        },
637        SimulationProfile {
638            project: "agent-workbench",
639            provider: "grok",
640            model: "grok-4.5",
641            effort: Some("low"),
642        },
643    ];
644    let index = usize::try_from(cycle % PROFILES.len() as u64).unwrap_or(0);
645    PROFILES[index]
646}
647
648#[allow(clippy::too_many_arguments)]
649fn active_request(
650    now: SystemTime,
651    instant_now: Instant,
652    request_id: &str,
653    session_id: Option<&str>,
654    session_seq: Option<u64>,
655    endpoint: EndpointKind,
656    elapsed: Duration,
657    status: RequestStatus,
658) -> ActiveRequest {
659    ActiveRequest {
660        request_id: request_id.to_string(),
661        session_id: session_id.map(str::to_string),
662        session_seq,
663        project: None,
664        provider: None,
665        model: None,
666        effort: None,
667        endpoint,
668        started_at: now - elapsed,
669        started_instant: instant_now - elapsed,
670        generation_started_at: None,
671        generation_started_instant: None,
672        generation_initial_output_tokens: 0,
673        generation_finished_at: None,
674        generation_duration: None,
675        status,
676        streamed_bytes: 0,
677        stream_chunks: 0,
678        input_tokens: None,
679        output_tokens: None,
680        error: None,
681        traffic_capture_path: None,
682    }
683}
684
685#[allow(clippy::too_many_arguments)]
686fn completed_request(
687    now: SystemTime,
688    request_id: &str,
689    session_id: Option<&str>,
690    session_seq: Option<u64>,
691    endpoint: EndpointKind,
692    finished_ago: Duration,
693    latency: Duration,
694    status: RequestStatus,
695    http_status: Option<u16>,
696) -> CompletedRequest {
697    let finished_at = now - finished_ago;
698    CompletedRequest {
699        request_id: request_id.to_string(),
700        session_id: session_id.map(str::to_string),
701        session_seq,
702        project: None,
703        provider: None,
704        model: None,
705        effort: None,
706        endpoint,
707        started_at: finished_at - latency,
708        finished_at,
709        generation_started_at: None,
710        generation_started_instant: None,
711        generation_initial_output_tokens: 0,
712        generation_finished_at: None,
713        generation_duration: None,
714        status,
715        http_status,
716        latency,
717        streamed_bytes: 0,
718        stream_chunks: 0,
719        input_tokens: None,
720        output_tokens: None,
721        error: None,
722        traffic_capture_path: None,
723    }
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729    use crate::monitor::Throughput;
730
731    #[test]
732    fn mock_state_covers_monitor_statuses_and_optional_data() {
733        let state = mock_state();
734
735        for status in [
736            RequestStatus::Started,
737            RequestStatus::ProviderSelected,
738            RequestStatus::Upstream,
739            RequestStatus::Streaming,
740        ] {
741            assert!(state.active.iter().any(|request| request.status == status));
742        }
743        assert!(
744            state
745                .recent
746                .iter()
747                .any(|request| request.status == RequestStatus::Completed)
748        );
749        assert!(
750            state
751                .recent
752                .iter()
753                .any(|request| request.status == RequestStatus::Failed)
754        );
755        assert!(state.recent.iter().any(|request| request.error.is_some()));
756        assert!(
757            state
758                .recent
759                .iter()
760                .any(|request| request.traffic_capture_path.is_some())
761        );
762        assert!(
763            state
764                .recent
765                .iter()
766                .any(|request| request.endpoint == EndpointKind::CountTokens)
767        );
768        assert!(
769            state
770                .sessions
771                .iter()
772                .any(|session| session.project.is_some())
773        );
774        assert!(
775            state
776                .sessions
777                .iter()
778                .any(|session| session.session_id.is_none())
779        );
780    }
781
782    #[test]
783    fn mock_sparkline_grows_current_bucket_and_freezes_completed_buckets() {
784        let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
785        let session_id = Some("57c7c914-ada4-4f40-9672-985f950fbb66".to_string());
786        let current_bucket = super::super::session_token_bucket(now);
787        let mut buckets = initial_output_buckets(now);
788        advance_output_buckets(&mut buckets, now, 0);
789        let initial_samples = buckets.get(&session_id).unwrap().clone();
790        let initial_current = initial_samples
791            .iter()
792            .find(|(bucket, _)| *bucket == current_bucket)
793            .unwrap()
794            .1;
795        let initial_past = initial_samples
796            .iter()
797            .filter(|(bucket, _)| *bucket < current_bucket)
798            .cloned()
799            .collect::<Vec<_>>();
800
801        advance_output_buckets(&mut buckets, now + Duration::from_secs(1), 1);
802        let growing_samples = buckets.get(&session_id).unwrap();
803        assert_eq!(
804            growing_samples
805                .iter()
806                .find(|(bucket, _)| *bucket == current_bucket)
807                .unwrap()
808                .1,
809            initial_current + 25
810        );
811        assert_eq!(
812            growing_samples
813                .iter()
814                .filter(|(bucket, _)| *bucket < current_bucket)
815                .cloned()
816                .collect::<Vec<_>>(),
817            initial_past
818        );
819
820        let next_bucket_time = now + Duration::from_secs(10);
821        advance_output_buckets(&mut buckets, next_bucket_time, 2);
822        let advanced_samples = buckets.get(&session_id).unwrap();
823        assert_eq!(
824            advanced_samples
825                .iter()
826                .find(|(bucket, _)| *bucket == current_bucket)
827                .unwrap()
828                .1,
829            initial_current + 25
830        );
831        assert_eq!(
832            advanced_samples
833                .iter()
834                .find(|(bucket, _)| *bucket == current_bucket + 1)
835                .unwrap()
836                .1,
837            10
838        );
839    }
840
841    #[test]
842    fn mock_monitor_advances_tokens_rates_and_session_totals() {
843        let mut monitor = MockMonitor::new();
844        let first = monitor.snapshot();
845        let second = monitor.snapshot();
846        let first_request = first
847            .active
848            .iter()
849            .find(|request| request.request_id == "req-active-codex")
850            .unwrap();
851        let second_request = second
852            .active
853            .iter()
854            .find(|request| request.request_id == "req-active-codex")
855            .unwrap();
856        let first_session = first
857            .sessions
858            .iter()
859            .find(|session| {
860                session.session_id.as_deref() == Some("57c7c914-ada4-4f40-9672-985f950fbb66")
861            })
862            .unwrap();
863        let second_session = second
864            .sessions
865            .iter()
866            .find(|session| {
867                session.session_id.as_deref() == Some("57c7c914-ada4-4f40-9672-985f950fbb66")
868            })
869            .unwrap();
870
871        assert!(second_request.output_tokens > first_request.output_tokens);
872        assert_ne!(second_request.rate(), first_request.rate());
873        assert!(second_session.output_tokens > first_session.output_tokens);
874        assert_ne!(
875            second_session.output_token_samples,
876            first_session.output_token_samples
877        );
878        assert_eq!(second.started_at, first.started_at);
879    }
880
881    #[test]
882    fn mock_monitor_cycles_requests_through_lifecycle_and_new_sessions() {
883        let mut monitor = MockMonitor::new();
884        let states = (0..=24).map(|_| monitor.snapshot()).collect::<Vec<_>>();
885        let simulated = |tick: usize| {
886            states[tick]
887                .active
888                .iter()
889                .find(|request| request.request_id.starts_with("req-simulated-"))
890        };
891
892        assert_eq!(simulated(0).unwrap().status, RequestStatus::Started);
893        assert_eq!(
894            simulated(3).unwrap().status,
895            RequestStatus::ProviderSelected
896        );
897        assert_eq!(simulated(6).unwrap().status, RequestStatus::Upstream);
898        assert_eq!(simulated(10).unwrap().status, RequestStatus::Streaming);
899        assert!(simulated(20).is_none());
900        assert!(
901            states[20]
902                .recent
903                .iter()
904                .any(|request| request.request_id == "req-simulated-000")
905        );
906        assert_eq!(
907            simulated(24).unwrap().session_id.as_deref(),
908            Some("demo-session-001")
909        );
910    }
911
912    #[test]
913    fn mock_state_covers_each_throughput_display() {
914        let state = mock_state();
915        let rates = state
916            .active
917            .iter()
918            .map(ActiveRequest::rate)
919            .chain(state.recent.iter().map(CompletedRequest::rate))
920            .collect::<Vec<_>>();
921
922        assert!(
923            rates
924                .iter()
925                .any(|rate| matches!(rate, Throughput::TokensPerSecond(_)))
926        );
927        assert!(
928            rates
929                .iter()
930                .any(|rate| matches!(rate, Throughput::BytesPerSecond(_)))
931        );
932        assert!(
933            rates
934                .iter()
935                .any(|rate| matches!(rate, Throughput::EventsPerSecond(_)))
936        );
937        assert!(rates.iter().any(|rate| rate == &Throughput::None));
938    }
939}