use crate::client::{Client, Endpoint};
use crate::i18n::Lang;
use crate::probes::{self, Cancel, Ctx, Depth, Event, Pace, Selection};
use crate::report::Report;
use crate::verdict;
use anyhow::Result;
#[derive(Clone)]
pub struct RunConfig {
pub endpoint: Endpoint,
pub claimed_model: Option<String>,
pub depth: Depth,
pub lang: Lang,
pub selection: Selection,
pub seed: Option<u64>,
pub http: Option<reqwest::Client>,
pub pace: Option<Pace>,
pub concurrency: usize,
pub max_in_flight: usize,
}
impl RunConfig {
pub fn new(endpoint: Endpoint) -> Self {
RunConfig {
endpoint,
claimed_model: None,
depth: Depth::Balanced,
lang: Lang::En,
selection: Selection::all(),
seed: None,
http: None,
pace: None,
concurrency: 1,
max_in_flight: 0,
}
}
pub fn model_only(mut self) -> Self {
self.selection = Selection::model_only();
self
}
pub fn turbo(mut self, in_flight: usize) -> Self {
self.selection = Selection::turbo();
self.depth = Depth::Fast;
self.concurrency = in_flight.max(1);
self.max_in_flight = in_flight;
self
}
pub fn concurrency(mut self, n: usize) -> Self {
self.concurrency = n.max(1);
self
}
pub fn max_in_flight(mut self, n: usize) -> Self {
self.max_in_flight = n;
self
}
pub fn depth(mut self, d: Depth) -> Self {
self.depth = d;
self
}
pub fn lang(mut self, l: Lang) -> Self {
self.lang = l;
self
}
pub fn seed(mut self, s: u64) -> Self {
self.seed = Some(s);
self
}
pub fn claimed_model(mut self, m: impl Into<String>) -> Self {
self.claimed_model = Some(m.into());
self
}
pub fn http(mut self, c: reqwest::Client) -> Self {
self.http = Some(c);
self
}
pub fn pace(mut self, min: std::time::Duration, max: std::time::Duration) -> Self {
self.pace = Some(Pace { min, max });
self
}
}
pub async fn run(
cfg: RunConfig,
cancel: &Cancel,
on_event: &mut (dyn FnMut(Event<'_>) + Send),
) -> Result<Report> {
let started_at = crate::util::iso8601_utc();
let t0 = crate::util::now_ms();
let seed = cfg.seed.unwrap_or_else(|| {
(crate::util::now_ms() as u64) ^ 0x9E37_79B9_7F4A_7C15
});
let claimed_model = cfg
.claimed_model
.clone()
.unwrap_or_else(|| cfg.endpoint.model.clone());
let protocol = cfg.endpoint.protocol;
let model = cfg.endpoint.model.clone();
let base_url = cfg.endpoint.base_url.clone();
let host = cfg.endpoint.host();
let client = match cfg.http.clone() {
Some(http) => Client::with_http(cfg.endpoint.clone(), http),
None => Client::new(cfg.endpoint.clone())?,
};
let client = client.with_limit(cfg.max_in_flight);
let ctx = Ctx::with_seed(client, cfg.depth, cfg.lang, claimed_model.clone(), seed);
let specs = cfg.selection.resolve();
let steps: Vec<String> = specs
.iter()
.map(|s| s.id.to_string())
.chain(
cfg.selection
.resolve_extra()
.iter()
.map(|p| p.id().to_string()),
)
.collect();
let extra = cfg.selection.resolve_extra();
let schedule = probes::Schedule {
pace: cfg.pace,
concurrency: cfg.concurrency,
};
let results = probes::run_with_extra(&ctx, &specs, &extra, cancel, schedule, on_event).await;
let l = cfg.lang;
let identity = verdict::build_identity(&results, &claimed_model, l);
let billing = verdict::build_billing(&results, &model, l);
let channel = verdict::build_channel(&results, l);
let v = verdict::decide(&results, &identity, &billing, &channel, protocol, l);
let perf = probes::perf::summarize(&ctx.perf.lock().unwrap());
let skipped = results
.iter()
.filter(|r| {
matches!(
r.status,
crate::report::Status::Skip | crate::report::Status::Error
)
})
.map(|r| t!(l, "{} ({}) — {}", "{}({}):{}", r.label, r.id, r.summary))
.collect();
Ok(Report {
schema_version: crate::report::schema_version(),
tool_version: env!("CARGO_PKG_VERSION").to_string(),
lang: l,
started_at,
finished_at: crate::util::iso8601_utc(),
duration_ms: (crate::util::now_ms() - t0) as u64,
host,
base_url,
protocol,
model,
claimed_model,
depth: cfg.depth.as_str().to_string(),
seed,
steps,
request_count: ctx.client.requests(),
results,
verdict: v,
identity,
billing,
channel,
perf,
skipped,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_future_is_send() {
fn assert_send<T: Send>(_: T) {}
let cfg = RunConfig::new(Endpoint {
base_url: "https://example.invalid".into(),
model: "m".into(),
..Default::default()
});
let cancel = Cancel::new();
let mut sink = |_: Event<'_>| {};
assert_send(run(cfg, &cancel, &mut sink));
}
fn ids(sel: Selection) -> Vec<&'static str> {
sel.resolve().iter().map(|s| s.id).collect()
}
#[test]
fn model_only_drops_endpoint_steps_but_keeps_preflight() {
let ids = ids(Selection::model_only());
assert!(
ids.contains(&"preflight"),
"the run is meaningless without it"
);
assert!(ids.contains(&"self_id"));
assert!(ids.contains(&"capability"));
assert!(ids.contains(&"perf"));
assert!(!ids.contains(&"billing"));
assert!(!ids.contains(&"channel"));
assert!(!ids.contains(&"missing_auth"));
}
#[test]
fn skip_wins_over_an_explicit_include() {
let sel = Selection {
only: vec!["identity".into(), "perf".into()],
skip: vec!["perf".into()],
..Default::default()
};
let ids = ids(sel);
assert!(ids.contains(&"self_id"));
assert!(!ids.contains(&"perf"));
}
#[test]
fn a_group_key_still_addresses_the_whole_family() {
let without = ids(Selection {
skip: vec!["identity".into()],
..Default::default()
});
for gone in ["self_id", "meta_creator", "world_knowledge", "capability"] {
assert!(!without.contains(&gone), "{gone} survived skip: [identity]");
}
assert!(without.contains(&"preflight"));
let only_identity = ids(Selection {
only: vec!["identity".into()],
..Default::default()
});
assert!(only_identity.contains(&"self_id") && only_identity.contains(&"verbosity"));
assert!(!only_identity.contains(&"cache_replay"));
}
#[test]
fn turbo_keeps_every_probe_a_verdict_rests_on() {
let kept_ids = ids(Selection::turbo());
for kept in [
"preflight",
"self_id",
"capability",
"verbosity",
"perf",
"cache_replay",
] {
assert!(kept_ids.contains(&kept), "turbo dropped {kept}");
}
for dropped in [
"meta_creator",
"context_claim",
"cutoff_claim",
"world_knowledge",
"signature_drift",
] {
assert!(!kept_ids.contains(&dropped), "turbo kept {dropped}");
}
let relayed = ids(Selection::model_only());
for id in &kept_ids {
assert!(relayed.contains(id), "{id} is not in model_only");
}
}
#[test]
fn plus_puts_the_endpoint_contract_checks_back() {
let ids = ids(Selection::turbo().plus(["stop_sequence", "system_adherence"]));
assert!(ids.contains(&"stop_sequence"));
assert!(ids.contains(&"system_adherence"));
assert!(ids.contains(&"self_id"), "and keeps what turbo had");
}
#[test]
fn plus_on_an_unfiltered_selection_is_a_no_op() {
let before = ids(Selection::all()).len();
let after = ids(Selection::all().plus(["self_id"])).len();
assert_eq!(before, after);
}
}