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}