1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// SPDX-License-Identifier: MIT OR Apache-2.0
//! 2026-09-26: The endpoint check that runs between pressing start and the benchmark starting.
//!
//! `coherence::probe_for` reads the endpoint's model list and asks the
//! `coherence::CHECKS` questions; a concern opens a modal. The check cannot
//! veto a run: the user can proceed past any concern.
//!
//! Owner: server tui.
//! Invariants:
//! - `poll` never returns `Some` twice: it drops the receiver on the first answer.
use std::sync::mpsc::{Receiver, TryRecvError, channel};
use metrale_bench::coherence::{self, Report};
/// 2026-09-26: Where the pre-flight has got to.
#[derive(Debug, PartialEq, Eq)]
pub enum Phase {
/// 2026-09-26: Asking. The modal shows a spinner.
Checking,
/// 2026-09-26: Something is worth saying before starting; the user picks.
Concern(String),
}
pub struct Preflight {
pub phase: Phase,
rx: Option<Receiver<Report>>,
}
impl Preflight {
/// 2026-09-26: Begin checking `target` on `runtime`.
///
/// The probe runs as a task and answers over a channel that the UI drains
/// on its tick, so the render thread never blocks on it.
pub fn begin(
runtime: &tokio::runtime::Handle,
target: metrale_bench::TargetEndpoint,
expectation: Option<metrale_bench::benchmark::ModelExpectation>,
timeout: std::time::Duration,
) -> Self {
let (tx, rx) = channel();
runtime.spawn(async move {
// 2026-09-26: A dropped receiver means the user moved on; not an error.
let _ = tx.send(coherence::probe_for(&target, expectation, timeout).await);
});
Self {
phase: Phase::Checking,
rx: Some(rx),
}
}
/// 2026-09-26: Drain the check. Returns `Some(true)` when the run should start now,
/// `Some(false)` when the user must be asked first, `None` while waiting.
pub fn poll(&mut self, target: &metrale_bench::TargetEndpoint) -> Option<bool> {
let rx = self.rx.as_ref()?;
match rx.try_recv() {
Ok(report) => {
self.rx = None;
match report.concern(target) {
None => Some(true),
Some(concern) => {
self.phase = Phase::Concern(concern);
Some(false)
}
}
}
Err(TryRecvError::Empty) => None,
// 2026-09-26: The task ended without answering; treat it as nothing to report and start.
Err(TryRecvError::Disconnected) => {
self.rx = None;
Some(true)
}
}
}
/// 2026-09-26: A pre-flight that stays waiting (its sender is leaked), for tests.
#[cfg(test)]
pub fn pending() -> Self {
let (_tx, rx) = channel();
std::mem::forget(_tx);
Self {
phase: Phase::Checking,
rx: Some(rx),
}
}
/// 2026-09-26: A pre-flight that has already found something to say, for tests.
#[cfg(test)]
pub fn with_concern(text: String) -> Self {
Self {
phase: Phase::Concern(text),
rx: None,
}
}
pub fn is_checking(&self) -> bool {
self.phase == Phase::Checking
}
}
#[cfg(test)]
#[path = "bench_preflight_tests.rs"]
mod tests;