//! Beholder framework — observer-side parsers and rewriters for structured output.
//!
//! A *beholder* attaches to a `TaskRun` at spawn time and turns raw output
//! chunks into structured `Event` rows. There are two modes:
//!
//! - **Rewriter:** mutates `argv` before the subprocess starts (e.g. cargo
//! gets `--message-format json`, planned by the cargo-verdict engine).
//! - **Parser:** reads human-formatted output and scrapes events out of it.
//!
//! # Integration point
//!
//! ```ignore
//! let ctx = RunContext { cwd: Some(&cwd), env: &env };
//! let result = registry.attach(raw_cmd, &BeholderSelect::Auto, OutputReader::Raw, &ctx);
//! // spawn the process with result.argv (may be rewritten)
//! // for each chunk captured:
//! if let Some(b) = result.beholder.as_mut() {
//! for event in b.parse_chunk(&chunk) {
//! store.append_event(run_id, ...);
//! }
//! }
//! // persist result.status on the run:
//! store.update_beholder_status(run_id, &result.status)?;
//! ```
//!
//!
//! @yah:ticket(R989-F21, "Engine v1, part 3: versioned result schema (cargo-verdict/1), `want` projections, and the `cargo verdict` CLI")
//! @yah:at(2026-10-16T22:09:35Z)
//! @yah:assignee(agent:bundle-anthropic-glimmerstone)
//! @yah:phase(P4)
//! @yah:parent(R989)
//! @yah:next("Tier: Cleric. The schema is a published contract: the typed door returns it verbatim as structuredContent, and QED and noisetable read it.")
//! @yah:next("W371 §4.5 module 4. `schema: \"cargo-verdict/1\"`; `request` (normalized); `argv` (what ran); `exit_code`; `outcome` (pass | test_failed | build_failed | interrupted); `build { success, counts { error, warning }, diagnostics [{ level, code, message, file, line, col, suggestion?, rendered? }], units { compiled, fresh } }`; `tests { totals, binaries [{ package, target, kind, structured, passed, failed, ignored, filtered_out, duration_ms, failures [{ name, location?, output, output_truncated_bytes }] }] }`. The camp adds run_id, lane, served_by and skew outside this schema (P5). Emit a JSON Schema for it, which the MCP outputSchema uses.")
//! @yah:next("`want: summary | failures | diagnostics | full` are VIEWS over the one structure (RFC 3558's own recommended shape), not separate code paths. A green test run under `summary` is counts only; a red one under `failures` carries names, panic locations and captured output, with truncation shown as bytes dropped.")
//! @yah:next("CLI: `cargo verdict <verb> [cargo args] [--want ...] [--json]`, usable from a terminal, a QED step or a camp that does not run yah. Copy rs-hack's product shape: library, CLI and MCP schema.")
//! @yah:verify("Golden tests: each F20 fixture renders to a checked-in cargo-verdict/1 JSON per `want`; the emitted JSON Schema validates every golden; `cargo verdict test` on a fixture exits with cargo's own exit code")
//! @yah:depends_on(R989-F20)
//! @yah:files(oss/cargo-verdict)
//! @yah:status(review)
//! @yah:handoff("LANDED oss/cargo-verdict/src/verdict.rs: SCHEMA=\"cargo-verdict/1\"; Verdict{schema, want, request (normalized, pre-plan), argv (what ran), added, exit_code (null only on signal), outcome: pass|test_failed|build_failed|interrupted, build{success, counts{error,warning}, diagnostics[{level,code,message,package,file,line,col,suggestion,rendered}], units{compiled,fresh}}, tests{totals{passed,failed,ignored,filtered_out,measured,unknown_binaries}, binaries[{package,target,kind,structured,passed?,failed?,ignored?,filtered_out?,duration_ms?,failures[{name,location,output,output_truncated_bytes}],output?,output_truncated_bytes}]}, notes}. Verdict::collect(&original, &plan, &demux, exit) builds the full one; Verdict::project(Want) gives the view. verdict::schema() emits the JSON Schema (schemars 1, already in yah's lockfile at 1.2.1) for the MCP outputSchema; `cargo verdict schema` prints it.")
//! @yah:handoff("ADDITIONS TO THE SPEC'D SHAPE, all additive: `want` (which view this is), `added` (the plan's additions, so nothing is hidden), `notes` (why structure is missing - plan.unstructured), `package` on diagnostics, `measured`/`unknown_binaries` in totals, and binary-level `output`/`output_truncated_bytes` (an unstructured binary - doctests, harness=false - has no per-test failures, so its raw output is what a failures view can show). Unstructured binaries have null counts unless libtest printed its human `test result:` line, which is parsed for counts only (W371's declared last resort; never failures).")
//! @yah:handoff("VIEWS (one structure, pruned copies of the same type): summary = counts only (no diagnostics, no failures, no output); failures (the CLI default) = error diagnostics + failing tests with name, panic location and output tail truncated to 4096 bytes with the dropped bytes counted, plus an unstructured binary's output when the run failed; diagnostics = every error and warning, rendered, no test detail; full = everything, untruncated. Warnings that a lib and its test harness both report are de-duplicated, as cargo's own display does.")
//! @yah:handoff("CLI: `cargo verdict <verb> [cargo args] [--want summary|failures|diagnostics|full] [--json]` (own flags read only before the first `--`), `cargo verdict parse <args>`, `cargo verdict schema`. Runs only Verdict-class requests (anything else is exit 2, a usage error, with the class named). It detects an existing runner and the host triple itself, streams cargo's progress to stderr, prints the projected result (human text, or JSON alone on stdout with --json), and exits with cargo's own code (128+signal when cargo was killed; the JSON then says exit_code null).")
//! @yah:handoff("GOLDENS: tests/goldens/{mixed-test,mixed-green,broken-check}.{summary,failures,diagnostics,full}.json, compared after normalizing host triple, thread ids, libtest timings, duration_ms and the compiled/fresh split (cache state). Regenerate with UPDATE_GOLDENS=1 cargo test --test goldens and read the diff - they will move with rustc's rendered text on a toolchain bump. Every golden validates against the emitted schema through a small validator in tests/goldens.rs that refuses any keyword it does not implement, with negative controls (bad enum, missing required field, wrong type all rejected).")
//! @yah:gotcha("CAMP BUG FOUND, NOT FIXED (not my file): .cargo/rustc-wrapper.sh is `#!/bin/sh` + `exec sccache`, and dash drops environment variables whose names are not shell identifiers, so `env!(\"CARGO_BIN_EXE_<name-with-a-hyphen>\")` fails to compile for any integration test built through this repo's wrapper - including this crate's, since cargo's config discovery walks up from oss/cargo-verdict into the yah root. tests/goldens.rs falls back to locating the binary beside the test executable. Anyone relying on CARGO_BIN_EXE_* for a hyphenated bin in this tree hits the same wall.")
//! @yah:verify("cargo test --manifest-path oss/cargo-verdict/Cargo.toml --test goldens (expect 4 passed: goldens per want + schema validation, view assertions, CLI exit codes 101/0/101 and usage 2, validator negative controls)")
//! @yah:verify("cd oss/cargo-verdict/tests/fixtures/mixed && CARGO_TARGET_DIR=/tmp/cv-mixed cargo run -q --manifest-path ../../../Cargo.toml -- verdict test --no-fail-fast; echo $? (expect a test_failed summary naming t::bad at src/lib.rs:17:9, then 101)")
use crate::types::{BeholderStatus, ChunkRef, Event, EventSource, Level, OutputChunk, Stream};
// ─── ToolVersionRange ─────────────────────────────────────────────────────────
/// Declared support range for the *tool's* structured-output schema (not the
/// beholder's own version). Used to surface `unknown_format` when drift is
/// detected at parse time.
///
/// Strings are the underlying tool's version: e.g. `"1.38.0"` for cargo
/// (when `--message-format=json-render-diagnostics` was stabilised).
/// `None` means "no known lower/upper bound".
#[derive(Debug, Clone)]
pub struct ToolVersionRange {
pub min: Option<&'static str>,
pub max: Option<&'static str>,
}
// ─── BeholderMode ─────────────────────────────────────────────────────────────
/// Whether a beholder modifies the invocation (rewriter) or reads human output
/// (parser).
#[derive(Clone)]
pub enum BeholderMode {
/// Rewrites argv before spawn to enable structured output.
Rewriter {
/// Applied to argv in-place when the beholder attaches.
adjust_argv: fn(&mut Vec<String>),
},
/// Rewrites argv by appending a fixed set of args. Used by user-defined
/// drop-in beholders whose `add_args` are loaded from TOML at runtime.
DynamicRewriter {
/// Args to append when not already present in argv.
add_args: Vec<String>,
},
/// Reads the tool's human-formatted output without rewriting argv.
Parser,
/// Rewrites argv through [`BeholderFactory::plan_argv`], which sees the
/// run's [`RunContext`]. For a rewrite that depends on where and how the
/// run happens — cargo's, which must not replace a target runner the
/// run's own cargo config already sets (R989-F23).
Planned,
}
impl BeholderMode {
/// Does this mode change the command line?
pub fn rewrites(&self) -> bool {
!matches!(self, BeholderMode::Parser)
}
}
/// Who reads a run's output, and what that reader does to it (R989-F24).
///
/// Decides whether a beholder that changes the command line may attach.
/// Named after the requirement, not the mechanism, for the reason R739-B10
/// renamed `tty_attached`: what matters is what happens to the bytes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputReader {
/// Nothing renders the bytes; any rewrite is fine (a QED step, a job).
#[default]
Raw,
/// Something renders the bytes as they are: a human at a terminal tile.
/// A rewriting beholder declines rather than change what they read.
Verbatim,
/// An attach client that promises its caller byte-identical output and
/// removes every stdout line beginning `{"reason":` before passing the
/// rest on — `yah build run`, the Bash door. A beholder whose rewrite adds
/// only such lines may attach ([`BeholderFactory::attach_for_stripping`]);
/// every other rewriter declines as it would for [`Self::Verbatim`].
StripsReasonLines,
}
impl OutputReader {
/// Would an ordinary rewrite change what this reader sees?
fn reads_bytes(self) -> bool {
!matches!(self, OutputReader::Raw)
}
}
/// What a beholder may know about the run it attaches to, beyond its argv.
#[derive(Debug, Clone, Copy, Default)]
pub struct RunContext<'a> {
/// The directory the command runs in.
pub cwd: Option<&'a std::path::Path>,
/// Environment the run sets on top of the inherited one.
pub env: &'a [(String, String)],
}
// ─── BeholderSelect ───────────────────────────────────────────────────────────
/// Caller-supplied attachment policy for `task.run`.
#[derive(Debug, Clone, Default)]
pub enum BeholderSelect {
/// Walk the registry in priority order; first matching beholder wins.
#[default]
Auto,
/// Pin to a specific beholder by name; bypass `matches` check.
///
/// The beholder runs even if it would normally decline (e.g. explicit
/// `--message-format=human` for cargo). Recorded as `forced-against-flags`
/// on `beholder_status` in that case.
Force(String),
/// Bytes-only, regardless of registry contents.
None,
}
// ─── AttachResult ─────────────────────────────────────────────────────────────
/// Returned by [`BeholderRegistry::attach`].
pub struct AttachResult {
/// Attached per-run beholder instance, if any.
pub beholder: Option<Box<dyn Beholder>>,
/// Status string to store on `TaskRunMeta.beholder_status`.
pub status: BeholderStatus,
/// Argv to pass to the subprocess launcher.
///
/// When a `Rewriter` beholder attaches, this reflects the adjusted argv.
/// Otherwise identical to the input tokens from `raw_cmd`.
pub argv: Vec<String>,
}
// ─── BeholderFactory ──────────────────────────────────────────────────────────
/// Static descriptor + factory for a beholder type. Held in the registry.
///
/// `create()` produces a fresh, independent per-run instance each time.
/// Implementors should be stateless — all per-run state belongs in the
/// instance returned by `create`.
pub trait BeholderFactory: Send + Sync {
fn name(&self) -> &'static str;
fn version(&self) -> &'static str;
/// Return `true` if this beholder handles the given invocation.
///
/// Receives the *resolved* argv (wrappers stripped). May inspect argv to
/// detect user-set conflicting flags (e.g. `--message-format=human`) and
/// return `false` to decline gracefully.
fn matches(&self, resolved_argv: &[String]) -> bool;
fn mode(&self) -> BeholderMode;
/// Create a fresh per-run instance.
fn create(&self) -> Box<dyn Beholder>;
/// Declared support range for the underlying tool's structured output format.
///
/// Informational: surfaced in `beholder_status` when format drift is detected
/// so agents can see which version window the beholder was built for.
/// `None` means the beholder makes no version claim.
fn tool_version_range(&self) -> Option<ToolVersionRange> { None }
/// The rewrite for [`BeholderMode::Planned`]. `argv` is a list of shell
/// words (the line is run by `sh -c` after a rewrite), so anything added
/// must already be quoted for the shell.
fn plan_argv(&self, _argv: &mut Vec<String>, _ctx: &RunContext<'_>) {}
/// R989-F24/F37 — the parse-only attach for a run under
/// [`OutputReader::StripsReasonLines`] whose `argv` ALREADY carries the
/// one flag that adds only stdout lines beginning `{"reason":` (the cargo
/// gate put it there, on the argv it spawns; this daemon never rewrites
/// the line). Returns the per-run instance, or `None` (the default) when
/// the argv is not this beholder's or lacks the flag. Never touches `argv`.
fn attach_for_stripping(&self, _argv: &[String]) -> Option<Box<dyn Beholder>> {
None
}
}
// ─── Beholder ─────────────────────────────────────────────────────────────────
/// Per-run stateful chunk processor.
///
/// The instance is private to a single `TaskRun`. `parse_chunk` is called for
/// every captured chunk in arrival order and may produce zero or more structured
/// events. Implementations must be incremental — buffering everything to EOF
/// defeats the lossless-during-execution property.
///
/// For tools that dump a single JSON document at exit (e.g. ESLint, Biome),
/// implement `parse_chunk` as a simple buffer accumulator and do all parsing
/// in `on_done`, which the driver calls once the PTY reader exits.
pub trait Beholder: Send {
fn name(&self) -> &'static str;
fn version(&self) -> &'static str;
fn mode(&self) -> BeholderMode;
/// Extract structured events from one output chunk.
fn parse_chunk(&mut self, chunk: &OutputChunk) -> Vec<Event>;
/// Called once after the last chunk has been delivered (PTY EOF).
///
/// Use this for tools that write a single JSON document at exit rather than
/// streaming line-by-line. The driver passes the run id and a final
/// timestamp so the returned events can be fully populated.
///
/// Default: returns no events (correct for streaming beholders).
fn on_done(&mut self, _run_id: &crate::types::TaskRunId, _offset_ms: u32) -> Vec<Event> {
Vec::new()
}
/// Returns a reason string if the beholder detected that the tool's output
/// format is unrecognized (schema drift). When `Some`, the driver detaches
/// this beholder and updates `beholder_status` to `unknown_format:name`.
///
/// Default: `None` — format OK or not yet probed.
fn unknown_format_reason(&self) -> Option<&str> { None }
}
// ─── BeholderRegistry ────────────────────────────────────────────────────────
/// Registry of available beholders, consulted at `task.run` time.
///
/// Entries are tried in insertion (priority) order. Earlier registrations have
/// higher priority when multiple beholders' `matches` predicates would fire.
pub struct BeholderRegistry {
entries: Vec<Box<dyn BeholderFactory>>,
}
impl BeholderRegistry {
pub fn new() -> Self {
Self { entries: Vec::new() }
}
/// Register a beholder factory. Earlier entries have higher priority.
pub fn register(&mut self, factory: Box<dyn BeholderFactory>) {
self.entries.push(factory);
}
/// Determine and attach a beholder for the given invocation.
///
/// - Tokenizes `raw_cmd` and strips wrapper binaries (bunx, npx, pnpm, npm exec).
/// - `reader` says who reads this run's output (see [`OutputReader`]).
/// For anything but `Raw`, rewriting beholders decline in `Auto` mode
/// rather than change what that reader sees — except that a
/// `StripsReasonLines` reader takes a beholder's
/// [`BeholderFactory::attach_for_stripping`] rewrite (R989-F24).
/// `Force` still attaches but records `forced-against-verbatim`.
///
/// This flag used to be `tty_attached`, and the rename is the R739-B10
/// fix rather than tidying. A PTY was never the thing that mattered: what
/// matters is whether anything downstream renders the bytes. `yah build
/// run` moved off a PTY onto pipes (R739-F6) and instantly started
/// leaking `--message-format=json-render-diagnostics` output into an
/// agent's stdout, because the only flag that could have stopped it was
/// named after the mechanism instead of the requirement.
/// - Behaviour depends on `select`:
/// - `Auto` — walk in priority order, first `matches` hit wins.
/// - `Force(name)` — find by name; bypass `matches` (records
/// `forced-against-flags` if the beholder would have declined, or
/// `forced-against-verbatim` if the output is read verbatim and mode is
/// Rewriter).
/// - `None` — bytes-only; no beholder attached.
/// - If a `Rewriter` beholder attaches, its `adjust_argv` is applied to the
/// returned `AttachResult.argv` and the diff is surfaced on `status`.
pub fn attach(
&self,
raw_cmd: &str,
select: &BeholderSelect,
reader: OutputReader,
ctx: &RunContext<'_>,
) -> AttachResult {
let mut argv = resolve_argv(raw_cmd);
// A shell line is not an argv, and this registry can only reason about
// an argv. `resolve_argv` whitespace-splits whatever it is given, so a
// `Rewriter` beholder's `adjust_argv` appends its flag to the LAST
// token of the line — which for `cargo check … 2>&1 | tail -12` is
// `tail`'s argument list, not cargo's. Observed live: `tail:
// unrecognized option '--message-format=json-render-diagnostics'`,
// i.e. the beholder does not merely mis-instrument the run, it
// destroys the command.
//
// Splitting the line properly is not the fix. Even placed correctly,
// structured JSON on stdout would flow into the caller's own `| rg …`
// and give them nothing they asked for; and this crate deliberately
// carries no shell parser. So the honest answer for anything that is
// not one simple command is bytes-only, recorded as a decline rather
// than as `none:auto` so the reason is legible on the run.
if !is_single_simple_command(raw_cmd) && !matches!(select, BeholderSelect::None) {
return AttachResult {
beholder: None,
status: BeholderStatus::declined("auto", "compound-command"),
argv,
};
}
match select {
BeholderSelect::None => AttachResult {
beholder: None,
status: BeholderStatus::none_explicit(),
argv,
},
BeholderSelect::Force(name) => {
match self.entries.iter().find(|e| e.name() == name.as_str()) {
None => AttachResult {
beholder: None,
status: BeholderStatus::none_auto(),
argv,
},
Some(factory) => {
let would_decline = !factory.matches(&argv);
let is_rewriter = factory.mode().rewrites();
let base_status = if would_decline {
BeholderStatus::forced_against_flags(factory.name(), factory.version())
} else if reader.reads_bytes() && is_rewriter {
BeholderStatus::forced_against_verbatim(
factory.name(),
factory.version(),
)
} else {
BeholderStatus::forced(factory.name(), factory.version())
};
let added = rewrite_if_rewriter(factory.as_ref(), ctx, &mut argv);
let status = base_status.with_rewrite(added);
AttachResult { beholder: Some(factory.create()), status, argv }
}
}
}
BeholderSelect::Auto => {
for factory in &self.entries {
// R989-F24/F37: a run whose argv already carries the one
// flag a stripping reader can undo attaches parse-only.
// Asked BEFORE `matches`, which declines an argv that
// names a `--message-format`, this flag included.
if reader == OutputReader::StripsReasonLines && factory.mode().rewrites() {
if let Some(beholder) = factory.attach_for_stripping(&argv) {
let status = BeholderStatus::attached_for_stripping(
factory.name(),
factory.version(),
);
return AttachResult { beholder: Some(beholder), status, argv };
}
}
if factory.matches(&argv) {
let is_rewriter = factory.mode().rewrites();
// Somebody reads these bytes: a Rewriter beholder would
// change what they read, so it declines instead.
if reader.reads_bytes() && is_rewriter {
return AttachResult {
beholder: None,
status: BeholderStatus::declined(factory.name(), "verbatim-output"),
argv,
};
}
let added = rewrite_if_rewriter(factory.as_ref(), ctx, &mut argv);
let status = BeholderStatus::attached(factory.name(), factory.version())
.with_rewrite(added);
return AttachResult {
beholder: Some(factory.create()),
status,
argv,
};
}
}
AttachResult {
beholder: None,
status: BeholderStatus::none_auto(),
argv,
}
}
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for BeholderRegistry {
fn default() -> Self {
Self::new()
}
}
// ─── helpers ─────────────────────────────────────────────────────────────────
/// Apply argv rewriting if `mode` is `Rewriter` or `DynamicRewriter`, and
/// return the args that were added. Returns an empty `Vec` for `Parser` mode
/// or when the rewriter added nothing.
fn rewrite_if_rewriter(
factory: &dyn BeholderFactory,
ctx: &RunContext<'_>,
argv: &mut Vec<String>,
) -> Vec<String> {
match factory.mode() {
BeholderMode::Rewriter { adjust_argv } => {
let before = argv.clone();
adjust_argv(argv);
argv.iter().filter(|a| !before.contains(a)).cloned().collect()
}
BeholderMode::DynamicRewriter { add_args } => {
let mut added = Vec::new();
for arg in add_args {
if !argv.contains(&arg) {
argv.push(arg.clone());
added.push(arg);
}
}
added
}
BeholderMode::Planned => {
let before = argv.clone();
factory.plan_argv(argv, ctx);
argv.iter().filter(|a| !before.contains(a)).cloned().collect()
}
BeholderMode::Parser => Vec::new(),
}
}
// ─── argv resolution ─────────────────────────────────────────────────────────
/// Tokenize `raw_cmd` and strip common wrapper binaries so beholders see the
/// bare tool name as `argv[0]`.
///
/// Wrappers stripped:
/// - `bunx` / `npx` — drop the wrapper token.
/// - `pnpm` / `npm exec` — drop the wrapper and optional `exec` subcommand.
pub fn resolve_argv(raw_cmd: &str) -> Vec<String> {
let mut argv: Vec<String> = tokenize(raw_cmd);
loop {
match argv.first().map(String::as_str) {
Some("bunx") | Some("npx") => {
argv.remove(0);
}
Some("pnpm") | Some("npm") => {
argv.remove(0);
if argv.first().map(String::as_str) == Some("exec") {
argv.remove(0);
}
break;
}
_ => break,
}
}
argv
}
/// Split on ASCII whitespace. Sufficient for well-formed command strings;
/// does not handle shell quoting.
fn tokenize(s: &str) -> Vec<String> {
s.split_ascii_whitespace().map(str::to_owned).collect()
}
/// Is `raw_cmd` one simple command, i.e. is treating it as an argv sound?
///
/// Deliberately a character scan and deliberately over-strict: this crate has
/// no shell parser and should not grow one to answer a safety question. Every
/// false positive costs one run its structured events; every false negative
/// costs that run its *command*, because a `Rewriter` beholder will splice a
/// flag into whatever program happens to be last on the line.
///
/// `$` is in the set for command substitution (`$(…)`) rather than for plain
/// variable expansion, which would be harmless — but distinguishing the two
/// needs the parser this function exists to avoid.
fn is_single_simple_command(raw_cmd: &str) -> bool {
!raw_cmd
.chars()
.any(|c| matches!(c, '|' | '&' | ';' | '<' | '>' | '(' | ')' | '`' | '$' | '\n'))
}
// ─── Cargo beholder ───────────────────────────────────────────────────────────
/// Factory for the bundled cargo beholder (Tier 1.5, planned rewrite).
///
/// Since R989-F23 this is a thin seam over the `cargo-verdict` engine
/// (`oss/cargo-verdict`), which owns what a cargo argv and cargo's output
/// *mean*; this module keeps only the ingestion: attach, rewrite, probe, and
/// turn the engine's parsed lines into run-store [`Event`]s.
///
/// Matches a cargo line the engine classes as a **verdict** (`check`, `clippy`,
/// `build`, `test`, `doc`, `bench`, `rustc`, …) — not `run`, `fix` or
/// `publish`, which the old subcommand list matched but which are tenancy or
/// mutation, and not `nextest`, whose output the engine does not adapt.
/// Declines when the user already chose a `--message-format` (respects
/// explicit intent), and — since the rewrite replaces cargo's human output
/// with JSON — whenever something downstream is going to render those bytes
/// ([`OutputReader::Verbatim`]; R070-T5, R739-B10). For a reader that strips
/// `{"reason":` lines ([`OutputReader::StripsReasonLines`], the Bash door) it
/// attaches with a smaller rewrite instead: see
/// [`CargoBeholderFactory::attach_for_stripping`].
///
/// The rewrite is the engine's plan ([`cargo_verdict::plan()`]), spliced into
/// the line as written: `--message-format json` for every verdict, and for
/// `cargo test` the scoped libtest runner (`--config
/// target.<host>.runner=[…]`), unless the run's cwd or env already configures
/// a target runner. See the R989-F23 handoff for why `json` and not
/// `json-render-diagnostics`.
///
/// @yah:ticket(R989-F19, "Engine v1, part 1: scaffold the oss/ cargo-structuring workspace with its Request model and a round-tripping argv parser")
/// @yah:at(2026-10-16T22:07:54Z)
/// @yah:assignee(agent:bundle-anthropic-glimmerstone)
/// @yah:phase(P4)
/// @yah:parent(R989)
/// @yah:next("Tier: Cleric. This is the vocabulary every later phase keys on: dedup, combine, classes. It is hard to change once the typed door ships.")
/// @yah:next("W371 §4.5 module 1. An independent Cargo workspace under oss/ (no `workspace = true` inheritance from yah's root; profile blocks in lockstep, as oss/qed/Cargo.toml documents). `Request { verb, selection (packages | workspace, exclude), targets, features (list | all | no-default), profile, target_triple, toolchain, test: { filters, exact, skip, include_ignored, fail_fast, threads, nocapture }, env_allowlist, passthrough: [opaque] }`. `parse(argv) -> Request` and `render(Request) -> argv` round-trip under test. Flags the model does not cover pass through verbatim and join the dedup key as opaque bytes, so coverage gaps never block structure (the jbr/cargo-mcp failure, W323 §2). The class (verdict / tenancy / mutation / query) falls out of `verb`.")
/// @yah:next("Parse corpus: every cargo line build_skew_stamp's tests already cover, plus W323 §3's misses (`doc`, `bench`, `nextest`, `+toolchain`), plus R989-B7's query spellings. This parser replaces the recognizer in a later ticket, so it must classify everything the recognizer classifies today.")
/// @yah:verify("cargo test in the new workspace: round-trip property test (parse then render then parse is a fixed point) and the full parse corpus, each case asserting class and normalized Request")
/// @yah:files(oss/cargo-verdict)
/// @yah:next("DECIDED (W371 §8.4, operator, F4455, 2026-10-16): B. An oss/ product with library, CLI and schema, exported to its own yah-ai mirror. The NAME is still the implementer's to settle: check crates.io for `cargo-verdict` before creating the directory, and if it is taken, pick a free name and update W371 §4.5 and every R989 ticket that says cargo-verdict.")
/// @yah:status(review)
/// @yah:handoff("NAME CHECKED 2026-10-17: cargo-verdict is free. crates.io/api/v1/crates/cargo-verdict -> 404 and sparse index index.crates.io/ca/rg/cargo-verdict -> 404; serde -> 200 through the same proxy as positive control. Recorded in W371 §4.5. No ticket rename needed.")
/// @yah:handoff("LANDED oss/cargo-verdict: own [workspace] (no root inheritance; profile block in lockstep with the root), package cargo-verdict 0.1.0, publish = false (T25 flips it), lib cargo_verdict + bin cargo-verdict (`cargo verdict parse <cargo args>` prints {class, argv, request} as JSON; strips the `verdict` arg cargo passes). Deps: serde, serde_json only. Cargo.lock created. Modules src/verb.rs (Verb, Class, dialect table), src/request.rs (Request and parts), src/parse.rs, src/render.rs.")
/// @yah:handoff("PUBLIC API for F20/F21/F22: `parse(&[impl AsRef<str>]) -> Request` (args AFTER `cargo`; total, never fails); `render(&Request) -> Vec<String>` (= Request::render). Request { toolchain, globals: Globals{directory(-C), config, unstable(-Z), passthrough} (pre-verb flags, kept apart because position is meaning for external subcommands), verb: Verb, help, manifest_path, target_dir, selection: Selection{workspace, packages, exclude}, targets: Targets{lib,bins,bin[],examples,example[],tests,test[],benches,bench[],all_targets,doc}, features: Features{list,all,no_default}, profile, target_triples, message_format, config, unstable, test: TestArgs{filters,exact,skip,ignored: Ignored{Exclude,Include,Only},fail_fast,no_run,threads,nocapture}, env_allowlist: BTreeMap (never from argv, never rendered), args (operands; for opaque verbs the whole verbatim tail), passthrough (unmodelled post-verb flags, verbatim, value kept adjacent), trailing (unmodelled after `--`) }. Request::class() -> Class{Verdict,Tenancy,Mutation,Query,Unknown}; packages_in_scope() (the old CargoBuild::packages); effective_profile(); profile_dir(). Verb::{from_word, word, class, default_profile}; VERB_WORDS. All types are serde Serialize/Deserialize.")
/// @yah:handoff("PARSER RULES: verbs read in one of three dialects - Build (cargo build flags modelled), Test (also filters + libtest args after `--`: --exact/--skip/--ignored/--include-ignored/--test-threads/--nocapture), Opaque (fmt, add, metadata, xtask, unknown cargo-foo...: tail kept verbatim, because an external subcommand's flags are its own). nextest has its own dialect: --cargo-profile is cargo's profile, --profile/-P/--message-format are nextest's (passthrough), -j/--test-threads are threads, --run-ignored maps to Ignored. Unknown flags glue a directly following positional (adjacency rule), so guessing wrong under-models and never changes meaning. Render is canonical: long verb names, `--package`, `--profile release` (never --release), features comma-joined, test filters after `--`, value flags take `--name=value` form when the value starts with '-'.")
/// @yah:handoff("CLASS DECISIONS a reviewer should check: (1) a fifth Class::Unknown for unknown subcommands/aliases - the four classes cannot place `cargo udeps` honestly. (2) any -h/--help cargo sees -> Query (B7); after `--` it is the binary's, so `cargo test -- --help` stays Verdict; xtask is exempt (its tail is the xtask binary's). (3) clippy --fix -> Mutation. (4) xtask -> Tenancy (it is `run -p xtask --`), and `cargo xtask install --profile debug` no longer sets profile (that flag is the xtask binary's); lane dir is debug either way. (5) profile-less `cargo bench` -> profile_dir release (cargo's bench profile inherits release); the old recognizer never saw bench. (6) `--all -p x` keeps the -p in selection.packages (render never drops input) but packages_in_scope() is empty, matching the recognizer. (7) package -> Verdict; install/publish/clean/fix -> Mutation; nextest run/list/archive -> Verdict, bare nextest/show-config -> Query, nextest self -> Mutation.")
/// @yah:handoff("FOR R989-F22: the recognizer admitted exactly {verb in Build,Test,Check,Clippy and class Verdict} plus Verb::Xtask; tests/corpus.rs states this as recognizer_would_admit() and asserts it over the whole harvested corpus. Admission policy for the new verdict verbs (doc/bench/nextest/rustc/package) and for Tenancy run is F22's call, not this ticket's. The shell `cwd` (cd oss/qed && cargo test) is not argv and is NOT in Request; the Bash door keeps carrying it. ROOT WIRING: the root already excludes all of oss/ (`exclude = [\"oss\", \"external\"]`), so no exclude edit was needed; the [patch.crates-io] path entry was deliberately NOT added - nothing depends on the crate yet, cargo warns `patch ... was not used in the crate graph` on every root build for an unused patch, and F22's own next line owns adding it beside the yubaba/qed bridges.")
/// @yah:handoff("TESTS: cargo test --manifest-path oss/cargo-verdict/Cargo.toml -> corpus 23 passed / 0 failed, roundtrip 2 passed / 0 failed (40,000 seeded random argvs from a hostile vocabulary: parse(render(parse(x))) == parse(x), render canonical, class stable). Negative control run: commenting out the trailing render made the roundtrip test FAIL with a printed argv, then restored. cargo clippy --all-targets: 0 warnings. Corpus = every argv build_skew_stamp's tests cover (read from HEAD), W323 §3's doc/bench/nextest/+toolchain, R989-B7's query table, plus the W371 §4.4(2) scoped-runner line F20 will generate.")
/// @yah:verify("cargo test --manifest-path oss/cargo-verdict/Cargo.toml (expect corpus 23 passed, roundtrip 2 passed)")
/// @yah:verify("cargo clippy --manifest-path oss/cargo-verdict/Cargo.toml --all-targets (expect no warnings)")
/// @yah:verify("cargo run --manifest-path oss/cargo-verdict/Cargo.toml -q -- parse +nightly test -p yah -- --exact foo (expect class verdict, toolchain nightly, filters [foo])")
///
/// @yah:ticket(R989-T25, "Create the engine's yah-ai mirror, teach export-oss.sh about it, and cut the first crates.io release")
/// @yah:status(review)
/// @yah:at(2026-10-20T10:53:02Z)
/// @yah:assignee(agent:bundle-anthropic-ashguard)
/// @yah:phase(P4)
/// @yah:parent(R989)
/// @yah:next("DECIDED (W371 §8.4, operator, F4455, 2026-10-16): the engine is an oss/ product with its own mirror. That approves the product, not the moment it goes public: confirm with the operator immediately before the first public push and the crates.io publish, because neither can be taken back.")
/// @yah:next("Tier: Warrior. A procedure CLAUDE.md already spells out.")
/// @yah:next("Follow CLAUDE.md's 'Co-developed OSS repos' procedure as written for orphan-gc: `gh repo create yah-ai/<name> --public` run as yah-human (check `gh auth status` first); add the subtree to scripts/export-oss.sh (and to mirror_repo() if the mirror name differs from the directory); `scripts/export-oss.sh --dry-run <name>`, then the real export; flip `publish`; `scripts/oss-publish.sh oss/<name>`. Push identity must resolve to yah-human (pre-push hook + PUSH_ACCOUNTS); commit authors must be inside the export-oss.sh allowlist.")
/// @yah:next("Add the project to the oss/ list in CLAUDE.md's 'Co-developed OSS repos' section.")
/// @yah:verify("The mirror exists and its Activity tab shows yah-human as pusher; `cargo search <name>` shows the published version; export-oss.sh --dry-run for the subtree is clean")
/// @yah:depends_on(R989-F21)
/// @yah:files(scripts/export-oss.sh)
/// @yah:files(oss/cargo-verdict)
/// @yah:handoff("PREP DONE, nothing public. oss/cargo-verdict (crate cargo-verdict 0.1.0, lib cargo_verdict + bin cargo-verdict) is an independent [workspace], MIT OR Apache-2.0, repository=github.com/yah-ai/cargo-verdict, publish=false. Added README.md, LICENSE-MIT, LICENSE-APACHE, readme field (all UNCOMMITTED/untracked - git policy defer; commit them before export, export-oss.sh warns untracked files are not exported). Added cargo-verdict to ALL_REPOS in scripts/export-oss.sh (mirror name == dir, no mirror_repo entry). export-oss.sh --dry-run --allow-dirty-tree cargo-verdict: fails as expected, mirror missing, prints gh repo create command. cargo package --list --allow-dirty: PASS (incl. LICENSEs, README). cargo publish --dry-run refuses only because publish=false. CLAUDE.md oss list updated.")
/// @yah:handoff("OPERATOR SEQUENCE: 1) git add oss/cargo-verdict scripts/export-oss.sh CLAUDE.md && commit (as Yah Dev). 2) gh auth status (must show yah-human active/usable; see CLAUDE.md push-identity section). 3) gh repo create yah-ai/cargo-verdict --public. 4) scripts/export-oss.sh --dry-run cargo-verdict, then scripts/export-oss.sh cargo-verdict. 5) flip publish: in oss/cargo-verdict/Cargo.toml delete `publish = false` (and the 'Held until R989-T25' comment above it). 6) cargo publish --dry-run in oss/cargo-verdict, commit the flip and re-export, then scripts/oss-publish.sh oss/cargo-verdict.")
/// @yah:handoff("Tree anchor at handoff: 0c62b1fe121e008b434c7e5a004bc5fa2e407a09 — the shared tree as I left it. Diff against it (`git diff 0c62b1fe121e008b434c7e5a004bc5fa2e407a09..HEAD`) to see what landed under you, and quote this SHA rather than 'HEAD' in any revert/restore instruction.")
/// @yah:next("operator confirms, then run the sequence")
/// @yah:handoff("OPERATOR CONFIRMED (2026-10-19, chat): go public, and keep the name cargo-verdict (rename to cargo-json was considered and declined, since cargo already prints raw JSON and this crate returns the condensed verdict). crates.io name checked free 2026-10-19 (API 404). Remaining: the operator runs the OPERATOR SEQUENCE above (git policy defer).")
/// @yah:handoff("LANDED (operator ran the sequence 2026-10-19/20): mirror github.com/yah-ai/cargo-verdict exists, head 888bef17 'export: cargo-verdict snapshot' authored human@yah.dev, GitHub author login yah-human; crates.io cargo-verdict 0.1.0 published 2026-10-20T04:42Z. Committed as dfc3a1e2 + bb7fc1de.")
/// @yah:handoff("Residual fixed this pass: bb7fc1de flipped publish to `true` but left the stale 'Held until R989-T25' comment; deleted both lines (true is the default) in oss/cargo-verdict/Cargo.toml. CLAUDE.md cargo-verdict bullet rewritten from 'mirror does not exist' to its live state, and notes that subtree split finds no history for this prefix so every export is a snapshot commit.")
/// @yah:handoff("Mirror is ONE commit behind: the bb7fc1de publish flip was never re-exported (mirror Cargo.toml still says publish = false). Uncommitted under git-policy defer; operator to run: git add oss/cargo-verdict/Cargo.toml CLAUDE.md && git commit -m 'R989-T25: cargo-verdict public; drop stale publish hold' && scripts/export-oss.sh cargo-verdict")
/// @yah:verify("export-oss.sh --dry-run --allow-dirty-tree cargo-verdict: EXIT 0, 'would push 9f2a702a -> main (1 new commits)' = the unexported publish flip (diff mirror vs HEAD:oss/cargo-verdict: Cargo.toml only)")
/// @yah:verify("crates.io API: cargo-verdict max_version 0.1.0; cargo publish --dry-run --allow-dirty after the edit: EXIT 0")
/// @yah:verify("GitHub events API returned no events for the repo (Activity pusher not machine-confirmable here); commits API shows author login yah-human")
/// @yah:cleanup("\"After the operator's commit + re-export: export-oss.sh --dry-run cargo-verdict should print 'mirror already up to date (snapshot tree matches)'; then strike this.\"")
pub struct CargoBeholderFactory;
impl BeholderFactory for CargoBeholderFactory {
fn name(&self) -> &'static str { "cargo" }
fn version(&self) -> &'static str { CARGO_BEHOLDER_VERSION }
fn matches(&self, argv: &[String]) -> bool {
cargo_request(argv).is_some_and(|r| {
r.class() == cargo_verdict::Class::Verdict
&& !matches!(r.verb, cargo_verdict::Verb::Nextest(_))
// Decline when the user explicitly chose a message format.
&& r.message_format.is_empty()
})
}
fn mode(&self) -> BeholderMode {
BeholderMode::Planned
}
fn plan_argv(&self, argv: &mut Vec<String>, ctx: &RunContext<'_>) {
let Some(request) = cargo_request(argv) else { return };
let plan = cargo_verdict::plan(&request, &cargo_plan_options(&request, ctx));
// The engine's additions, as shell words, before cargo's own `--`.
let at = 1 + cargo_verdict::plan::cargo_args_end(&argv[1..]);
let words: Vec<String> = plan.added_argv(&request).iter().map(|w| shell_word(w)).collect();
argv.splice(at..at, words);
}
fn create(&self) -> Box<dyn Beholder> {
Box::new(CargoBeholder::default())
}
/// R989-F24/F37, W371 §4.5 last paragraph: the Bash door's one flag is
/// `--message-format=json-render-diagnostics` (added by the cargo gate to
/// the argv it spawns) and nothing else. Under it
/// cargo still renders every diagnostic as human text on stderr, and adds
/// only stdout lines beginning `{"reason":` — R989-S2 found the stripped
/// output identical to the unrewritten run's on every case (W371 §4.4,
/// 2026-10-17). No libtest runner: that replaces libtest's human output,
/// which no line filter can restore (F23's trade-off (c)), so structured
/// tests go through the typed door.
///
/// The cost, recorded rather than hidden: cargo emits no compiler-message
/// JSON under this format, so the run gets artifact and build-finished
/// structure but no structured diagnostics. The beholder says so on the
/// run (see [`CargoBeholder::for_reason_stripping`]).
fn attach_for_stripping(&self, argv: &[String]) -> Option<Box<dyn Beholder>> {
let request = cargo_request(argv)?;
// Only cargo's own args: a `--message-format=…` after `--` is the test
// binary's.
let own = &argv[1..1 + cargo_verdict::plan::cargo_args_end(&argv[1..])];
(request.class() == cargo_verdict::Class::Verdict
&& !matches!(request.verb, cargo_verdict::Verb::Nextest(_))
&& own.iter().any(|a| a == BASH_DOOR_MESSAGE_FORMAT))
.then(|| Box::new(CargoBeholder::for_reason_stripping()) as Box<dyn Beholder>)
}
fn tool_version_range(&self) -> Option<ToolVersionRange> {
// `--message-format json` dates from cargo 1.31; the runner rides on
// `--config KEY=VALUE`, stable since cargo 1.63.0. No upper bound —
// cargo's JSON has been additive; libtest's sits behind the engine's
// versioned adapter.
Some(ToolVersionRange { min: Some("1.63.0"), max: None })
}
}
/// The one flag the Bash door adds (R989-F24). Public so the attach client can
/// recognise from a run's `rewrite_added` that it must strip.
pub const BASH_DOOR_MESSAGE_FORMAT: &str = "--message-format=json-render-diagnostics";
/// The cargo beholder's own version: 2 is the engine-backed one (R989-F23),
/// which emits test events and real diagnostics; 1.38 parsed compiler JSON it
/// never received under its own `json-render-diagnostics` rewrite.
const CARGO_BEHOLDER_VERSION: &str = "2";
/// `argv` is `cargo …`: the engine's reading of it.
fn cargo_request(argv: &[String]) -> Option<cargo_verdict::Request> {
(argv.first().map(String::as_str) == Some("cargo")).then(|| cargo_verdict::parse(&argv[1..]))
}
/// What the engine needs to know about the machine and the run: the camp's
/// target runner as cargo would resolve it from the run's cwd and env (which
/// the scoped runner then composes with, never replaces), and the workspace's
/// `harness = false` targets. Only a test run pays for the lookup.
fn cargo_plan_options(request: &cargo_verdict::Request, ctx: &RunContext<'_>) -> cargo_verdict::PlanOptions {
let cwd = ctx.cwd.map(std::path::Path::to_path_buf).or_else(|| std::env::current_dir().ok()).unwrap_or_default();
// The run's env is set on top of the inherited one, as cargo will see it.
let mut env: std::collections::BTreeMap<String, String> = std::env::vars().collect();
env.extend(ctx.env.iter().cloned());
cargo_verdict::runner::discover(request, &cwd, &env)
}
/// Quote `w` for `sh -c` if it needs it.
fn shell_word(w: &str) -> String {
let plain = !w.is_empty()
&& w.chars().all(|c| c.is_ascii_alphanumeric() || "-_./=:,+@%".contains(c));
if plain {
w.to_string()
} else {
format!("'{}'", w.replace('\'', "'\\''"))
}
}
/// After this many JSON-shaped lines that are neither a cargo message nor a
/// libtest event, without a single recognizable one, the beholder declares
/// `unknown_format` and stops emitting events.
const FORMAT_PROBE_LIMIT: u8 = 5;
/// Bytes of a failed test's captured output kept on its event (the tail:
/// libtest prints the panic last). The full output is in the run's chunks.
const TEST_OUTPUT_LIMIT: usize = 4096;
/// Per-run cargo parser, over the engine's demux.
///
/// Buffers incoming bytes into lines and hands each to a streaming
/// [`cargo_verdict::Demux`]: cargo messages (`reason` key, parsed with
/// cargo_metadata), libtest events (`type` key, the engine's versioned
/// adapter), raw otherwise. New messages become `cargo::rustc` / `cargo`
/// events and new test results `cargo::test` events as they arrive. A PTY run
/// (all chunks on stdout) is fed as one combined stream, so cargo's `Running`
/// lines attribute results to their binary; a pipe run's stdout pump has no
/// `Running` lines and binaries are told apart by libtest's `suite started`.
///
/// If the first [`FORMAT_PROBE_LIMIT`] JSON-shaped lines are all unrecognized,
/// `unknown_format_reason` returns a non-`None` value and `parse_chunk` stops
/// emitting events.
///
/// @yah:ticket(R989-F20, "Engine v1, part 2: execution plan, stdout demux, cargo_metadata message parsing, and a versioned libtest-JSON adapter with an honest structured:false path")
/// @yah:at(2026-10-16T22:08:56Z)
/// @yah:assignee(agent:bundle-anthropic-glimmerstone)
/// @yah:phase(P4)
/// @yah:parent(R989)
/// @yah:next("DECIDED (W371 §8.5, operator, F4455, 2026-10-16): cargo runs the tests, with a scoped target.runner and libtest JSON. nextest is not the default runner.")
/// @yah:next("Tier: Wizard. Unstable upstream formats, plus a must-not list where one slip silently costs the whole camp a rebuild.")
/// @yah:next("W371 §4.5 modules 2-3. PLAN: every verdict gets `--message-format=json-render-diagnostics`; `test` adds the scoped runner from §4.4(2) (`--config target.<triple>.runner=[\"env\",\"RUSTC_BOOTSTRAP=1\"]` and `-- -Z unstable-options --format json --report-time`), composed with any existing target.runner the way R989-S3 found works. The plan's argv is returned. DEMUX: classify each stdout line as a cargo message (`reason` key, parsed with cargo_metadata::Message, which is already in the lockfile at 0.19.2), a libtest event (`type` key, through a VERSIONED adapter, because the format will move: RFC 3558), or raw (kept in order). A binary that emits no libtest JSON within its first lines is marked `structured: false` and captured raw (beholders.rs FORMAT_PROBE_LIMIT pattern). Regex over libtest's human text is the last resort, never the primary path.")
/// @yah:next("MUST-NOT, each held by a test: never put env on cargo that a build script tracks (RUSTC_BOOTSTRAP reruns proc-macro2's build script, §4.4(3)); never change what a request builds (the plan adds output and runner flags only, and says so in argv); never invent an exit code; never drop bytes.")
/// @yah:verify("Fixture crates (warning, failing build, passing/failing/ignored tests, harness=false, a doctest) each produce the expected demuxed events; a test asserts RUSTC_BOOTSTRAP is absent from cargo's own env in every plan, and that a plain build after a planned test run is Fresh")
/// @yah:depends_on(R989-F19)
/// @yah:depends_on(R989-S3)
/// @yah:files(oss/cargo-verdict)
/// @yah:status(review)
/// @yah:handoff("LANDED in oss/cargo-verdict: src/plan.rs (plan(&Request,&PlanOptions)->Plan{request,argv,added,cargo_json,diagnostics_json,libtest_json,unstructured}; existing_runner(cwd); host_triple(); RUNNER_SCRIPT), src/exec.rs (exec::run(&Plan, cwd, on_line)->Exit{Code(i32)|Signalled(Option<i32>)}), src/demux.rs (Demux::push(Stream,line); classify(); BinaryRun{kind,source,executable,structured,events,raw}; artifact_of(); FORMAT_PROBE_LIMIT=5), src/libtest.rs (TestEvent; trait LibtestAdapter; LibtestJsonV1 id libtest-json/1; panic_location()). cargo_metadata 0.19 (0.19.2, already in yah's lockfile) parses `reason` lines; an unknown reason stays raw.")
/// @yah:handoff("DEVIATION 1, MEASURED (cargo/rustc 1.97.0, 2026-10-17, scratch crate): `--message-format=json-render-diagnostics` emits NO compiler-message lines - cargo renders diagnostics to stderr instead. `cargo check` with one warning gave 1 compiler-artifact + 1 build-finished and 0 compiler-message under json-render-diagnostics; under `json` the same warning arrived as a compiler-message with `rendered`. So the plan adds `--message-format json`. THIS MATTERS BEYOND F20: the CargoBeholderFactory rewrite above appends json-render-diagnostics and so never sees a diagnostic as JSON; F23 and F24 (Bash-door demux) should be read against this.")
/// @yah:handoff("DEVIATION 2, MEASURED: `cargo test -- -Z unstable-options --format json` with the env runner from W371 §4.4(2) makes rustdoc's doctest harness fail with `the option Z is only accepted on the nightly compiler` (doctests are not run through target.runner), i.e. an invented failure on any crate with a doctest. So the libtest flags move INTO the runner: target.<triple>.runner=[\"sh\",\"-c\",\"exec env RUSTC_BOOTSTRAP=1 \\\"$0\\\" \\\"$@\\\" -Z unstable-options --format json --report-time\"]. Measured on the same crate: lib + integration binaries emit libtest JSON, doctests run untouched (reported structured:false; counts read from libtest's `test result:` line, the declared last resort), harness=false runs (and ignores the args), filters still reach binaries, and `cargo test --no-run` / `cargo build` afterwards were all Fresh. Needs a POSIX sh; without one the plan declines libtest (honest note). Still cargo running the tests with a scoped runner, as §8 call 5 decided.")
/// @yah:handoff("R989-S3 HAD NOT LANDED when this was finished (no 2026-10-17 entry in W371 §4.4). Conservative path, per the coordinator: when any target runner is already configured (CARGO_TARGET_*_RUNNER env, a `runner` key in any .cargo/config(.toml) cargo would read from cwd or CARGO_HOME, or a --config target.*.runner in the request) the plan adds NO runner and reports tests structured:false with the reason naming S3. Revisit plan::libtest_refusal once S3 says how runners compose. Doctest and harness=false behaviour above is my own measurement, not S3's; S3 should confirm on the yah workspace (its case 4: unit hashes).")
/// @yah:handoff("DEVIATION 3: exec::run gives cargo ONE pipe for stdout+stderr (std::io::pipe) and reports lines as Stream::Combined. Two pipes on two threads raced under load: a golden run reported the unittests binary unstructured because its libtest JSON was read before cargo's `Running` line. One pipe preserves write order exactly; cargo writes `Running` before spawning. Demux still accepts separate Stdout/Stderr for callers like task-runs that hold separate chunks, and documents the race for them.")
/// @yah:handoff("MUST-NOT list, each held by a test in oss/cargo-verdict/tests/engine.rs: the_plan_never_puts_rustc_bootstrap_on_cargo (env untouched; RUSTC_BOOTSTRAP only inside the runner config value), the_plan_never_changes_what_a_request_builds (removing the plan's message_format/config additions gives back the request exactly; argv == render(planned); every addition listed in `added`), never invent an exit code (Exit is cargo's status; Signalled has no code), never drop bytes (Demux.lines keeps every line; unit test a_binary_that_never_speaks_json_is_unstructured_and_kept), and the mixed fixture's run leaves `cargo test --no-run` and `cargo build` 100% Fresh.")
/// @yah:handoff("TESTS: cargo test --manifest-path oss/cargo-verdict/Cargo.toml - lib 9 passed, corpus 23, engine 7, goldens 4, roundtrip 2, all 0 failed; three consecutive full runs green. clippy --all-targets clean. Fixtures: tests/fixtures/mixed (warning, pass/fail/ignored unit tests, doctest, integration test, harness=false) and tests/fixtures/broken (E0308).")
/// @yah:handoff("2026-10-18, R989-S3 APPLIED (supersedes the 'R989-S3 HAD NOT LANDED' conservative path above). An existing target runner is now COMPOSED, not refused. New oss/cargo-verdict/src/runner.rs: CargoConfig::load(cwd, env) reads cargo's layers as measured (W371 §4.4 2026-10-18 entry: --config > CARGO_TARGET_<T>_RUNNER > config files deepest-first, no array concatenation, a triple key beats every cfg(...) key, cfg(...) evaluated against `rustc --print cfg --target T`, relative `./x` resolved against the parent of .cargo for files and cwd for env); runner::discover(request, cwd, full_env) -> PlanOptions{host_triple, default_targets (CARGO_BUILD_TARGET/build.target), runners: triple -> CampRunner{Compose{argv,source}|Unknown(why)}, no_harness: Result<stems>, posix_sh}. plan::runner_script(camp, no_harness) keeps $0 = the binary and puts the camp argv after env: `case \"${0##*/}\" in rust_out|<stem>-????????????????) exec <camp> \"$0\" \"$@\";; esac; exec env RUSTC_BOOTSTRAP=1 <camp> \"$0\" \"$@\" -Z unstable-options --format json --report-time`. harness=false targets (read from member manifests; cargo metadata does not expose harness) and rustdoc's rust_out doctest binaries get their argv untouched, measured necessary: a harness that rejects unknown args exits 2 under the flags. Undecidable (unreadable config, include, unparseable cfg, cfg keys plus configured rustflags, two matching cfg tables, string/array mix, a request --config that sets a runner, metadata failure) => NO runner added, tests structured:false with a note, so the camp's runner runs as configured. API break, pre-1.0, no shim: plan::existing_runner and RUNNER_SCRIPT are gone (LIBTEST_FLAGS + runner_script replace the const); cargo_plan_options(request, ctx) here now calls discover with the inherited env plus ctx.env. New dep: toml 0.8 (already in qed's lockfile). Fixture tests/fixtures/runner (edition 2024, merged doctests, strict harness=false).")
/// @yah:verify("cargo test --manifest-path oss/cargo-verdict/Cargo.toml --test engine (expect 12 passed, including a_camp_runner_in_a_{config_file,cfg_table,the_environment}_is_composed_not_replaced)")
/// @yah:verify("cargo test --manifest-path oss/cargo-verdict/Cargo.toml --lib (expect 13 passed: libtest adapter, demux, verdict units, cfg parser, path resolution)")
/// @yah:verify("cargo test --manifest-path oss/qed/Cargo.toml -p task-runs --lib beholders (expect 180 passed, including an_existing_runner_is_composed_into_the_scoped_runner)")
pub struct CargoBeholder {
buf: Vec<u8>,
demux: cargo_verdict::Demux,
/// `demux.messages` already turned into events.
messages_seen: usize,
/// Per binary, `events` already turned into events.
test_events_seen: Vec<usize>,
/// A lib's warning is reported for the lib and again for its test
/// harness; cargo shows it once, and so do the events.
diagnostics_seen: std::collections::HashSet<String>,
/// JSON-object lines seen that are neither cargo nor libtest.
/// Only counted before a recognizable line has been seen.
json_lines_unrecognized: u8,
/// Set once a cargo message or libtest event is seen. After this point
/// unrecognized lines are silently dropped (forward compat).
recognized_line_seen: bool,
/// Set when `json_lines_unrecognized >= FORMAT_PROBE_LIMIT` and no
/// recognizable line has been seen yet.
unknown_format: Option<String>,
/// R989-F24 — attached to a Bash-door run, under
/// `json-render-diagnostics`: cargo sends no compiler-message JSON, so the
/// build-finished event says diagnostics are unstructured here.
reason_stripping: bool,
/// Compilation units cargo reported, fresh and rebuilt, for the
/// build-finished event.
units_fresh: u32,
units_compiled: u32,
}
impl Default for CargoBeholder {
fn default() -> Self {
Self {
buf: Vec::new(),
demux: cargo_verdict::Demux::streaming(),
messages_seen: 0,
test_events_seen: Vec::new(),
diagnostics_seen: std::collections::HashSet::new(),
json_lines_unrecognized: 0,
recognized_line_seen: false,
unknown_format: None,
reason_stripping: false,
units_fresh: 0,
units_compiled: 0,
}
}
}
impl CargoBeholder {
/// The instance for a Bash-door run (R989-F24).
fn for_reason_stripping() -> Self {
Self { reason_stripping: true, ..Self::default() }
}
/// Feed one complete line; return the events it produced.
fn line(&mut self, stream: Stream, text: &str, anchor: &Anchor) -> Vec<Event> {
// A PTY line can carry a progress bar erased by `\r ESC[K` before the
// JSON; probe what a terminal would show.
let visible = cargo_verdict::demux::visible(text);
if visible.trim_start().starts_with('{') {
match cargo_verdict::demux::classify(&visible) {
cargo_verdict::demux::StdoutLine::Raw => {
if !self.recognized_line_seen {
self.json_lines_unrecognized = self.json_lines_unrecognized.saturating_add(1);
if self.json_lines_unrecognized >= FORMAT_PROBE_LIMIT {
self.unknown_format = Some(format!(
"no recognizable cargo or libtest JSON lines in first {FORMAT_PROBE_LIMIT} JSON-object lines"
));
return Vec::new();
}
}
}
_ => self.recognized_line_seen = true,
}
}
let demux_stream = match stream {
Stream::Stderr => cargo_verdict::Stream::Stderr,
// A PTY merges both streams into stdout chunks; read them as one.
_ => cargo_verdict::Stream::Combined,
};
self.demux.push(demux_stream, text);
self.drain(anchor)
}
/// Turn whatever the demux learned since last time into events.
fn drain(&mut self, anchor: &Anchor) -> Vec<Event> {
let mut events = Vec::new();
for m in &self.demux.messages[self.messages_seen..] {
if let cargo_verdict::cargo_metadata::Message::CompilerArtifact(a) = m {
if a.fresh {
self.units_fresh += 1;
} else {
self.units_compiled += 1;
}
}
let ctx = MessageContext {
reason_stripping: self.reason_stripping,
units_fresh: self.units_fresh,
units_compiled: self.units_compiled,
};
if let Some(e) = cargo_message_event(m, anchor, &mut self.diagnostics_seen, &ctx) {
events.push(e);
}
}
self.messages_seen = self.demux.messages.len();
self.test_events_seen.resize(self.demux.binaries.len(), 0);
for (i, b) in self.demux.binaries.iter().enumerate() {
for e in &b.events[self.test_events_seen[i]..] {
if let Some(ev) = cargo_test_event(e, b, anchor) {
events.push(ev);
}
}
self.test_events_seen[i] = b.events.len();
}
events
}
}
/// @yah:ticket(R989-F23, "task-runs' CargoBeholder parses through the engine; it keeps the ingestion seam and stops owning what cargo output means")
/// @yah:at(2026-10-16T22:10:08Z)
/// @yah:assignee(agent:bundle-anthropic-glimmerstone)
/// @yah:phase(P4)
/// @yah:parent(R989)
/// @yah:next("Tier: Warrior. A consumer swap inside a framework that stays as it is.")
/// @yah:next("W371 §1 row 19. CargoBeholderFactory / CargoBeholder keep the ingestion seam: argv rewrite to json-render-diagnostics, FORMAT_PROBE_LIMIT, and run events into the store. They delegate line parsing to the engine's demux (R989-F20) and gain TEST events, which they lack today: the cargo beholder parses compiler JSON only. QED keeps the multi-tool beholder framework (tsc, eslint, biome, vitest, jest, bun, pytest are untouched).")
/// @yah:next("Dependency: add the engine to oss/qed/Cargo.toml's own [patch.crates-io] by path (that block already path-patches sibling oss crates such as ../yah-base/crates/workload-spec), so oss/qed still builds as a standalone workspace.")
/// @yah:next("This does NOT change the relocated agent rail. That rail still declines the beholder (build_run.rs R739-B10, ~:404) until the Bash-door ticket lands.")
/// @yah:verify("cargo test -p task-runs inside oss/qed (standalone workspace); a QED step running cargo test on a fixture records per-test pass/fail events in the run store")
/// @yah:depends_on(R989-F21)
/// @yah:files(oss/qed/crates/task-runs/src/beholders.rs)
/// @yah:files(oss/qed/crates/task-runs/Cargo.toml)
/// @yah:files(oss/qed/Cargo.toml)
/// @yah:status(review)
/// @yah:handoff("LANDED. CargoBeholderFactory/CargoBeholder (beholder version 2) parse through cargo-verdict: matches() is cargo_verdict::parse(..).class()==Verdict (not nextest, no user --message-format); the rewrite is cargo_verdict::plan() spliced into the line as written (Plan::added_argv + plan::cargo_args_end, new pub API), shell-quoted because the driver runs a rewritten line with sh -c; parse_chunk feeds a streaming cargo_verdict::Demux and turns new messages into cargo::rustc / cargo events (same field shapes as before: error.code, file.{path,line,col}; plus rendered) and new libtest results into cargo::test events (one per finished test: test.{name,outcome,binary,exec_time_ms}, failed ones with output tail <=4096B + output_truncated_bytes + file from the panic location; one per binary from SuiteFinished); on_done adds an Info event per binary that never spoke libtest JSON (doctests, harness=false), so absence reads as unobserved, not not-run. FORMAT_PROBE_LIMIT kept: 5 JSON-object lines that are neither cargo nor libtest before any recognized one -> unknown_format. A lib warning reported for lib and lib-test is one event. The old hand-rolled CargoLine/CargoMessage parser and CARGO_DIAG_SUBCOMMANDS are deleted.")
/// @yah:handoff("THE ARGV DECISION: the beholder now rewrites to `--message-format json`, NOT json-render-diagnostics. Reason: under json-render-diagnostics cargo emits no compiler-message lines (measured, F20), so the 1.38 beholder never produced a single cargo::rustc event in practice - and `task-events:cargo::rustc error.code=E0308` gnome rules (rule_filter.rs) plus scryer's `WARN cargo::rustc` counts were therefore silently always-clean. Cost: this seam only attaches when verbatim_output is false (nobody renders the bytes), but the stored run log of such a run now carries diagnostics as JSON lines (with `rendered`) instead of human text on stderr, and a `cargo test` run's libtest output as JSON lines. That is the existing Rewriter contract, applied honestly.")
/// @yah:handoff("FOR R989-F24 (Bash door, strip-parity) - THE TRADE-OFF, NOT DECIDED HERE, the relocated rail is unchanged (it still declines via verbatim_output): (a) json-render-diagnostics + strip `{\"reason\":` lines gives human-identical bytes (W371 §4.4(1)) but yields only artifacts + build-finished: NO diagnostics; (b) `json` yields diagnostics but removes them from stderr, so parity needs re-rendering each compiler-message's `rendered` text, which lands on stdout's position rather than stderr's - byte-identical per stream only if the rail re-emits it to stderr in order, unproven; (c) test structure needs the scoped libtest runner, which REPLACES libtest's human output with JSON on the binary's stdout - incompatible with strip-parity unless the rail re-renders libtest events as human lines (a renderer, which R739-B10 wanted to avoid). Recommendation for F24: (a) for builds if P0(b) holds, and no test runner on the Bash door; take structured tests through the typed door.")
/// @yah:handoff("FRAMEWORK CHANGE (task-runs only, pre-1.0, no shim): BeholderRegistry::attach gained a 4th arg `&RunContext { cwd, env }` and BeholderMode a `Planned` variant whose rewrite goes through the new BeholderFactory::plan_argv(argv, ctx) (default no-op), so the cargo plan can see the run's cwd and env. Needed because the plan must not replace a target runner the run's own cargo config sets: it checks CARGO_TARGET_*_RUNNER in the run env, then cargo_verdict::plan::existing_runner(run cwd) (process cwd if none) - R989-S3 still open, so any runner => no scoped runner, tests unstructured. Only caller is driver.rs spawn_run (updated); every attach call in tests now passes &RunContext::default(). BeholderMode::rewrites() replaces the two matches! lists.")
/// @yah:handoff("ENGINE CHANGES (oss/cargo-verdict, mine): Demux::streaming() (keeps structure, not lines - the run store already has the bytes); demux::visible() - under a PTY cargo draws its progress bar, then `\\r ESC[K`, then writes the JSON message on the SAME line, and colours `Running`; classification now reads the terminal-visible text (raw bytes still kept). Without that, the first live run here recorded test events but no diagnostics and no binary names. Without `Running` lines (pipe-mode stdout pump) binaries are split at each libtest `suite started`. Plan::added_argv / apply_to, plan::cargo_args_end, and a host-triple-unknown refusal.")
/// @yah:handoff("WIRING: task-runs depends on `cargo-verdict = \"0.1.0\"` (version, not path: oss/qed exports standalone and a ../cargo-verdict path would dangle in the mirror), bridged by `cargo-verdict = { path = \"../cargo-verdict\" }` in oss/qed/Cargo.toml [patch.crates-io] and `{ path = \"oss/cargo-verdict\" }` in the root yah Cargo.toml [patch.crates-io] (root needs it because task-runs is in the root graph). RELEASE ORDER for T25: cargo-verdict must be on crates.io before the next task-runs/qed publish, or that publish fails resolution.")
/// @yah:handoff("TESTS: cargo test -p task-runs (oss/qed): lib 273 passed, 1 failed - driver::tests::pipefail_reports_the_failing_stage_and_posix_reports_the_last_one, which is environmental and unrelated: this container's /bin/sh is dash without `set -o pipefail` (`sh -c 'set -o pipefail'` -> Illegal option), and the line under test is a compound command the beholder declines. New: a_cargo_test_run_records_per_test_events_in_the_store (hermetic crate in a tempdir, real cargo through spawn_run: t::passes ok, t::fails failed, the unused-variable warning as cargo::rustc, status attached:cargo@2 with the runner), cargo_beholder_emits_test_events_per_test_and_per_binary, a_test_run_gets_the_scoped_runner_quoted_for_the_shell_and_before_the_double_dash (runs the rewritten line through sh and checks it parses to the plan), an_existing_runner_keeps_the_plan_off_the_test_runner, cargo_factory_matches_verdicts_only, a_lib_warning_reported_twice_is_one_event. cargo-verdict: all suites green (lib 11). clippy -p task-runs: no warnings in the changed code (the 9 it prints are pre-existing, elsewhere).")
/// @yah:handoff("OUTSIDE MY FILES, one line: crates/yah/camp-service/tests/e2e.rs verbatim_output_keeps_the_beholder_from_rewriting_the_command's control assertion now checks `--message-format json` instead of `--message-format=json-render-diagnostics`. MATCHES NARROWED: run/fix/publish no longer attach (tenancy/mutation); they only ever got artifacts + build-finished from the old rewrite.")
/// @yah:verify("cd oss/qed && cargo test -p task-runs --lib beholders (all pass) && cargo test -p task-runs --lib a_cargo_test_run_records_per_test_events_in_the_store")
/// @yah:verify("cargo check -p yah from the root, and cargo test -p camp-service --test e2e verbatim_output_keeps_the_beholder")
impl Beholder for CargoBeholder {
fn name(&self) -> &'static str { "cargo" }
fn version(&self) -> &'static str { CARGO_BEHOLDER_VERSION }
fn mode(&self) -> BeholderMode {
BeholderMode::Planned
}
fn parse_chunk(&mut self, chunk: &OutputChunk) -> Vec<Event> {
if self.unknown_format.is_some() || chunk.stream == Stream::Synth {
return Vec::new();
}
let anchor = Anchor {
run_id: chunk.run_id.clone(),
offset_ms: chunk.offset_ms,
chunk: Some(ChunkRef { seq: chunk.seq }),
};
self.buf.extend_from_slice(&chunk.bytes);
let mut events = Vec::new();
while let Some(nl) = self.buf.iter().position(|&b| b == b'\n') {
let line: Vec<u8> = self.buf.drain(..=nl).collect();
// Strip trailing \n (and optional \r for Windows cargo output).
let line = String::from_utf8_lossy(line.trim_ascii_end()).into_owned();
events.extend(self.line(chunk.stream, &line, &anchor));
if self.unknown_format.is_some() {
return Vec::new();
}
}
events
}
fn on_done(&mut self, run_id: &crate::types::TaskRunId, offset_ms: u32) -> Vec<Event> {
if self.unknown_format.is_some() {
return Vec::new();
}
let anchor = Anchor { run_id: run_id.clone(), offset_ms, chunk: None };
let mut events = Vec::new();
if !self.buf.is_empty() {
let rest = std::mem::take(&mut self.buf);
let line = String::from_utf8_lossy(rest.trim_ascii_end()).into_owned();
events.extend(self.line(Stream::Stdout, &line, &anchor));
}
// Honest about what had no structure: a binary that never spoke
// libtest JSON (harness = false, doctests, a planless run) says so,
// so a missing test event reads as "not observed", not "not run".
for b in self.demux.binaries.iter().filter(|b| !b.structured) {
events.push(Event {
run_id: run_id.clone(),
seq: 0,
offset_ms,
level: Level::Info,
target: "cargo::test".to_owned(),
msg: format!(
"test binary {} produced no libtest JSON; its results are in the run's output only",
binary_label(b)
),
fields: serde_json::json!({ "test": { "binary": binary_fields(b), "structured": false } }),
anchor: None,
source: cargo_source(),
});
}
events
}
fn unknown_format_reason(&self) -> Option<&str> {
self.unknown_format.as_deref()
}
}
/// Construct a [`BeholderRegistry`] pre-loaded with all bundled beholders.
pub fn default_registry() -> BeholderRegistry {
let mut r = BeholderRegistry::new();
r.register(Box::new(CargoBeholderFactory));
r.register(Box::new(TscBeholderFactory));
r.register(Box::new(EslintBeholderFactory));
r.register(Box::new(BiomeBeholderFactory));
r.register(Box::new(VitestBeholderFactory));
r.register(Box::new(JestBeholderFactory));
r.register(Box::new(BunTestBeholderFactory));
r.register(Box::new(PytestBeholderFactory));
r.register(Box::new(ViteBuildBeholderFactory));
r
}
/// Construct a [`BeholderRegistry`] with bundled beholders plus any user-defined
/// beholders loaded from `user_dir` (typically `~/.yah/beholders/`).
///
/// User beholders are appended after bundled ones, so bundled beholders have
/// higher priority when both would match the same command. Malformed or
/// unreadable TOML files in `user_dir` are skipped silently.
pub fn registry_with_user_beholders(user_dir: Option<&std::path::Path>) -> BeholderRegistry {
let mut r = default_registry();
if let Some(dir) = user_dir {
for factory in crate::user_beholders::load_user_beholders(dir) {
r.register(factory);
}
}
r
}
// ─── tsc beholder ─────────────────────────────────────────────────────────────
/// Factory for the bundled tsc beholder (Tier 1.5, Rewriter + Parser).
///
/// Matches `tsc` invocations (bare or via wrapper: `bunx tsc`, `pnpm tsc`,
/// `npx tsc`) and rewrites argv to add `--pretty=false` when not already set,
/// enabling the machine-parseable diagnostic format:
///
/// ```text
/// src/foo.ts(10,5): error TS2345: Argument of type 'string' is not assignable…
/// Found 1 error.
/// ```
///
/// Declines when:
/// - `--pretty` or `--pretty=true` is already set (user wants colored output).
/// - `--version` / `-v` / `--init` are present (non-diagnostic invocations).
/// - `--pretty=false` is already present (rewriter is a no-op; beholder still
/// attaches in Parser mode so diagnostics are captured — argv unchanged, no
/// `rewrite_added` entry on status).
pub struct TscBeholderFactory;
impl BeholderFactory for TscBeholderFactory {
fn name(&self) -> &'static str { "tsc" }
fn version(&self) -> &'static str { "3.0" }
fn matches(&self, argv: &[String]) -> bool {
if argv.first().map(String::as_str) != Some("tsc") {
return false;
}
// Decline for non-diagnostic invocations.
if argv.iter().any(|a| matches!(a.as_str(), "--version" | "-v" | "--init")) {
return false;
}
// Decline if the user explicitly requested colored (pretty) output.
// `--pretty` alone means true; `--pretty=true` is explicit. We do NOT
// decline for `--pretty=false` — the rewriter becomes a no-op but we
// still want to parse the output.
!argv.iter().any(|a| a == "--pretty" || a == "--pretty=true")
}
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter {
adjust_argv: |argv| {
// Only inject if the user hasn't already set --pretty=false.
if !argv.iter().any(|a| a == "--pretty=false") {
argv.push("--pretty=false".to_owned());
}
},
}
}
fn create(&self) -> Box<dyn Beholder> {
Box::new(TscBeholder::default())
}
fn tool_version_range(&self) -> Option<ToolVersionRange> {
// --pretty=false has been available since tsc 1.x; 3.0.0 is a
// conservative lower bound for the stable output format.
Some(ToolVersionRange { min: Some("3.0.0"), max: None })
}
}
/// Per-run tsc parser.
///
/// Buffers incoming bytes and extracts complete lines. Diagnostic lines in the
/// `--pretty=false` format are turned into structured events; watch-mode
/// timestamp headers and blank lines are silently skipped. Summary lines
/// ("Found N errors.") produce an Info event so the agent can see the final
/// outcome without polling run status.
pub struct TscBeholder {
buf: Vec<u8>,
}
impl Default for TscBeholder {
fn default() -> Self {
Self { buf: Vec::new() }
}
}
impl Beholder for TscBeholder {
fn name(&self) -> &'static str { "tsc" }
fn version(&self) -> &'static str { "3.0" }
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter {
adjust_argv: |argv| {
if !argv.iter().any(|a| a == "--pretty=false") {
argv.push("--pretty=false".to_owned());
}
},
}
}
fn parse_chunk(&mut self, chunk: &OutputChunk) -> Vec<Event> {
self.buf.extend_from_slice(&chunk.bytes);
let mut events = Vec::new();
while let Some(nl) = self.buf.iter().position(|&b| b == b'\n') {
let raw: Vec<u8> = self.buf.drain(..=nl).collect();
let Ok(line) = std::str::from_utf8(raw.trim_ascii_end()) else { continue };
if line.is_empty() { continue }
// Skip watch-mode timestamp headers: "[12:00:00 AM] …"
if line.starts_with('[') { continue }
let source = EventSource::Beholder {
name: "tsc".to_owned(),
version: "3.0".to_owned(),
};
if let Some(diag) = parse_tsc_diagnostic(line) {
let mut fields = serde_json::json!({});
fields["error"] = serde_json::json!({ "code": diag.code });
fields["file"] = serde_json::json!({
"path": diag.file,
"line": diag.line,
"col": diag.col,
});
events.push(Event {
run_id: chunk.run_id.clone(),
seq: 0,
offset_ms: chunk.offset_ms,
level: diag.level,
target: "tsc".to_owned(),
msg: diag.msg,
fields,
anchor: Some(ChunkRef { seq: chunk.seq }),
source,
});
} else if let Some(summary) = parse_tsc_summary(line) {
events.push(Event {
run_id: chunk.run_id.clone(),
seq: 0,
offset_ms: chunk.offset_ms,
level: if summary.errors == 0 { Level::Info } else { Level::Error },
target: "tsc".to_owned(),
msg: line.to_owned(),
fields: serde_json::json!({ "build": { "errors": summary.errors } }),
anchor: Some(ChunkRef { seq: chunk.seq }),
source,
});
}
}
events
}
}
// ─── tsc parsing helpers ──────────────────────────────────────────────────────
struct TscDiagnostic {
file: String,
line: u32,
col: u32,
level: Level,
code: String,
msg: String,
}
struct TscSummary {
errors: u32,
}
/// Parse a single `--pretty=false` diagnostic line of the form:
/// `<file>(<line>,<col>): <level> TS<code>: <message>`
fn parse_tsc_diagnostic(line: &str) -> Option<TscDiagnostic> {
const MARKERS: &[(&str, Level)] = &[
("): error TS", Level::Error),
("): warning TS", Level::Warn),
("): message TS", Level::Info),
];
for (marker, level) in MARKERS {
let Some(marker_pos) = line.find(marker) else { continue };
// Everything before ")" is "file(line,col".
let file_pos_str = &line[..marker_pos];
let Some((file, ln, col)) = parse_file_pos(file_pos_str) else { continue };
// After "): error TS" (etc.) starts the numeric code.
let after = &line[marker_pos + marker.len()..];
let code_len = after.chars().take_while(|c| c.is_ascii_digit()).count();
if code_len == 0 { continue; }
let code = format!("TS{}", &after[..code_len]);
let rest = &after[code_len..];
if !rest.starts_with(": ") { continue; }
let msg = rest[2..].to_owned();
return Some(TscDiagnostic { file, line: ln, col, level: *level, code, msg });
}
None
}
/// Extract `(file, line, col)` from the `file(line,col` part of a tsc diagnostic.
///
/// `s` is the substring before the closing `)`, e.g. `"src/foo.ts(10,5"`.
fn parse_file_pos(s: &str) -> Option<(String, u32, u32)> {
let comma = s.rfind(',')?;
let col: u32 = s[comma + 1..].parse().ok()?;
let before_comma = &s[..comma];
let open_paren = before_comma.rfind('(')?;
let ln: u32 = before_comma[open_paren + 1..].parse().ok()?;
let file = s[..open_paren].to_owned();
if file.is_empty() { return None; }
Some((file, ln, col))
}
/// Parse `"Found N errors."` / `"Found N errors in M files."` summary lines.
fn parse_tsc_summary(line: &str) -> Option<TscSummary> {
let rest = line.strip_prefix("Found ")?;
// Extract the leading digit sequence (error count).
let count_len = rest.chars().take_while(|c| c.is_ascii_digit()).count();
if count_len == 0 { return None; }
let errors: u32 = rest[..count_len].parse().ok()?;
// Remainder must start with " error" to distinguish from other "Found …" lines.
if !rest[count_len..].starts_with(" error") { return None; }
Some(TscSummary { errors })
}
// ─── Cargo events ─────────────────────────────────────────────────────────────
//
// What the engine parsed, as run-store events. Field shapes are the ones the
// 1.38 beholder emitted (`error.code`, `file.{path,line,col}` on
// `cargo::rustc`), because `task-events:` gnome rules query them
// (rule_filter.rs); test events are new, under `cargo::test`.
/// Where the events of one line anchor.
struct Anchor {
run_id: crate::types::TaskRunId,
offset_ms: u32,
chunk: Option<ChunkRef>,
}
fn cargo_source() -> EventSource {
EventSource::Beholder { name: "cargo".to_owned(), version: CARGO_BEHOLDER_VERSION.to_owned() }
}
fn cargo_event(anchor: &Anchor, level: Level, target: &str, msg: String, fields: serde_json::Value) -> Event {
Event {
run_id: anchor.run_id.clone(),
seq: 0, // assigned by store on insert
offset_ms: anchor.offset_ms,
level,
target: target.to_owned(),
msg,
fields,
anchor: anchor.chunk.clone(),
source: cargo_source(),
}
}
/// A `compiler-message` or `build-finished` as an event; other messages
/// (artifacts, build scripts) produce none.
/// What a build-finished event reports beyond cargo's own message.
struct MessageContext {
reason_stripping: bool,
units_fresh: u32,
units_compiled: u32,
}
/// The Bash door's answer to "where are the diagnostics" (R989-F24): on that
/// door cargo renders them as text and sends no JSON for them, so
/// `task.diagnostics` cannot list them. Said on the event, so an empty
/// diagnostics list reads as "not observed", never as "clean".
const BASH_DOOR_DIAGNOSTICS_NOTE: &str = "compiler diagnostics are not structured on the Bash door \
(--message-format=json-render-diagnostics renders them as text); they are in the run's \
output (task.lines), and the typed cargo door returns them structured";
fn cargo_message_event(
m: &cargo_verdict::cargo_metadata::Message,
anchor: &Anchor,
seen: &mut std::collections::HashSet<String>,
ctx: &MessageContext,
) -> Option<Event> {
use cargo_verdict::cargo_metadata::{diagnostic::DiagnosticLevel, Message};
match m {
Message::CompilerMessage(c) => {
let d = &c.message;
let level = match d.level {
DiagnosticLevel::Error | DiagnosticLevel::Ice | DiagnosticLevel::FailureNote => Level::Error,
DiagnosticLevel::Warning => Level::Warn,
DiagnosticLevel::Note | DiagnosticLevel::Help => Level::Info,
_ => Level::Debug,
};
let primary = d.spans.iter().find(|s| s.is_primary);
let key = format!(
"{:?}|{:?}|{}|{:?}",
level,
d.code.as_ref().map(|c| &c.code),
d.message,
primary.map(|s| (&s.file_name, s.line_start, s.column_start))
);
if !seen.insert(key) {
return None;
}
let mut fields = serde_json::json!({});
if let Some(code) = &d.code {
fields["error"] = serde_json::json!({ "code": code.code });
}
if let Some(span) = primary {
fields["file"] = serde_json::json!({
"path": span.file_name,
"line": span.line_start,
"col": span.column_start,
});
}
if let Some(r) = &d.rendered {
fields["rendered"] = serde_json::json!(r);
}
Some(cargo_event(anchor, level, "cargo::rustc", d.message.clone(), fields))
}
Message::BuildFinished(f) => {
let mut fields = serde_json::json!({ "build": {
"success": f.success,
"units": { "compiled": ctx.units_compiled, "fresh": ctx.units_fresh },
} });
let mut msg = if f.success { "build finished".to_owned() } else { "build failed".to_owned() };
if ctx.reason_stripping {
fields["build"]["diagnostics"] = serde_json::json!("unstructured");
msg = format!("{msg}; {BASH_DOOR_DIAGNOSTICS_NOTE}");
}
Some(cargo_event(anchor, if f.success { Level::Info } else { Level::Error }, "cargo", msg, fields))
}
_ => None,
}
}
fn binary_fields(b: &cargo_verdict::BinaryRun) -> serde_json::Value {
serde_json::json!({ "kind": b.kind, "source": b.source, "executable": b.executable })
}
fn binary_label(b: &cargo_verdict::BinaryRun) -> String {
b.source.clone().unwrap_or_else(|| format!("{:?}", b.kind).to_lowercase())
}
/// A libtest result as an event: one per test that finished, one per binary
/// that finished. `started` events produce none.
fn cargo_test_event(e: &cargo_verdict::TestEvent, b: &cargo_verdict::BinaryRun, anchor: &Anchor) -> Option<Event> {
use cargo_verdict::TestEvent as T;
let ms = |s: &Option<f64>| s.map(|s| (s * 1000.0).round() as u64);
let test = |name: &str, outcome: &str, extra: serde_json::Value| {
let mut t = serde_json::json!({ "name": name, "outcome": outcome, "binary": binary_fields(b) });
if let (Some(t), Some(x)) = (t.as_object_mut(), extra.as_object()) {
t.extend(x.clone());
}
serde_json::json!({ "test": t })
};
Some(match e {
T::TestOk { name, exec_time } => cargo_event(
anchor, Level::Info, "cargo::test", format!("test {name} ... ok"),
test(name, "ok", serde_json::json!({ "exec_time_ms": ms(exec_time) })),
),
T::TestIgnored { name, message } => cargo_event(
anchor, Level::Info, "cargo::test", format!("test {name} ... ignored"),
test(name, "ignored", serde_json::json!({ "reason": message })),
),
T::TestTimeout { name } => cargo_event(
anchor, Level::Warn, "cargo::test", format!("test {name} exceeded its time limit"),
test(name, "timeout", serde_json::json!({})),
),
T::TestFailed { name, exec_time, stdout, message } => {
let mut output = stdout.clone().unwrap_or_default();
if let Some(m) = message {
output.push_str(m);
}
let location = cargo_verdict::libtest::panic_location(&output);
let mut dropped = 0;
if output.len() > TEST_OUTPUT_LIMIT {
let mut cut = output.len() - TEST_OUTPUT_LIMIT;
while !output.is_char_boundary(cut) {
cut += 1;
}
output.drain(..cut);
dropped = cut;
}
let mut fields = test(name, "failed", serde_json::json!({
"exec_time_ms": ms(exec_time),
"output": output,
"output_truncated_bytes": dropped,
}));
// `file:line:col` of the panic, in the same shape as a diagnostic.
if let Some(loc) = location {
let mut parts = loc.rsplitn(3, ':');
let (col, line, path) = (parts.next(), parts.next(), parts.next());
fields["file"] = serde_json::json!({
"path": path,
"line": line.and_then(|l| l.parse::<u64>().ok()),
"col": col.and_then(|c| c.parse::<u64>().ok()),
});
}
cargo_event(anchor, Level::Error, "cargo::test", format!("test {name} ... FAILED"), fields)
}
T::SuiteFinished { ok, passed, failed, ignored, measured, filtered_out, exec_time } => cargo_event(
anchor,
if *ok { Level::Info } else { Level::Error },
"cargo::test",
format!(
"test binary {}: {passed} passed; {failed} failed; {ignored} ignored; {filtered_out} filtered out",
binary_label(b)
),
serde_json::json!({ "test": { "binary": binary_fields(b), "suite": {
"ok": ok, "passed": passed, "failed": failed, "ignored": ignored,
"measured": measured, "filtered_out": filtered_out, "exec_time_ms": ms(exec_time),
} } }),
),
_ => return None,
})
}
// ─── ESLint beholder ─────────────────────────────────────────────────────────
/// Factory for the bundled ESLint beholder (Tier 1.5, Rewriter mode).
///
/// Matches `eslint` invocations and rewrites argv to add `--format=json`, which
/// makes ESLint emit a single JSON array at exit rather than human-formatted
/// text. Parsing happens in `on_done` because ESLint writes the entire document
/// at process exit, not line-by-line.
///
/// Declines when:
/// - `--version`, `--env-info`, or `--print-config` are present (non-lint).
/// - `--format=<value>` is already set to something other than `json` (respects
/// explicit user intent; if it's already `json`, the rewriter is a no-op and
/// the beholder still attaches as a parser).
pub struct EslintBeholderFactory;
impl BeholderFactory for EslintBeholderFactory {
fn name(&self) -> &'static str { "eslint" }
fn version(&self) -> &'static str { "8.0" }
fn matches(&self, argv: &[String]) -> bool {
if argv.first().map(String::as_str) != Some("eslint") {
return false;
}
// Non-lint invocations.
if argv.iter().any(|a| matches!(a.as_str(), "--version" | "--env-info" | "--print-config")) {
return false;
}
// Decline if the user explicitly chose a non-json formatter.
// `--format=json` is fine — rewriter is a no-op; beholder still parses.
// `-f json` / `-f compact` etc. are also handled.
let format_arg = argv.windows(2)
.find(|w| w[0] == "-f" || w[0] == "--format")
.map(|w| w[1].as_str());
let format_eq = argv.iter()
.find(|a| a.starts_with("--format="))
.map(|a| a.trim_start_matches("--format="));
let explicit_format = format_arg.or(format_eq);
matches!(explicit_format, None | Some("json"))
}
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter {
adjust_argv: |argv| {
if !argv.iter().any(|a| a == "--format=json") {
argv.push("--format=json".to_owned());
}
},
}
}
fn create(&self) -> Box<dyn Beholder> {
Box::new(EslintBeholder::default())
}
fn tool_version_range(&self) -> Option<ToolVersionRange> {
// --format=json has been available since ESLint 1.x; 8.0 is the
// ESLint version range this beholder was built and tested against.
Some(ToolVersionRange { min: Some("8.0.0"), max: None })
}
}
/// Per-run ESLint JSON parser. Buffers all output; parses in `on_done`.
///
/// ESLint writes the entire JSON array to stdout at process exit. There is no
/// streaming line-by-line format in the `json` formatter, so incremental
/// parsing is not possible. The beholder accumulates raw bytes and flushes
/// structured events once the PTY closes.
pub struct EslintBeholder {
buf: Vec<u8>,
unknown_format: Option<String>,
}
impl Default for EslintBeholder {
fn default() -> Self {
Self { buf: Vec::new(), unknown_format: None }
}
}
impl Beholder for EslintBeholder {
fn name(&self) -> &'static str { "eslint" }
fn version(&self) -> &'static str { "8.0" }
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter {
adjust_argv: |argv| {
if !argv.iter().any(|a| a == "--format=json") {
argv.push("--format=json".to_owned());
}
},
}
}
fn parse_chunk(&mut self, chunk: &OutputChunk) -> Vec<Event> {
self.buf.extend_from_slice(&chunk.bytes);
Vec::new()
}
fn on_done(&mut self, run_id: &crate::types::TaskRunId, offset_ms: u32) -> Vec<Event> {
if self.buf.is_empty() {
return Vec::new();
}
let Ok(s) = std::str::from_utf8(&self.buf) else {
self.unknown_format = Some("non-UTF-8 output".to_owned());
return Vec::new();
};
// ESLint JSON output is an array; strip any leading/trailing ANSI escapes
// or shell prompt noise that a PTY might inject before/after the document.
let s = s.trim();
match serde_json::from_str::<Vec<EslintFile>>(s) {
Ok(files) => eslint_to_events(files, run_id, offset_ms),
Err(e) => {
self.unknown_format = Some(format!("failed to parse ESLint JSON: {e}"));
Vec::new()
}
}
}
fn unknown_format_reason(&self) -> Option<&str> {
self.unknown_format.as_deref()
}
}
// ─── ESLint JSON structs ──────────────────────────────────────────────────────
#[derive(serde::Deserialize)]
struct EslintFile {
#[serde(rename = "filePath")]
file_path: String,
messages: Vec<EslintMessage>,
}
#[derive(serde::Deserialize)]
struct EslintMessage {
#[serde(rename = "ruleId")]
rule_id: Option<String>,
severity: u8,
message: String,
line: Option<u32>,
column: Option<u32>,
}
fn eslint_to_events(
files: Vec<EslintFile>,
run_id: &crate::types::TaskRunId,
offset_ms: u32,
) -> Vec<Event> {
let mut events = Vec::new();
for file in files {
for msg in file.messages {
let level = match msg.severity {
2 => Level::Error,
1 => Level::Warn,
_ => Level::Info,
};
let mut fields = serde_json::json!({});
if let Some(ref rule) = msg.rule_id {
fields["error"] = serde_json::json!({ "code": rule });
}
let mut file_fields = serde_json::json!({ "path": file.file_path });
if let Some(l) = msg.line { file_fields["line"] = serde_json::json!(l); }
if let Some(c) = msg.column { file_fields["col"] = serde_json::json!(c); }
fields["file"] = file_fields;
events.push(Event {
run_id: run_id.clone(),
seq: 0,
offset_ms,
level,
target: "eslint".to_owned(),
msg: msg.message,
fields,
anchor: None,
source: EventSource::Beholder {
name: "eslint".to_owned(),
version: "8.0".to_owned(),
},
});
}
}
events
}
// ─── Biome beholder ──────────────────────────────────────────────────────────
/// Subcommands that produce lint diagnostics.
const BIOME_LINT_SUBCOMMANDS: &[&str] = &["check", "lint", "ci"];
/// Factory for the bundled Biome beholder (Tier 1.5, Rewriter mode).
///
/// Matches `biome check/lint/ci` invocations and rewrites argv to add
/// `--reporter=json`, which makes Biome emit a single JSON object at exit.
/// Parsing happens in `on_done`.
///
/// Declines when:
/// - Subcommand is not one of `check`, `lint`, `ci`.
/// - `--reporter=<value>` is already set to something other than `json`.
/// - `--version` is present.
pub struct BiomeBeholderFactory;
impl BeholderFactory for BiomeBeholderFactory {
fn name(&self) -> &'static str { "biome" }
fn version(&self) -> &'static str { "1.0" }
fn matches(&self, argv: &[String]) -> bool {
if argv.first().map(String::as_str) != Some("biome") {
return false;
}
if argv.iter().any(|a| a == "--version") {
return false;
}
// Must have a lint-producing subcommand.
let has_lint_sub = argv.iter().skip(1)
.any(|a| BIOME_LINT_SUBCOMMANDS.contains(&a.as_str()));
if !has_lint_sub {
return false;
}
// Decline if the user explicitly chose a non-json reporter.
let reporter = argv.iter()
.find(|a| a.starts_with("--reporter="))
.map(|a| a.trim_start_matches("--reporter="));
matches!(reporter, None | Some("json"))
}
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter {
adjust_argv: |argv| {
if !argv.iter().any(|a| a == "--reporter=json") {
argv.push("--reporter=json".to_owned());
}
},
}
}
fn create(&self) -> Box<dyn Beholder> {
Box::new(BiomeBeholder::default())
}
fn tool_version_range(&self) -> Option<ToolVersionRange> {
// --reporter=json was available from Biome 1.0.
Some(ToolVersionRange { min: Some("1.0.0"), max: None })
}
}
/// Per-run Biome JSON parser. Buffers all output; parses in `on_done`.
///
/// Biome writes a single JSON object to stdout at process exit. Like ESLint,
/// the entire document appears at once, so incremental parsing is not possible.
pub struct BiomeBeholder {
buf: Vec<u8>,
unknown_format: Option<String>,
}
impl Default for BiomeBeholder {
fn default() -> Self {
Self { buf: Vec::new(), unknown_format: None }
}
}
impl Beholder for BiomeBeholder {
fn name(&self) -> &'static str { "biome" }
fn version(&self) -> &'static str { "1.0" }
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter {
adjust_argv: |argv| {
if !argv.iter().any(|a| a == "--reporter=json") {
argv.push("--reporter=json".to_owned());
}
},
}
}
fn parse_chunk(&mut self, chunk: &OutputChunk) -> Vec<Event> {
self.buf.extend_from_slice(&chunk.bytes);
Vec::new()
}
fn on_done(&mut self, run_id: &crate::types::TaskRunId, offset_ms: u32) -> Vec<Event> {
if self.buf.is_empty() {
return Vec::new();
}
let Ok(s) = std::str::from_utf8(&self.buf) else {
self.unknown_format = Some("non-UTF-8 output".to_owned());
return Vec::new();
};
let s = s.trim();
match serde_json::from_str::<BiomeOutput>(s) {
Ok(output) => biome_to_events(output.diagnostics, run_id, offset_ms),
Err(e) => {
self.unknown_format = Some(format!("failed to parse Biome JSON: {e}"));
Vec::new()
}
}
}
fn unknown_format_reason(&self) -> Option<&str> {
self.unknown_format.as_deref()
}
}
// ─── Biome JSON structs ───────────────────────────────────────────────────────
#[derive(serde::Deserialize)]
struct BiomeOutput {
#[serde(default)]
diagnostics: Vec<BiomeDiagnostic>,
}
#[derive(serde::Deserialize)]
struct BiomeDiagnostic {
category: Option<String>,
severity: String,
description: String,
location: Option<BiomeLocation>,
}
#[derive(serde::Deserialize)]
struct BiomeLocation {
path: Option<BiomePath>,
}
#[derive(serde::Deserialize)]
struct BiomePath {
file: Option<String>,
}
fn biome_to_events(
diagnostics: Vec<BiomeDiagnostic>,
run_id: &crate::types::TaskRunId,
offset_ms: u32,
) -> Vec<Event> {
diagnostics.into_iter().map(|d| {
let level = match d.severity.as_str() {
"fatal" => Level::Fatal,
"error" => Level::Error,
"warning" => Level::Warn,
"information" => Level::Info,
"hint" => Level::Debug,
_ => Level::Warn,
};
let mut fields = serde_json::json!({});
if let Some(ref cat) = d.category {
fields["error"] = serde_json::json!({ "code": cat });
}
if let Some(loc) = d.location {
if let Some(path) = loc.path {
if let Some(file) = path.file {
fields["file"] = serde_json::json!({ "path": file });
}
}
}
Event {
run_id: run_id.clone(),
seq: 0,
offset_ms,
level,
target: "biome".to_owned(),
msg: d.description,
fields,
anchor: None,
source: EventSource::Beholder {
name: "biome".to_owned(),
version: "1.0".to_owned(),
},
}
}).collect()
}
// ─── JS test beholder (vitest / jest / bun-test) ─────────────────────────────
/// Argv rewriter for vitest: adds `--reporter=json` when not already present.
fn vitest_adjust_argv(argv: &mut Vec<String>) {
if !argv.iter().any(|a| a == "--reporter=json") {
argv.push("--reporter=json".to_owned());
}
}
/// Argv rewriter for jest: adds `--json` when not already present.
fn jest_adjust_argv(argv: &mut Vec<String>) {
if !argv.iter().any(|a| a == "--json") {
argv.push("--json".to_owned());
}
}
/// Argv rewriter for bun test: adds `--reporter=json` when not already present.
fn bun_test_adjust_argv(argv: &mut Vec<String>) {
if !argv.iter().any(|a| a == "--reporter=json") {
argv.push("--reporter=json".to_owned());
}
}
/// Factory for the bundled vitest beholder (Tier 1.5, Rewriter mode).
///
/// Matches `vitest` invocations and rewrites argv to add `--reporter=json`,
/// which makes vitest emit a single Jest-compatible JSON document to stdout
/// at exit. Parsing happens in `on_done`.
///
/// Declines when:
/// - `--version` / `-v` are present (non-test invocations).
/// - `--reporter=<value>` is already set to a non-json reporter (respects
/// explicit user intent; if it's already `json`, rewriter is a no-op).
pub struct VitestBeholderFactory;
impl BeholderFactory for VitestBeholderFactory {
fn name(&self) -> &'static str { "vitest" }
fn version(&self) -> &'static str { "1.0" }
fn matches(&self, argv: &[String]) -> bool {
if argv.first().map(String::as_str) != Some("vitest") {
return false;
}
if argv.iter().any(|a| matches!(a.as_str(), "--version" | "-v")) {
return false;
}
// Decline if user set a non-json reporter (--reporter=<x> or --reporter <x>).
let reporter_eq = argv.iter()
.find(|a| a.starts_with("--reporter="))
.map(|a| a.trim_start_matches("--reporter="));
let reporter_space = argv.windows(2)
.find(|w| w[0] == "--reporter")
.map(|w| w[1].as_str());
let explicit = reporter_eq.or(reporter_space);
matches!(explicit, None | Some("json"))
}
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter { adjust_argv: vitest_adjust_argv }
}
fn create(&self) -> Box<dyn Beholder> {
Box::new(JsTestBeholder::new("vitest", "1.0", vitest_adjust_argv))
}
fn tool_version_range(&self) -> Option<ToolVersionRange> {
Some(ToolVersionRange { min: Some("1.0.0"), max: None })
}
}
/// Factory for the bundled jest beholder (Tier 1.5, Rewriter mode).
///
/// Matches `jest` invocations and rewrites argv to add `--json`, which makes
/// jest emit a JSON test report to stdout at exit. Parsing happens in `on_done`.
///
/// Declines when:
/// - `--version` / `-v` are present.
/// - `--outputFile` is present (JSON would go to a file, not stdout).
pub struct JestBeholderFactory;
impl BeholderFactory for JestBeholderFactory {
fn name(&self) -> &'static str { "jest" }
fn version(&self) -> &'static str { "27.0" }
fn matches(&self, argv: &[String]) -> bool {
if argv.first().map(String::as_str) != Some("jest") {
return false;
}
if argv.iter().any(|a| matches!(a.as_str(), "--version" | "-v")) {
return false;
}
// --outputFile sends JSON to a file rather than stdout; we can't capture that.
if argv.iter().any(|a| a.starts_with("--outputFile")) {
return false;
}
true
}
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter { adjust_argv: jest_adjust_argv }
}
fn create(&self) -> Box<dyn Beholder> {
Box::new(JsTestBeholder::new("jest", "27.0", jest_adjust_argv))
}
fn tool_version_range(&self) -> Option<ToolVersionRange> {
Some(ToolVersionRange { min: Some("27.0.0"), max: None })
}
}
/// Factory for the bundled bun-test beholder (Tier 1.5, Rewriter mode).
///
/// Matches `bun test` invocations (argv[0]="bun", argv[1]="test") and rewrites
/// argv to add `--reporter=json`. Parsing happens in `on_done`.
///
/// Declines when:
/// - `--version` is present.
/// - `--reporter=<value>` is already set to a non-json reporter.
pub struct BunTestBeholderFactory;
impl BeholderFactory for BunTestBeholderFactory {
fn name(&self) -> &'static str { "bun-test" }
fn version(&self) -> &'static str { "1.0" }
fn matches(&self, argv: &[String]) -> bool {
if argv.first().map(String::as_str) != Some("bun") {
return false;
}
if argv.get(1).map(String::as_str) != Some("test") {
return false;
}
if argv.iter().any(|a| a == "--version") {
return false;
}
let reporter = argv.iter()
.find(|a| a.starts_with("--reporter="))
.map(|a| a.trim_start_matches("--reporter="));
matches!(reporter, None | Some("json"))
}
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter { adjust_argv: bun_test_adjust_argv }
}
fn create(&self) -> Box<dyn Beholder> {
Box::new(JsTestBeholder::new("bun-test", "1.0", bun_test_adjust_argv))
}
fn tool_version_range(&self) -> Option<ToolVersionRange> {
Some(ToolVersionRange { min: Some("1.0.0"), max: None })
}
}
/// Per-run JS test JSON parser shared by vitest, jest, and bun-test.
///
/// All three produce a Jest-compatible JSON report on stdout at process exit
/// (via `--reporter=json` for vitest/bun-test, `--json` for jest). There is no
/// streaming line-by-line format, so incremental parsing is not possible: the
/// beholder accumulates raw bytes and flushes structured events in `on_done`.
///
/// Events emitted:
/// - One summary event (Info on success, Error on failure) with pass/fail counts.
/// - One Error event per failed assertion with the test name and failure message.
pub struct JsTestBeholder {
tool: &'static str,
version: &'static str,
adjust_argv_fn: fn(&mut Vec<String>),
buf: Vec<u8>,
unknown_format: Option<String>,
}
impl JsTestBeholder {
fn new(tool: &'static str, version: &'static str, adjust_argv_fn: fn(&mut Vec<String>)) -> Self {
Self { tool, version, adjust_argv_fn, buf: Vec::new(), unknown_format: None }
}
}
impl Beholder for JsTestBeholder {
fn name(&self) -> &'static str { self.tool }
fn version(&self) -> &'static str { self.version }
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter { adjust_argv: self.adjust_argv_fn }
}
fn parse_chunk(&mut self, chunk: &OutputChunk) -> Vec<Event> {
self.buf.extend_from_slice(&chunk.bytes);
Vec::new()
}
fn on_done(&mut self, run_id: &crate::types::TaskRunId, offset_ms: u32) -> Vec<Event> {
if self.buf.is_empty() {
return Vec::new();
}
let Ok(s) = std::str::from_utf8(&self.buf) else {
self.unknown_format = Some("non-UTF-8 output".to_owned());
return Vec::new();
};
let s = s.trim();
match serde_json::from_str::<JsTestReport>(s) {
Ok(report) => js_test_to_events(report, run_id, offset_ms, self.tool, self.version),
Err(e) => {
self.unknown_format = Some(format!("failed to parse {} JSON: {e}", self.tool));
Vec::new()
}
}
}
fn unknown_format_reason(&self) -> Option<&str> {
self.unknown_format.as_deref()
}
}
// ─── JS test JSON structs ─────────────────────────────────────────────────────
#[derive(serde::Deserialize)]
struct JsTestReport {
#[serde(default)]
success: bool,
#[serde(rename = "numTotalTests", default)]
num_total: u32,
#[serde(rename = "numPassedTests", default)]
num_passed: u32,
#[serde(rename = "numFailedTests", default)]
num_failed: u32,
#[serde(rename = "numPendingTests", default)]
num_pending: u32,
#[serde(rename = "testResults", default)]
test_results: Vec<JsTestSuiteResult>,
}
#[derive(serde::Deserialize)]
struct JsTestSuiteResult {
#[serde(rename = "testFilePath")]
file_path: String,
#[serde(rename = "assertionResults", default)]
assertions: Vec<JsTestAssertionResult>,
}
#[derive(serde::Deserialize)]
struct JsTestAssertionResult {
#[serde(rename = "fullName", default)]
full_name: String,
status: String,
#[serde(rename = "failureMessages", default)]
failure_messages: Vec<String>,
}
fn js_test_to_events(
report: JsTestReport,
run_id: &crate::types::TaskRunId,
offset_ms: u32,
tool: &'static str,
version: &'static str,
) -> Vec<Event> {
let mut events = Vec::new();
// Summary event: one per run.
let summary_msg = if report.success {
format!("{} passed", report.num_passed)
} else {
format!("{} failed, {} passed", report.num_failed, report.num_passed)
};
events.push(Event {
run_id: run_id.clone(),
seq: 0,
offset_ms,
level: if report.success { Level::Info } else { Level::Error },
target: tool.to_owned(),
msg: summary_msg,
fields: serde_json::json!({
"test": {
"total": report.num_total,
"passed": report.num_passed,
"failed": report.num_failed,
"pending": report.num_pending,
},
"build": { "success": report.success }
}),
anchor: None,
source: EventSource::Beholder {
name: tool.to_owned(),
version: version.to_owned(),
},
});
// One Error event per failed assertion.
for suite in &report.test_results {
for assertion in &suite.assertions {
if assertion.status != "failed" {
continue;
}
// Use the first failure message (first line only to keep events compact).
let msg = assertion.failure_messages
.first()
.and_then(|s| s.lines().next())
.map(str::to_owned)
.unwrap_or_else(|| assertion.full_name.clone());
events.push(Event {
run_id: run_id.clone(),
seq: 0,
offset_ms,
level: Level::Error,
target: format!("{}::test", tool),
msg,
fields: serde_json::json!({
"test": { "name": assertion.full_name },
"file": { "path": suite.file_path },
}),
anchor: None,
source: EventSource::Beholder {
name: tool.to_owned(),
version: version.to_owned(),
},
});
}
}
events
}
// ─── pytest beholder ─────────────────────────────────────────────────────────
/// Factory for the bundled pytest beholder (Tier 1.5, Parser mode).
///
/// Matches `pytest`, `py.test`, and `python[-3] -m pytest` invocations and
/// parses the standard human-readable output for structured events. Parser mode
/// is used because pytest-json-report is not universally available; the standard
/// text format is stable across all supported pytest versions (≥ 7).
///
/// Events emitted:
/// - One Error event per `FAILED`/`ERROR` line in the short test summary.
/// - One summary event (Info on success, Error on failure) from the final
/// `N failed, M passed[…] in Ws` separator line.
///
/// Declines when:
/// - `--version` / `-V` / `--help` / `-h` are present.
/// - `--collect-only` / `--co` are present (no test execution, different output).
pub struct PytestBeholderFactory;
impl BeholderFactory for PytestBeholderFactory {
fn name(&self) -> &'static str { "pytest" }
fn version(&self) -> &'static str { "7.0" }
fn matches(&self, argv: &[String]) -> bool {
let first = argv.first().map(String::as_str);
let is_direct = matches!(first, Some("pytest") | Some("py.test"));
let is_python_m = matches!(first, Some("python") | Some("python3"))
&& argv.windows(2).any(|w| w[0] == "-m" && w[1] == "pytest");
if !is_direct && !is_python_m {
return false;
}
for a in argv {
match a.as_str() {
"--version" | "-V" | "--help" | "-h" => return false,
"--collect-only" | "--co" => return false,
_ => {}
}
}
true
}
fn mode(&self) -> BeholderMode { BeholderMode::Parser }
fn create(&self) -> Box<dyn Beholder> { Box::new(PytestBeholder::default()) }
fn tool_version_range(&self) -> Option<ToolVersionRange> {
Some(ToolVersionRange { min: Some("7.0.0"), max: None })
}
}
/// Per-run pytest text-output parser.
///
/// Processes lines incrementally. Emits:
/// - Error events for `FAILED`/`ERROR` entries in the short test summary section.
/// - A summary event from the final `=== N failed, M passed … ===` line.
pub struct PytestBeholder {
buf: Vec<u8>,
/// The inner text of the last `=== … ===` summary separator seen.
summary_line: Option<String>,
unknown_format: Option<String>,
}
impl Default for PytestBeholder {
fn default() -> Self {
Self { buf: Vec::new(), summary_line: None, unknown_format: None }
}
}
impl Beholder for PytestBeholder {
fn name(&self) -> &'static str { "pytest" }
fn version(&self) -> &'static str { "7.0" }
fn mode(&self) -> BeholderMode { BeholderMode::Parser }
fn parse_chunk(&mut self, chunk: &OutputChunk) -> Vec<Event> {
self.buf.extend_from_slice(&chunk.bytes);
let mut events = Vec::new();
let source = EventSource::Beholder {
name: "pytest".to_owned(),
version: "7.0".to_owned(),
};
while let Some(nl) = self.buf.iter().position(|&b| b == b'\n') {
let raw: Vec<u8> = self.buf.drain(..=nl).collect();
let Ok(line) = std::str::from_utf8(raw.trim_ascii_end()) else { continue };
if line.is_empty() { continue }
if let Some(ev) = parse_pytest_failure_line(line, chunk, &source) {
events.push(ev);
continue;
}
// Track the final `=== N failed, M passed … ===` line; may be updated
// multiple times in watch/loop mode — we keep the latest.
if line.starts_with("==") && line.ends_with("==") {
let inner = line.trim_matches('=').trim();
if inner.contains(" passed") || inner.contains(" failed")
|| inner.contains(" error")
{
self.summary_line = Some(inner.to_owned());
}
}
}
events
}
fn on_done(&mut self, run_id: &crate::types::TaskRunId, offset_ms: u32) -> Vec<Event> {
let Some(ref summary) = self.summary_line else {
return Vec::new();
};
let Ok(parsed) = parse_pytest_summary(summary) else {
self.unknown_format = Some(format!("could not parse pytest summary: {summary:?}"));
return Vec::new();
};
let level = if parsed.failed > 0 || parsed.errors > 0 { Level::Error } else { Level::Info };
let msg = format_pytest_summary_msg(&parsed);
vec![Event {
run_id: run_id.clone(),
seq: 0,
offset_ms,
level,
target: "pytest".to_owned(),
msg,
fields: serde_json::json!({
"test": {
"total": parsed.passed + parsed.failed + parsed.skipped + parsed.errors,
"passed": parsed.passed,
"failed": parsed.failed,
"skipped": parsed.skipped,
"errors": parsed.errors,
},
"build": { "success": parsed.failed == 0 && parsed.errors == 0 }
}),
anchor: None,
source: EventSource::Beholder {
name: "pytest".to_owned(),
version: "7.0".to_owned(),
},
}]
}
fn unknown_format_reason(&self) -> Option<&str> { self.unknown_format.as_deref() }
}
// ─── pytest parsing helpers ───────────────────────────────────────────────────
/// Parse a `FAILED` or `ERROR` short-summary line into an Event, or return None.
///
/// Formats handled:
/// - `FAILED tests/foo.py::test_name - AssertionError: assert 1 == 2`
/// - `ERROR tests/broken.py - ImportError: No module named 'x'`
fn parse_pytest_failure_line(
line: &str,
chunk: &OutputChunk,
source: &EventSource,
) -> Option<Event> {
let (label, rest) = if let Some(r) = line.strip_prefix("FAILED ") {
("FAILED", r)
} else if let Some(r) = line.strip_prefix("ERROR ") {
("ERROR", r)
} else {
return None;
};
// Require a `.py` in the nodeid portion to avoid false positives on
// arbitrary lines that happen to start with these words.
let (nodeid, reason) = if let Some(idx) = rest.find(" - ") {
let nodeid = &rest[..idx];
if !nodeid.contains(".py") { return None; }
(nodeid, rest[idx + 3..].trim())
} else {
if !rest.contains(".py") { return None; }
(rest, "")
};
let (file_path, test_name) = if let Some(idx) = nodeid.find("::") {
(&nodeid[..idx], &nodeid[idx + 2..])
} else {
(nodeid, "")
};
let msg = if reason.is_empty() {
format!("{label} {nodeid}")
} else {
reason.to_owned()
};
Some(Event {
run_id: chunk.run_id.clone(),
seq: 0,
offset_ms: chunk.offset_ms,
level: Level::Error,
target: "pytest::test".to_owned(),
msg,
fields: serde_json::json!({
"test": { "name": test_name },
"file": { "path": file_path },
}),
anchor: Some(ChunkRef { seq: chunk.seq }),
source: source.clone(),
})
}
struct PytestSummary {
passed: u32,
failed: u32,
skipped: u32,
errors: u32,
}
/// Parse `"2 failed, 5 passed, 1 skipped in 1.23s"` → `PytestSummary`.
fn parse_pytest_summary(s: &str) -> Result<PytestSummary, ()> {
// Strip duration suffix " in W.XXs" (optional — may be absent on early exit).
let s = if let Some(idx) = s.rfind(" in ") { &s[..idx] } else { s };
let mut passed = 0u32;
let mut failed = 0u32;
let mut skipped = 0u32;
let mut errors = 0u32;
let mut any = false;
for part in s.split(", ") {
let part = part.trim();
let mut it = part.splitn(2, ' ');
let count: u32 = it.next().and_then(|n| n.parse().ok()).ok_or(())?;
let kind = it.next().ok_or(())?;
any = true;
if kind.starts_with("passed") { passed = count; }
else if kind.starts_with("failed") { failed = count; }
else if kind.starts_with("skipped") { skipped = count; }
else if kind.starts_with("error") { errors = count; }
// Other kinds (warnings, deselected, xfailed, xpassed) pass through.
}
if !any { return Err(()); }
Ok(PytestSummary { passed, failed, skipped, errors })
}
fn format_pytest_summary_msg(s: &PytestSummary) -> String {
let mut parts: Vec<String> = Vec::new();
if s.failed > 0 { parts.push(format!("{} failed", s.failed)); }
if s.errors > 0 { parts.push(format!("{} error{}", s.errors, if s.errors == 1 { "" } else { "s" })); }
if s.passed > 0 { parts.push(format!("{} passed", s.passed)); }
if s.skipped > 0 { parts.push(format!("{} skipped", s.skipped)); }
if parts.is_empty() { return "no tests ran".to_owned(); }
parts.join(", ")
}
// ─── vite-build beholder ─────────────────────────────────────────────────────
/// Factory for the bundled vite-build beholder (Tier 1.5, Parser mode).
///
/// Matches `vite build` invocations (bare or via wrapper: `bunx vite build`,
/// `pnpm vite build`). Parses the human-formatted output for `(!)` warnings,
/// per-asset bundle sizes, and the final `built in X.XXs` timing line.
///
/// Declines when:
/// - `argv[0]` (post-wrapper-stripping) is not `vite`.
/// - No `build` subcommand is present in argv.
/// - `--help` / `-h` / `--version` flags are present.
pub struct ViteBuildBeholderFactory;
impl BeholderFactory for ViteBuildBeholderFactory {
fn name(&self) -> &'static str { "vite-build" }
fn version(&self) -> &'static str { "4.0" }
fn matches(&self, argv: &[String]) -> bool {
if argv.first().map(String::as_str) != Some("vite") { return false; }
if !argv.iter().skip(1).any(|a| a == "build") { return false; }
for a in argv {
match a.as_str() {
"--help" | "-h" | "--version" => return false,
_ => {}
}
}
true
}
fn mode(&self) -> BeholderMode { BeholderMode::Parser }
fn create(&self) -> Box<dyn Beholder> { Box::new(ViteBuildBeholder::default()) }
fn tool_version_range(&self) -> Option<ToolVersionRange> {
Some(ToolVersionRange { min: Some("4.0.0"), max: None })
}
}
/// Per-run vite build output parser.
///
/// Processes lines incrementally. Emits:
/// - `Warn` events for `(!) <text>` warning lines (oversized chunks, eval, etc.)
/// - `Info` events for `dist/<file> X kB │ gzip: Y kB` bundle-output lines.
/// - An `Info` timing event from the `✓ built in X.XXs` line.
pub struct ViteBuildBeholder {
buf: Vec<u8>,
unknown_format: Option<String>,
}
impl Default for ViteBuildBeholder {
fn default() -> Self {
Self { buf: Vec::new(), unknown_format: None }
}
}
impl Beholder for ViteBuildBeholder {
fn name(&self) -> &'static str { "vite-build" }
fn version(&self) -> &'static str { "4.0" }
fn mode(&self) -> BeholderMode { BeholderMode::Parser }
fn parse_chunk(&mut self, chunk: &OutputChunk) -> Vec<Event> {
self.buf.extend_from_slice(&chunk.bytes);
let mut events = Vec::new();
let source = EventSource::Beholder {
name: "vite-build".to_owned(),
version: "4.0".to_owned(),
};
while let Some(nl) = self.buf.iter().position(|&b| b == b'\n') {
let raw: Vec<u8> = self.buf.drain(..=nl).collect();
let Ok(line) = std::str::from_utf8(raw.trim_ascii_end()) else { continue };
if line.is_empty() { continue }
// (!) warning lines
if let Some(warn_text) = line.strip_prefix("(!) ") {
events.push(Event {
run_id: chunk.run_id.clone(),
seq: 0,
offset_ms: chunk.offset_ms,
level: Level::Warn,
target: "vite-build::warning".to_owned(),
msg: warn_text.trim().to_owned(),
fields: serde_json::json!({ "build": { "warning": true } }),
anchor: Some(ChunkRef { seq: chunk.seq }),
source: source.clone(),
});
continue;
}
// dist/<file> X.XX kB [│ gzip: Y.YY kB]
if let Some(ev) = parse_vite_bundle_line(line, chunk, &source) {
events.push(ev);
continue;
}
// "✓ built in X.XXs" or "built in X.XXs"
if let Some(ms) = parse_vite_built_ms(line) {
events.push(Event {
run_id: chunk.run_id.clone(),
seq: 0,
offset_ms: chunk.offset_ms,
level: Level::Info,
target: "vite-build".to_owned(),
msg: format!("built in {:.2}s", ms as f64 / 1000.0),
fields: serde_json::json!({
"build": { "duration_ms": ms, "success": true }
}),
anchor: Some(ChunkRef { seq: chunk.seq }),
source: source.clone(),
});
}
}
events
}
fn unknown_format_reason(&self) -> Option<&str> { self.unknown_format.as_deref() }
}
// ─── vite-build parsing helpers ───────────────────────────────────────────────
/// Parse `dist/<file> X.XX kB [│ gzip: Y.YY kB]` into an Event, or None.
///
/// The `│` (U+2502) separator and gzip column are optional.
fn parse_vite_bundle_line(line: &str, chunk: &OutputChunk, source: &EventSource) -> Option<Event> {
let trimmed = line.trim();
if !trimmed.starts_with("dist/") { return None; }
if !trimmed.contains("kB") { return None; }
let tokens: Vec<&str> = trimmed.split_whitespace().collect();
let file = tokens.first()?;
// Collect all `float kB` pairs in order: first is size, second (if any) is gzip.
let mut values: Vec<f64> = Vec::new();
let mut i = 1;
while i < tokens.len() {
if let Ok(v) = tokens[i].parse::<f64>() {
if tokens.get(i + 1).copied() == Some("kB") {
values.push(v);
i += 2;
continue;
}
}
i += 1;
}
let size_kb = *values.first()?;
let gzip_kb = values.get(1).copied();
let msg = if let Some(g) = gzip_kb {
format!("{file}: {size_kb:.2} kB (gzip: {g:.2} kB)")
} else {
format!("{file}: {size_kb:.2} kB")
};
let mut fields = serde_json::json!({
"file": { "path": file },
"build": { "size_kb": size_kb },
});
if let Some(g) = gzip_kb {
fields["build"]["gzip_kb"] = serde_json::json!(g);
}
Some(Event {
run_id: chunk.run_id.clone(),
seq: 0,
offset_ms: chunk.offset_ms,
level: Level::Info,
target: "vite-build::bundle".to_owned(),
msg,
fields,
anchor: Some(ChunkRef { seq: chunk.seq }),
source: source.clone(),
})
}
/// Parse `"✓ built in 2.43s"` or `"built in 2.43s"` → duration in ms.
fn parse_vite_built_ms(line: &str) -> Option<u64> {
let idx = line.find("built in ")?;
let rest = line[idx + "built in ".len()..].trim();
let secs_str = rest.trim_end_matches(|c: char| c.is_alphabetic());
let secs: f64 = secs_str.parse().ok()?;
Some((secs * 1000.0).round() as u64)
}
// ─── Tests ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
// ─── Test fixtures ────────────────────────────────────────────────────────
/// A no-op parser beholder that matches commands starting with a prefix.
struct PrefixFactory {
prefix: &'static str,
name: &'static str,
}
struct NoopBeholder {
name: &'static str,
}
impl BeholderFactory for PrefixFactory {
fn name(&self) -> &'static str { self.name }
fn version(&self) -> &'static str { "1.0" }
fn matches(&self, argv: &[String]) -> bool {
argv.first().map(|s| s.starts_with(self.prefix)).unwrap_or(false)
}
fn mode(&self) -> BeholderMode { BeholderMode::Parser }
fn create(&self) -> Box<dyn Beholder> { Box::new(NoopBeholder { name: self.name }) }
}
impl Beholder for NoopBeholder {
fn name(&self) -> &'static str { self.name }
fn version(&self) -> &'static str { "1.0" }
fn mode(&self) -> BeholderMode { BeholderMode::Parser }
fn parse_chunk(&mut self, _chunk: &OutputChunk) -> Vec<Event> { vec![] }
}
/// A rewriter beholder that declines when --no-json is present.
struct RewriterFactory;
struct RewriterBeholder;
impl BeholderFactory for RewriterFactory {
fn name(&self) -> &'static str { "cargo" }
fn version(&self) -> &'static str { "1.78" }
fn matches(&self, argv: &[String]) -> bool {
argv.first().map(|s| s == "cargo").unwrap_or(false)
&& !argv.iter().any(|a| a == "--no-json")
}
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter {
adjust_argv: |argv| {
argv.push("--message-format=json-render-diagnostics".to_string());
},
}
}
fn create(&self) -> Box<dyn Beholder> { Box::new(RewriterBeholder) }
}
impl Beholder for RewriterBeholder {
fn name(&self) -> &'static str { "cargo" }
fn version(&self) -> &'static str { "1.78" }
fn mode(&self) -> BeholderMode {
BeholderMode::Rewriter {
adjust_argv: |argv| {
argv.push("--message-format=json-render-diagnostics".to_string());
},
}
}
fn parse_chunk(&mut self, _chunk: &OutputChunk) -> Vec<Event> { vec![] }
}
// ─── Registry tests ───────────────────────────────────────────────────────
#[test]
fn empty_registry_returns_none_auto() {
let registry = BeholderRegistry::new();
let result = registry.attach("cargo check", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_none());
assert_eq!(result.status.text, "none:auto");
assert_eq!(result.argv, vec!["cargo", "check"]);
}
#[test]
fn auto_attaches_first_matching() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(PrefixFactory { prefix: "cargo", name: "cargo" }));
let result = registry.attach("cargo check --workspace", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some());
assert_eq!(result.status.text, "attached:cargo@1.0");
}
#[test]
fn rewriter_adjusts_argv_in_result() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
let result = registry.attach("cargo check --workspace", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some());
// Rewrite must be surfaced in status text and in rewrite_added.
assert!(
result.status.text.starts_with("attached:cargo@1.78"),
"got: {}",
result.status.text
);
assert!(
result.status.text.contains("rewrite="),
"rewrite must be surfaced in status; got: {}",
result.status.text
);
assert!(
result.argv.contains(&"--message-format=json-render-diagnostics".to_string()),
"rewriter must append the JSON flag"
);
assert_eq!(
result.status.rewrite_added.as_deref(),
Some(vec!["--message-format=json-render-diagnostics".to_string()].as_slice()),
"rewrite_added must list the injected arg"
);
}
#[test]
fn auto_respects_decline_from_matches() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
let result = registry.attach("cargo check --no-json", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_none());
assert_eq!(result.status.text, "none:auto");
// argv unchanged — no rewrite when beholder declined
assert!(!result.argv.contains(&"--message-format=json-render-diagnostics".to_string()));
}
/// R739-S2. The live failure this guard exists for: `yah build run`
/// relocates an agent's whole bash line into `task.run`, and 76.5% of the
/// lines in this camp are compound. Without the guard the rewriter's flag
/// lands on the last token of the pipeline and the command dies with
/// `tail: unrecognized option '--message-format=…'`.
#[test]
fn a_piped_line_gets_no_rewriter_because_the_flag_would_land_on_the_last_stage() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
let result = registry.attach(
"cargo check -p yah-agent-tools --lib 2>&1 | tail -12",
&BeholderSelect::Auto,
OutputReader::Raw,
&RunContext::default(),
);
assert!(result.beholder.is_none(), "no beholder may attach to a shell line");
assert_eq!(result.status.text, "declined:auto reason=\"compound-command\"");
assert!(
!result
.argv
.contains(&"--message-format=json-render-diagnostics".to_string()),
"the flag must not be spliced into `tail`'s arguments"
);
}
/// Force is not an escape hatch here: forcing a rewriter onto a compound
/// line breaks the command just as thoroughly as Auto would.
#[test]
fn force_does_not_override_the_compound_command_guard() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
let result = registry.attach(
"cargo check && cargo test",
&BeholderSelect::Force("cargo".to_string()),
OutputReader::Raw,
&RunContext::default(),
);
assert!(result.beholder.is_none());
assert_eq!(result.status.text, "declined:auto reason=\"compound-command\"");
}
#[test]
fn a_bare_command_still_attaches() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
let result = registry.attach("cargo check --workspace", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "the guard must not cost the simple case");
}
/// Over-declining is the deliberate error direction — see
/// [`is_single_simple_command`]. Recorded so a later tightening is a
/// choice rather than an accident.
#[test]
fn the_guard_is_a_character_scan_and_over_declines_on_purpose() {
assert!(is_single_simple_command("cargo check --workspace"));
assert!(is_single_simple_command("cargo test -p yah --lib mcp::tools"));
// Genuinely compound.
assert!(!is_single_simple_command("cargo check 2>&1 | tail -5"));
assert!(!is_single_simple_command("cargo check && cargo test"));
assert!(!is_single_simple_command("cargo check; echo done"));
assert!(!is_single_simple_command("cargo check $(cat args)"));
assert!(!is_single_simple_command("cargo check\ncargo test"));
// Harmless in principle, declined anyway: no shell parser here.
assert!(!is_single_simple_command("cargo check > out.txt"));
assert!(!is_single_simple_command("cargo test -p yah -- --nocapture $FILTER"));
}
#[test]
fn explicit_none_bypasses_registry() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(PrefixFactory { prefix: "cargo", name: "cargo" }));
let result = registry.attach("cargo check", &BeholderSelect::None, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_none());
assert_eq!(result.status.text, "none:explicit");
}
#[test]
fn force_pins_by_name() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(PrefixFactory { prefix: "tsc", name: "tsc" }));
registry.register(Box::new(PrefixFactory { prefix: "cargo", name: "cargo" }));
// Force "cargo" even though Auto would have matched "tsc" with "tsc" prefix
let result = registry.attach("cargo check", &BeholderSelect::Force("cargo".to_string()), OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some());
assert!(result.status.text.starts_with("forced:cargo"), "got: {}", result.status.text);
}
#[test]
fn force_against_flags_when_would_decline() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
// cargo with --no-json would normally decline; Force overrides
let result =
registry.attach("cargo check --no-json", &BeholderSelect::Force("cargo".to_string()), OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some());
assert!(
result.status.text.contains("forced-against-flags"),
"got: {}",
result.status.text
);
}
#[test]
fn force_unknown_name_returns_none() {
let registry = BeholderRegistry::new();
let result =
registry.attach("cargo check", &BeholderSelect::Force("unknown".to_string()), OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_none());
}
#[test]
fn priority_order_first_match_wins() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(PrefixFactory { prefix: "cargo", name: "first" }));
registry.register(Box::new(PrefixFactory { prefix: "cargo", name: "second" }));
let result = registry.attach("cargo check", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert_eq!(result.status.text, "attached:first@1.0");
}
// ─── verbatim-output behaviour tests ──────────────────────────────────────
#[test]
fn verbatim_output_causes_rewriter_to_decline_in_auto() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
let result = registry.attach("cargo check --workspace", &BeholderSelect::Auto, OutputReader::Verbatim, &RunContext::default());
// Beholder declines rather than change what the reader sees.
assert!(result.beholder.is_none(), "rewriter must not attach under verbatim_output");
assert!(
result.status.text.contains("declined:cargo"),
"got: {}",
result.status.text
);
assert!(
result.status.text.contains("verbatim-output"),
"got: {}",
result.status.text
);
// argv must be unchanged — no rewrite applied.
assert!(!result.argv.contains(&"--message-format=json-render-diagnostics".to_string()));
}
/// R739-B10, against the REAL cargo factory rather than the test double.
///
/// The double proves the registry's rule; this proves the rule reaches the
/// beholder that actually shipped the defect. A relocated `cargo check`
/// that succeeds emits one `compiler-artifact` record per crate, and with
/// the rewrite applied every one of them lands on the agent's stdout as raw
/// JSON — observed live 2026-08-28.
#[test]
fn the_real_cargo_rewriter_declines_when_the_output_is_read_verbatim() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(CargoBeholderFactory));
let result = registry.attach("cargo check -p yah --lib", &BeholderSelect::Auto, OutputReader::Verbatim, &RunContext::default());
assert!(result.beholder.is_none(), "got: {}", result.status.text);
assert!(
!result.argv.contains(&"--message-format=json-render-diagnostics".to_string()),
"the command line must reach the shell exactly as the caller wrote it: {:?}",
result.argv
);
// …and the same line with nobody reading the bytes still gets the
// structured events, so this is a targeted decline and not a disabling.
let observed = registry.attach("cargo check -p yah --lib", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(observed.beholder.is_some(), "got: {}", observed.status.text);
}
#[test]
fn verbatim_output_does_not_affect_parser_beholder() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(PrefixFactory { prefix: "cargo", name: "cargo" }));
// Parser beholders never touch argv, so they change nothing the reader
// sees and have no reason to decline.
let result = registry.attach("cargo check --workspace", &BeholderSelect::Auto, OutputReader::Verbatim, &RunContext::default());
assert!(
result.beholder.is_some(),
"parser beholder must attach even under verbatim_output"
);
assert_eq!(result.status.text, "attached:cargo@1.0");
}
#[test]
fn force_overrides_verbatim_decline_and_records_forced_against_verbatim() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
// Force overrides the decline; the operator explicitly wants structured
// output and accepts what it does to the stream.
let result =
registry.attach("cargo check", &BeholderSelect::Force("cargo".to_string()), OutputReader::Verbatim, &RunContext::default());
assert!(result.beholder.is_some(), "Force must attach under verbatim_output");
assert!(
result.status.text.contains("forced-against-verbatim"),
"got: {}",
result.status.text
);
// Rewrite still applied (forced) and surfaced.
assert!(
result.argv.contains(&"--message-format=json-render-diagnostics".to_string()),
"rewrite must still be applied when forced"
);
}
#[test]
fn rewrite_surfaced_on_force_normal() {
let mut registry = BeholderRegistry::new();
registry.register(Box::new(RewriterFactory));
let result =
registry.attach("cargo check", &BeholderSelect::Force("cargo".to_string()), OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some());
assert!(
result.status.text.contains("forced:cargo"),
"got: {}",
result.status.text
);
assert!(
result.status.text.contains("rewrite="),
"rewrite must be surfaced on forced attach; got: {}",
result.status.text
);
assert!(result.status.rewrite_added.is_some());
}
// ─── argv resolution tests ────────────────────────────────────────────────
#[test]
fn resolve_bare_command() {
assert_eq!(resolve_argv("cargo check --workspace"), ["cargo", "check", "--workspace"]);
}
#[test]
fn resolve_strips_bunx() {
let argv = resolve_argv("bunx vitest --run");
assert_eq!(argv[0], "vitest");
assert_eq!(argv[1], "--run");
}
#[test]
fn resolve_strips_npx() {
let argv = resolve_argv("npx tsc --noEmit");
assert_eq!(argv[0], "tsc");
assert_eq!(argv[1], "--noEmit");
}
#[test]
fn resolve_strips_pnpm() {
let argv = resolve_argv("pnpm tsc");
assert_eq!(argv[0], "tsc");
}
#[test]
fn resolve_strips_npm_exec() {
let argv = resolve_argv("npm exec tsc --noEmit");
assert_eq!(argv[0], "tsc");
assert_eq!(argv[1], "--noEmit");
}
#[test]
fn resolve_pnpm_without_exec_subcommand() {
// `pnpm tsc` — no exec subcommand, just strip pnpm
let argv = resolve_argv("pnpm vitest --reporter=verbose");
assert_eq!(argv[0], "vitest");
}
#[test]
fn resolve_empty_command() {
let argv = resolve_argv("");
assert!(argv.is_empty());
}
// ─── CargoBeholderFactory tests ───────────────────────────────────────────
fn dummy_chunk(bytes: &[u8]) -> OutputChunk {
use crate::types::{Stream, TaskRunId};
OutputChunk {
run_id: TaskRunId::new(),
seq: 0,
offset_ms: 0,
stream: Stream::Stdout,
bytes: bytes.to_vec(),
}
}
fn combined_chunk(seq: u32, text: &str) -> OutputChunk {
OutputChunk { seq, ..dummy_chunk(text.as_bytes()) }
}
fn cargo_argv(line: &str) -> Vec<String> {
line.split_whitespace().map(str::to_owned).collect()
}
/// A cwd with no cargo config anywhere above it that sets a runner.
/// A one-package workspace `x` (the engine reads its targets with `cargo
/// metadata` before a test run) with no target runner configured.
fn runnerless_dir() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[workspace]\n").unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/lib.rs"), "").unwrap();
dir
}
#[test]
fn cargo_factory_matches_verdicts_only() {
let f = CargoBeholderFactory;
for line in ["cargo doc -p x", "cargo bench", "cargo test -- --nocapture", "cargo +nightly build"] {
assert!(f.matches(&cargo_argv(line)), "{line}");
}
// run/fix/publish were in the old subcommand list; they are tenancy
// and mutation. nextest's output is not adapted. Queries never.
for line in [
"cargo run -p x", "cargo fix", "cargo publish", "cargo nextest run",
"cargo build --help", "cargo metadata", "cargo fmt", "cargo check --message-format short",
] {
assert!(!f.matches(&cargo_argv(line)), "{line}");
}
}
#[test]
fn a_test_run_gets_the_scoped_runner_quoted_for_the_shell_and_before_the_double_dash() {
let dir = runnerless_dir();
let ctx = RunContext { cwd: Some(dir.path()), env: &[] };
let registry = default_registry();
let result = registry.attach("cargo test -p x -- --nocapture", &BeholderSelect::Auto, OutputReader::Raw, &ctx);
assert!(result.beholder.is_some());
let line = result.argv.join(" ");
let dd = result.argv.iter().position(|a| a == "--").unwrap();
assert!(result.argv[..dd].iter().any(|a| a.contains(".runner=")), "{line}");
// What `sh -c` makes of the rewritten line is exactly the engine's plan.
let out = std::process::Command::new("sh")
.arg("-c")
.arg(format!("for a in {}; do printf '%s\\n' \"$a\"; done", &line["cargo ".len()..]))
.output()
.unwrap();
let words: Vec<String> = String::from_utf8(out.stdout).unwrap().lines().map(str::to_owned).collect();
let request = cargo_verdict::parse(&["test", "-p", "x", "--", "--nocapture"]);
let host = cargo_verdict::plan::host_triple().unwrap();
let plan = cargo_verdict::plan(&request, &cargo_verdict::PlanOptions::new(host));
assert!(plan.libtest_json);
assert_eq!(cargo_verdict::parse(&words), plan.request, "{line}");
assert!(!line.contains("json-render-diagnostics"));
}
#[test]
fn an_existing_runner_is_composed_into_the_scoped_runner() {
let host = cargo_verdict::plan::host_triple().unwrap();
let registry = default_registry();
// The scoped runner the plan adds for `cargo test -p x`, given the
// camp runner cargo would otherwise use.
let composed = |camp: &[&str]| {
let camp: Vec<String> = camp.iter().map(|w| w.to_string()).collect();
let script = cargo_verdict::plan::runner_script(&camp, &Default::default());
format!("target.{host}.runner={}", serde_json::to_string(&["sh", "-c", &script]).unwrap())
};
let config_of = |argv: &[String]| -> Vec<String> {
argv.windows(2).filter(|w| w[0] == "--config").map(|w| w[1].clone()).collect()
};
let unquote = |w: &str| w.strip_prefix('\'').and_then(|w| w.strip_suffix('\'')).map(|w| w.replace("'\\''", "'")).unwrap_or(w.to_string());
// In the run's cargo config: composed, not replaced.
let dir = runnerless_dir();
std::fs::create_dir_all(dir.path().join(".cargo")).unwrap();
std::fs::write(dir.path().join(".cargo/config.toml"), format!("[target.{host}]\nrunner = \"qemu -L /sysroot\"\n")).unwrap();
let ctx = RunContext { cwd: Some(dir.path()), env: &[] };
let result = registry.attach("cargo test -p x", &BeholderSelect::Auto, OutputReader::Raw, &ctx);
let got: Vec<String> = config_of(&result.argv).iter().map(|w| unquote(w)).collect();
assert_eq!(got, [composed(&["qemu", "-L", "/sysroot"])], "{:?}", result.argv);
// In the run's env: the same.
let clean = runnerless_dir();
let key = format!("CARGO_TARGET_{}_RUNNER", host.to_uppercase().replace(['-', '.'], "_"));
let env = [(key, "qemu".to_string())];
let ctx = RunContext { cwd: Some(clean.path()), env: &env };
let result = registry.attach("cargo test -p x", &BeholderSelect::Auto, OutputReader::Raw, &ctx);
let got: Vec<String> = config_of(&result.argv).iter().map(|w| unquote(w)).collect();
assert_eq!(got, [composed(&["qemu"])], "{:?}", result.argv);
// One the engine cannot resolve (two cfg tables match): no runner at
// all, so the camp's runs exactly as configured.
std::fs::write(
dir.path().join(".cargo/config.toml"),
"[target.'cfg(unix)']\nrunner = \"a\"\n[target.'cfg(not(windows))']\nrunner = \"b\"\n",
)
.unwrap();
let ctx = RunContext { cwd: Some(dir.path()), env: &[] };
let result = registry.attach("cargo test -p x", &BeholderSelect::Auto, OutputReader::Raw, &ctx);
assert_eq!(result.argv, cargo_argv("cargo test -p x --message-format json"));
}
#[test]
fn cargo_beholder_emits_test_events_per_test_and_per_binary() {
// A PTY run: cargo's stderr and the binaries' stdout in one stream.
let lines = [
" Running unittests src/lib.rs (target/debug/deps/mixed-1)",
r#"{ "type": "suite", "event": "started", "test_count": 3 }"#,
r#"{ "type": "test", "event": "started", "name": "t::bad" }"#,
r#"{ "type": "test", "name": "t::ign", "event": "ignored", "message": "slow" }"#,
r#"{ "type": "test", "name": "t::ok", "event": "ok", "exec_time": 0.001 }"#,
r#"{ "type": "test", "name": "t::bad", "event": "failed", "exec_time": 0.1, "stdout": "cap\n\nthread 't::bad' (1) panicked at src/lib.rs:17:9:\nboom\n" }"#,
r#"{ "type": "suite", "event": "failed", "passed": 1, "failed": 1, "ignored": 1, "measured": 0, "filtered_out": 0, "exec_time": 0.1 }"#,
"error: test failed, to rerun pass `--lib`",
" Doc-tests mixed",
"",
"running 1 test",
"test src/lib.rs - two (line 1) ... ok",
];
let mut b = CargoBeholder::default();
let mut events = Vec::new();
for (i, l) in lines.iter().enumerate() {
events.extend(b.parse_chunk(&combined_chunk(i as u32, &format!("{l}\n"))));
}
events.extend(b.on_done(&crate::types::TaskRunId::new(), 0));
let tests: Vec<&Event> = events.iter().filter(|e| e.target == "cargo::test").collect();
let outcome = |name: &str| {
tests.iter().find(|e| e.fields["test"]["name"] == name).map(|e| e.fields["test"]["outcome"].clone())
};
assert_eq!(outcome("t::ok"), Some("ok".into()));
assert_eq!(outcome("t::ign"), Some("ignored".into()));
let failed = tests.iter().find(|e| e.fields["test"]["outcome"] == "failed").unwrap();
assert_eq!(failed.level, Level::Error);
assert_eq!(failed.fields["file"]["path"], "src/lib.rs");
assert_eq!(failed.fields["file"]["line"], 17);
assert_eq!(failed.fields["test"]["binary"]["source"], "src/lib.rs");
assert!(failed.fields["test"]["output"].as_str().unwrap().contains("boom"));
assert_eq!(failed.anchor.as_ref().map(|a| a.seq), Some(5), "anchored to its chunk");
let suite = tests.iter().find(|e| e.fields["test"]["suite"].is_object()).unwrap();
assert_eq!(suite.fields["test"]["suite"]["failed"], 1);
// The doctest binary had no JSON, and says so instead of going quiet.
let unstructured = tests.iter().find(|e| e.fields["test"]["structured"] == false).unwrap();
assert_eq!(unstructured.fields["test"]["binary"]["kind"], "doctest");
assert!(b.unknown_format_reason().is_none(), "libtest lines are recognized, not unknown");
}
#[test]
fn a_lib_warning_reported_twice_is_one_event() {
let warn = r#"{"reason":"compiler-message","package_id":"path+file:///x#0.1.0","manifest_path":"/x/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"x","src_path":"/x/src/lib.rs","edition":"2021","doctest":true,"test":true,"doc":true},"message":{"$message_type":"diagnostic","message":"unused variable: `unused`","code":{"code":"unused_variables","explanation":null},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":0,"byte_end":1,"line_start":5,"line_end":5,"column_start":9,"column_end":15,"is_primary":true,"text":[],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"warning: unused variable"}}"#;
let mut b = CargoBeholder::default();
let events = b.parse_chunk(&dummy_chunk(format!("{warn}\n{warn}\n").as_bytes()));
assert_eq!(events.len(), 1);
assert_eq!(events[0].fields["rendered"], "warning: unused variable");
}
#[test]
fn cargo_factory_matches_check() {
let f = CargoBeholderFactory;
let argv: Vec<String> = vec!["cargo".into(), "check".into(), "--workspace".into()];
assert!(f.matches(&argv));
}
#[test]
fn cargo_factory_matches_clippy() {
let f = CargoBeholderFactory;
let argv: Vec<String> = vec!["cargo".into(), "clippy".into()];
assert!(f.matches(&argv));
}
#[test]
fn cargo_factory_declines_explicit_message_format() {
let f = CargoBeholderFactory;
let argv: Vec<String> =
vec!["cargo".into(), "check".into(), "--message-format=human".into()];
assert!(!f.matches(&argv));
}
#[test]
fn cargo_factory_declines_non_diag_subcommand() {
let f = CargoBeholderFactory;
let argv: Vec<String> = vec!["cargo".into(), "fmt".into()];
assert!(!f.matches(&argv));
}
#[test]
fn cargo_factory_declines_non_cargo() {
let f = CargoBeholderFactory;
let argv: Vec<String> = vec!["rustc".into(), "--edition=2021".into()];
assert!(!f.matches(&argv));
}
#[test]
fn cargo_factory_rewriter_adds_flag() {
let registry = default_registry();
let result = registry.attach("cargo check --workspace", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "cargo beholder must attach");
// `json`, not `json-render-diagnostics`: under the latter cargo
// prints diagnostics to stderr and emits no compiler-message lines
// (measured, R989-F20), so the events would never arrive.
assert_eq!(
result.argv,
["cargo", "check", "--workspace", "--message-format", "json"],
"rewriter must inject the JSON flag"
);
// Rewrite must be visible on status.
assert!(
result.status.text.contains("rewrite="),
"rewrite must be surfaced in status; got: {}",
result.status.text
);
}
#[test]
fn cargo_beholder_parses_compiler_error() {
let json = r#"{"reason":"compiler-message","package_id":"foo","manifest_path":"foo","target":{"kind":["lib"],"name":"foo","src_path":"src/lib.rs","edition":"2021","doctest":true,"test":true,"doc":true},"message":{"$message_type":"diagnostic","message":"mismatched types","code":{"code":"E0308","explanation":""},"level":"error","spans":[{"file_name":"src/lib.rs","byte_start":0,"byte_end":1,"line_start":5,"line_end":5,"column_start":1,"column_end":14,"is_primary":true,"text":[],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"error[E0308]: mismatched types\n"}}"#;
let chunk = dummy_chunk(format!("{json}\n").as_bytes());
let mut b = CargoBeholder::default();
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
let ev = &events[0];
assert_eq!(ev.target, "cargo::rustc");
assert_eq!(ev.level, Level::Error);
assert_eq!(ev.msg, "mismatched types");
assert_eq!(ev.fields["error"]["code"], "E0308");
assert_eq!(ev.fields["file"]["path"], "src/lib.rs");
assert_eq!(ev.fields["file"]["line"], 5u32);
}
#[test]
fn cargo_beholder_parses_warning() {
let json = r#"{"reason":"compiler-message","package_id":"foo","manifest_path":"foo","target":{"kind":["lib"],"name":"foo","src_path":"src/lib.rs","edition":"2021","doctest":true,"test":true,"doc":true},"message":{"$message_type":"diagnostic","message":"unused variable: `x`","code":{"code":"unused_variables","explanation":""},"level":"warning","spans":[{"file_name":"src/lib.rs","byte_start":0,"byte_end":1,"line_start":10,"line_end":10,"column_start":9,"column_end":10,"is_primary":true,"text":[],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"warning: unused variable"}}"#;
let chunk = dummy_chunk(format!("{json}\n").as_bytes());
let mut b = CargoBeholder::default();
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, Level::Warn);
assert_eq!(events[0].fields["file"]["line"], 10u32);
}
#[test]
fn cargo_beholder_parses_build_finished_success() {
let json = r#"{"reason":"build-finished","success":true}"#;
let chunk = dummy_chunk(format!("{json}\n").as_bytes());
let mut b = CargoBeholder::default();
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, Level::Info);
assert_eq!(events[0].msg, "build finished");
}
#[test]
fn cargo_beholder_parses_build_finished_failure() {
let json = r#"{"reason":"build-finished","success":false}"#;
let chunk = dummy_chunk(format!("{json}\n").as_bytes());
let mut b = CargoBeholder::default();
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, Level::Error);
assert_eq!(events[0].msg, "build failed");
}
#[test]
fn cargo_beholder_skips_artifacts_and_unknown() {
// compiler-artifact and build-script-executed should produce no events
let artifact = r#"{"reason":"compiler-artifact","package_id":"foo","manifest_path":"foo","target":{"kind":["lib"],"name":"foo","src_path":"src/lib.rs","edition":"2021","doctest":true,"test":true,"doc":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":[],"executable":null,"fresh":false}"#;
let script = r#"{"reason":"build-script-executed","package_id":"foo 0.1.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"/tmp"}"#;
let mut b = CargoBeholder::default();
let chunk = dummy_chunk(format!("{artifact}\n{script}\n").as_bytes());
let events = b.parse_chunk(&chunk);
assert!(events.is_empty(), "got: {events:?}");
}
// ─── R989-F24: the Bash door (a reader that strips `{"reason":` lines) ───
const GATED: &str = "--message-format=json-render-diagnostics";
/// R989-F37: the daemon never rewrites a build-run line any more; the
/// gate puts the flag on the argv it spawns, and a stripping reader's
/// beholder attaches parse-only to a run whose argv already carries it.
#[test]
fn a_stripping_reader_attaches_parse_only_to_an_argv_the_gate_flagged() {
let registry = default_registry();
let cmd = format!("cargo check -p yah {GATED}");
let r = registry.attach(&cmd, &BeholderSelect::Auto, OutputReader::StripsReasonLines, &RunContext::default());
assert!(r.beholder.is_some());
assert!(r.status.rewrite_added.is_none(), "{}", r.status.text);
assert!(r.status.text.starts_with("attached:cargo@2 door=bash"), "{}", r.status.text);
assert_eq!(r.argv, ["cargo", "check", "-p", "yah", GATED]);
}
/// The flag counts only among cargo's own args, never after `--`.
#[test]
fn a_flag_after_the_double_dash_is_the_programs_not_the_gates() {
let registry = default_registry();
let ours = registry.attach(
&format!("cargo test -p x {GATED} -- --nocapture"),
&BeholderSelect::Auto,
OutputReader::StripsReasonLines,
&RunContext::default(),
);
assert!(ours.beholder.is_some());
assert_eq!(ours.argv, ["cargo", "test", "-p", "x", GATED, "--", "--nocapture"]);
let theirs = registry.attach(
&format!("cargo test -p x -- {GATED}"),
&BeholderSelect::Auto,
OutputReader::StripsReasonLines,
&RunContext::default(),
);
assert!(theirs.beholder.is_none(), "{}", theirs.status.text);
}
/// No rewrite for any reader: an unflagged line (the outer relocated run,
/// whose cargo is the gate's), a user's own `--message-format`, a terminal
/// tile and a non-cargo tool all stay exactly as written.
#[test]
fn a_stripping_reader_rewrites_nothing() {
let registry = default_registry();
for line in ["cargo check", "cargo check --message-format=json", "vitest --run"] {
let r = registry.attach(line, &BeholderSelect::Auto, OutputReader::StripsReasonLines, &RunContext::default());
assert!(r.status.rewrite_added.is_none(), "{line}: {}", r.status.text);
assert_eq!(r.argv, line.split(' ').collect::<Vec<_>>(), "{line}");
}
let plain = registry.attach("cargo check", &BeholderSelect::Auto, OutputReader::StripsReasonLines, &RunContext::default());
assert!(plain.beholder.is_none());
let tile = registry.attach("cargo check", &BeholderSelect::Auto, OutputReader::Verbatim, &RunContext::default());
assert!(tile.beholder.is_none());
assert!(tile.status.text.contains("verbatim-output"), "{}", tile.status.text);
}
/// What `task.diagnostics` gets from a Bash-door run, recorded honestly: no
/// compiler diagnostics (cargo renders them as text under this format), so
/// the build-finished event says so and carries the unit counts.
#[test]
fn a_bash_door_build_finished_event_says_diagnostics_are_unstructured() {
let fresh = r#"{"reason":"compiler-artifact","package_id":"foo","manifest_path":"foo","target":{"kind":["lib"],"name":"foo","src_path":"src/lib.rs","edition":"2021","doctest":true,"test":true,"doc":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":[],"executable":null,"fresh":true}"#;
let built = fresh.replace(r#""fresh":true"#, r#""fresh":false"#);
let done = r#"{"reason":"build-finished","success":false}"#;
let mut b = CargoBeholder::for_reason_stripping();
let events = b.parse_chunk(&dummy_chunk(format!("{fresh}\n{built}\n{done}\n").as_bytes()));
assert_eq!(events.len(), 1, "{events:?}");
let e = &events[0];
assert_eq!(e.level, Level::Error);
assert!(e.msg.starts_with("build failed; compiler diagnostics are not structured on the Bash door"), "{}", e.msg);
assert_eq!(e.fields["build"]["diagnostics"], "unstructured");
assert_eq!(e.fields["build"]["units"]["fresh"], 1);
assert_eq!(e.fields["build"]["units"]["compiled"], 1);
// The full rewrite's beholder makes no such claim.
let mut full = CargoBeholder::default();
let ev = full.parse_chunk(&dummy_chunk(format!("{done}\n").as_bytes()));
assert_eq!(ev[0].msg, "build failed");
assert!(ev[0].fields["build"].get("diagnostics").is_none());
}
#[test]
fn cargo_beholder_buffers_partial_lines() {
let json = r#"{"reason":"build-finished","success":true}"#;
let half = json.len() / 2;
let mut b = CargoBeholder::default();
// First half — no newline yet, no events
let chunk1 = dummy_chunk(json[..half].as_bytes());
let ev1 = b.parse_chunk(&chunk1);
assert!(ev1.is_empty(), "should buffer partial line");
// Second half + newline — now the event appears
let chunk2 = dummy_chunk(format!("{}\n", &json[half..]).as_bytes());
let ev2 = b.parse_chunk(&chunk2);
assert_eq!(ev2.len(), 1);
assert_eq!(ev2[0].msg, "build finished");
}
// ─── Schema versioning + unknown-format fallback tests ────────────────────
#[test]
fn cargo_beholder_unknown_format_reason_none_initially() {
let b = CargoBeholder::default();
assert!(b.unknown_format_reason().is_none(), "fresh beholder must not flag unknown format");
}
#[test]
fn cargo_factory_declares_tool_version_range() {
let f = CargoBeholderFactory;
let range = f.tool_version_range().expect("cargo factory must declare a version range");
assert_eq!(range.min, Some("1.63.0"), "min version is --config's stabilisation");
assert!(range.max.is_none(), "no upper bound declared");
}
#[test]
fn cargo_beholder_format_probe_fires_after_limit() {
// Feed FORMAT_PROBE_LIMIT JSON-object lines that lack a `reason` field.
let bad_line = r#"{"not_reason":"something","value":42}"#;
let input: String = (0..super::FORMAT_PROBE_LIMIT)
.map(|_| format!("{bad_line}\n"))
.collect();
let chunk = dummy_chunk(input.as_bytes());
let mut b = CargoBeholder::default();
let events = b.parse_chunk(&chunk);
assert!(events.is_empty(), "no events from unrecognized lines");
let reason = b.unknown_format_reason();
assert!(
reason.is_some(),
"unknown_format_reason must be Some after {FORMAT_PROBE_LIMIT} unrecognized JSON lines"
);
assert!(
reason.unwrap().contains("no recognizable cargo or libtest JSON"),
"reason text must describe the problem; got: {:?}",
reason
);
}
#[test]
fn cargo_beholder_format_probe_not_triggered_below_limit() {
// One fewer than the limit — not yet flagged.
let bad_line = r#"{"not_reason":"something"}"#;
let input: String = (0..super::FORMAT_PROBE_LIMIT - 1)
.map(|_| format!("{bad_line}\n"))
.collect();
let chunk = dummy_chunk(input.as_bytes());
let mut b = CargoBeholder::default();
b.parse_chunk(&chunk);
assert!(
b.unknown_format_reason().is_none(),
"must not flag unknown format before probe limit"
);
}
#[test]
fn cargo_beholder_format_probe_suppressed_after_recognized_line() {
// One valid cargo line clears the probe; subsequent bad lines are silently dropped.
let good = r#"{"reason":"build-finished","success":true}"#;
let bad_line = r#"{"not_reason":"something"}"#;
// Build more bad lines than the threshold.
let mut input = format!("{good}\n");
for _ in 0..super::FORMAT_PROBE_LIMIT + 2 {
input.push_str(bad_line);
input.push('\n');
}
let chunk = dummy_chunk(input.as_bytes());
let mut b = CargoBeholder::default();
let events = b.parse_chunk(&chunk);
// The good line produces one event.
assert_eq!(events.len(), 1, "expected one event from the recognized line");
assert_eq!(events[0].msg, "build finished");
assert!(
b.unknown_format_reason().is_none(),
"format probe must not fire after a recognized line has been seen"
);
}
#[test]
fn cargo_beholder_stops_emitting_after_unknown_format() {
// Trigger unknown format, then verify subsequent chunks produce no events.
let bad_line = r#"{"not_reason":"x"}"#;
let trigger: String = (0..super::FORMAT_PROBE_LIMIT)
.map(|_| format!("{bad_line}\n"))
.collect();
let mut b = CargoBeholder::default();
b.parse_chunk(&dummy_chunk(trigger.as_bytes()));
assert!(b.unknown_format_reason().is_some(), "format must be flagged");
// Now feed a valid cargo line — must produce no events (beholder silenced).
let valid = r#"{"reason":"build-finished","success":true}"#.to_string() + "\n";
let events = b.parse_chunk(&dummy_chunk(valid.as_bytes()));
assert!(events.is_empty(), "beholder must not emit events after unknown_format is set");
}
#[test]
fn cargo_beholder_non_json_lines_dont_count_toward_probe() {
// Non-JSON lines (and empty lines) are skipped; they must not consume probe budget.
let mut input = String::new();
// More non-JSON lines than FORMAT_PROBE_LIMIT.
for _ in 0..super::FORMAT_PROBE_LIMIT + 3 {
input.push_str(" warning: some human text\n");
}
// Then a valid cargo line.
input.push_str(r#"{"reason":"build-finished","success":true}"#);
input.push('\n');
let mut b = CargoBeholder::default();
let events = b.parse_chunk(&dummy_chunk(input.as_bytes()));
assert_eq!(events.len(), 1, "valid cargo line must still produce an event");
assert!(b.unknown_format_reason().is_none(), "non-JSON lines must not count toward probe");
}
// ─── TscBeholderFactory tests ─────────────────────────────────────────────
#[test]
fn tsc_factory_matches_bare_tsc() {
let f = TscBeholderFactory;
let argv: Vec<String> = vec!["tsc".into(), "--noEmit".into()];
assert!(f.matches(&argv));
}
#[test]
fn tsc_factory_matches_tsc_with_project() {
let f = TscBeholderFactory;
let argv: Vec<String> = vec!["tsc".into(), "-p".into(), "tsconfig.json".into()];
assert!(f.matches(&argv));
}
#[test]
fn tsc_factory_matches_when_pretty_false_already_set() {
// Should still attach (rewriter becomes a no-op; we still parse).
let f = TscBeholderFactory;
let argv: Vec<String> = vec!["tsc".into(), "--noEmit".into(), "--pretty=false".into()];
assert!(f.matches(&argv));
}
#[test]
fn tsc_factory_declines_pretty_true() {
let f = TscBeholderFactory;
let argv: Vec<String> = vec!["tsc".into(), "--pretty=true".into()];
assert!(!f.matches(&argv));
}
#[test]
fn tsc_factory_declines_bare_pretty() {
let f = TscBeholderFactory;
let argv: Vec<String> = vec!["tsc".into(), "--pretty".into()];
assert!(!f.matches(&argv));
}
#[test]
fn tsc_factory_declines_version() {
let f = TscBeholderFactory;
let argv: Vec<String> = vec!["tsc".into(), "--version".into()];
assert!(!f.matches(&argv));
}
#[test]
fn tsc_factory_declines_init() {
let f = TscBeholderFactory;
let argv: Vec<String> = vec!["tsc".into(), "--init".into()];
assert!(!f.matches(&argv));
}
#[test]
fn tsc_factory_declines_non_tsc_command() {
let f = TscBeholderFactory;
let argv: Vec<String> = vec!["node".into(), "build.js".into()];
assert!(!f.matches(&argv));
}
#[test]
fn tsc_factory_rewriter_adds_pretty_false() {
let registry = default_registry();
let result = registry.attach("tsc --noEmit", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "tsc beholder must attach");
assert!(
result.argv.contains(&"--pretty=false".to_string()),
"rewriter must inject --pretty=false; got: {:?}",
result.argv
);
assert!(
result.status.text.contains("rewrite="),
"rewrite must be surfaced in status; got: {}",
result.status.text
);
}
#[test]
fn tsc_factory_no_rewrite_when_pretty_false_present() {
let registry = default_registry();
let result = registry.attach("tsc --noEmit --pretty=false", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "tsc beholder must attach");
// Argv already had --pretty=false; rewriter is a no-op, no duplicate added.
let count = result.argv.iter().filter(|a| a.as_str() == "--pretty=false").count();
assert_eq!(count, 1, "--pretty=false must appear exactly once; got: {:?}", result.argv);
// No rewrite_added because the flag was already present.
assert!(
result.status.rewrite_added.as_ref().map(|v| v.is_empty()).unwrap_or(true),
"rewrite_added must be empty when flag already present"
);
}
#[test]
fn tsc_beholder_parses_error_line() {
let line = "src/foo.ts(10,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.\n";
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(line.as_bytes()));
assert_eq!(events.len(), 1);
let ev = &events[0];
assert_eq!(ev.target, "tsc");
assert_eq!(ev.level, Level::Error);
assert_eq!(ev.msg, "Argument of type 'string' is not assignable to parameter of type 'number'.");
assert_eq!(ev.fields["error"]["code"], "TS2345");
assert_eq!(ev.fields["file"]["path"], "src/foo.ts");
assert_eq!(ev.fields["file"]["line"], 10u32);
assert_eq!(ev.fields["file"]["col"], 5u32);
}
#[test]
fn tsc_beholder_parses_warning_line() {
let line = "src/bar.tsx(42,3): warning TS6133: 'x' is declared but its value is never read.\n";
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(line.as_bytes()));
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, Level::Warn);
assert_eq!(events[0].fields["error"]["code"], "TS6133");
assert_eq!(events[0].fields["file"]["path"], "src/bar.tsx");
assert_eq!(events[0].fields["file"]["line"], 42u32);
assert_eq!(events[0].fields["file"]["col"], 3u32);
}
#[test]
fn tsc_beholder_parses_summary_with_errors() {
let line = "Found 2 errors.\n";
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(line.as_bytes()));
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, Level::Error);
assert_eq!(events[0].fields["build"]["errors"], 2u32);
}
#[test]
fn tsc_beholder_parses_summary_zero_errors() {
let line = "Found 0 errors.\n";
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(line.as_bytes()));
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, Level::Info);
assert_eq!(events[0].fields["build"]["errors"], 0u32);
}
#[test]
fn tsc_beholder_parses_summary_in_n_files() {
let line = "Found 3 errors in 2 files.\n";
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(line.as_bytes()));
assert_eq!(events.len(), 1);
assert_eq!(events[0].fields["build"]["errors"], 3u32);
}
#[test]
fn tsc_beholder_skips_watch_mode_header() {
let line = "[12:00:00 AM] Starting compilation in watch mode...\n";
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(line.as_bytes()));
assert!(events.is_empty(), "watch-mode header must be skipped");
}
#[test]
fn tsc_beholder_skips_blank_lines() {
let input = "\n\n";
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(input.as_bytes()));
assert!(events.is_empty());
}
#[test]
fn tsc_beholder_buffers_partial_lines() {
let line = "src/foo.ts(1,1): error TS2304: Cannot find name 'foo'.";
let half = line.len() / 2;
let mut b = TscBeholder::default();
let ev1 = b.parse_chunk(&dummy_chunk(line[..half].as_bytes()));
assert!(ev1.is_empty(), "partial line must not produce events");
let ev2 = b.parse_chunk(&dummy_chunk(format!("{}\n", &line[half..]).as_bytes()));
assert_eq!(ev2.len(), 1, "complete line must produce one event");
assert_eq!(ev2[0].fields["error"]["code"], "TS2304");
}
#[test]
fn tsc_beholder_multiple_errors_in_one_chunk() {
let input = concat!(
"src/a.ts(1,1): error TS2304: Cannot find name 'a'.\n",
"src/b.ts(2,3): error TS2304: Cannot find name 'b'.\n",
"Found 2 errors.\n",
);
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(input.as_bytes()));
assert_eq!(events.len(), 3, "two diagnostics + one summary");
assert_eq!(events[0].fields["file"]["path"], "src/a.ts");
assert_eq!(events[1].fields["file"]["path"], "src/b.ts");
assert_eq!(events[2].fields["build"]["errors"], 2u32);
}
#[test]
fn tsc_beholder_full_watch_cycle() {
let input = concat!(
"[12:00:00 AM] Starting compilation in watch mode...\n",
"\n",
"src/foo.ts(5,9): error TS2345: wrong type.\n",
"\n",
"[12:00:01 AM] Found 1 error. Watching for file changes.\n",
);
let mut b = TscBeholder::default();
let events = b.parse_chunk(&dummy_chunk(input.as_bytes()));
// Only the diagnostic line produces an event; watch headers are skipped.
assert_eq!(events.len(), 1, "only diagnostic line should produce event; got: {events:?}");
assert_eq!(events[0].level, Level::Error);
assert_eq!(events[0].fields["file"]["path"], "src/foo.ts");
}
#[test]
fn tsc_factory_declares_tool_version_range() {
let f = TscBeholderFactory;
let range = f.tool_version_range().expect("tsc factory must declare a version range");
assert_eq!(range.min, Some("3.0.0"));
assert!(range.max.is_none());
}
// ─── EslintBeholderFactory tests ─────────────────────────────────────────
#[test]
fn eslint_factory_matches_bare_eslint() {
let f = EslintBeholderFactory;
let argv: Vec<String> = vec!["eslint".into(), "src/".into()];
assert!(f.matches(&argv));
}
#[test]
fn eslint_factory_matches_when_format_json_already_set() {
let f = EslintBeholderFactory;
let argv: Vec<String> = vec!["eslint".into(), "--format=json".into(), "src/".into()];
assert!(f.matches(&argv), "must match when --format=json already present");
}
#[test]
fn eslint_factory_declines_non_json_format() {
let f = EslintBeholderFactory;
let argv: Vec<String> = vec!["eslint".into(), "--format=compact".into(), "src/".into()];
assert!(!f.matches(&argv));
}
#[test]
fn eslint_factory_declines_version() {
let f = EslintBeholderFactory;
let argv: Vec<String> = vec!["eslint".into(), "--version".into()];
assert!(!f.matches(&argv));
}
#[test]
fn eslint_factory_declines_env_info() {
let f = EslintBeholderFactory;
let argv: Vec<String> = vec!["eslint".into(), "--env-info".into()];
assert!(!f.matches(&argv));
}
#[test]
fn eslint_factory_declines_non_eslint_command() {
let f = EslintBeholderFactory;
let argv: Vec<String> = vec!["tsc".into(), "--noEmit".into()];
assert!(!f.matches(&argv));
}
#[test]
fn eslint_factory_rewriter_adds_format_json() {
let registry = default_registry();
let result = registry.attach("eslint src/", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "eslint beholder must attach");
assert!(
result.argv.contains(&"--format=json".to_string()),
"rewriter must inject --format=json; got: {:?}",
result.argv
);
assert!(
result.status.text.contains("rewrite="),
"rewrite must be surfaced in status; got: {}",
result.status.text
);
}
#[test]
fn eslint_factory_no_rewrite_when_format_json_present() {
let registry = default_registry();
let result = registry.attach("eslint --format=json src/", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "eslint beholder must attach");
let count = result.argv.iter().filter(|a| a.as_str() == "--format=json").count();
assert_eq!(count, 1, "--format=json must appear exactly once; got: {:?}", result.argv);
}
#[test]
fn eslint_beholder_parse_chunk_buffers_only() {
let json = r#"[{"filePath":"/src/a.js","messages":[]}]"#;
let mut b = EslintBeholder::default();
let events = b.parse_chunk(&dummy_chunk(json.as_bytes()));
assert!(events.is_empty(), "parse_chunk must not emit events; ESLint parses at EOF");
}
#[test]
fn eslint_beholder_on_done_parses_errors_and_warnings() {
use crate::types::TaskRunId;
let json = r#"[
{
"filePath": "/src/foo.ts",
"messages": [
{
"ruleId": "no-unused-vars",
"severity": 2,
"message": "'x' is defined but never used.",
"line": 10,
"column": 5
},
{
"ruleId": "no-console",
"severity": 1,
"message": "Unexpected console statement.",
"line": 20,
"column": 1
}
],
"errorCount": 1,
"warningCount": 1
}
]"#;
let mut b = EslintBeholder::default();
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let run_id = TaskRunId::new();
let events = b.on_done(&run_id, 0);
assert_eq!(events.len(), 2);
let err = &events[0];
assert_eq!(err.level, Level::Error);
assert_eq!(err.target, "eslint");
assert_eq!(err.msg, "'x' is defined but never used.");
assert_eq!(err.fields["error"]["code"], "no-unused-vars");
assert_eq!(err.fields["file"]["path"], "/src/foo.ts");
assert_eq!(err.fields["file"]["line"], 10u32);
assert_eq!(err.fields["file"]["col"], 5u32);
let warn = &events[1];
assert_eq!(warn.level, Level::Warn);
assert_eq!(warn.fields["error"]["code"], "no-console");
}
#[test]
fn eslint_beholder_on_done_empty_output() {
use crate::types::TaskRunId;
let mut b = EslintBeholder::default();
let events = b.on_done(&TaskRunId::new(), 0);
assert!(events.is_empty(), "empty buffer must produce no events");
}
#[test]
fn eslint_beholder_on_done_invalid_json_flags_unknown_format() {
use crate::types::TaskRunId;
let bad = b"not json at all";
let mut b = EslintBeholder::default();
b.parse_chunk(&dummy_chunk(bad));
let events = b.on_done(&TaskRunId::new(), 0);
assert!(events.is_empty(), "bad JSON must not produce events");
assert!(
b.unknown_format_reason().is_some(),
"bad JSON must set unknown_format_reason"
);
}
#[test]
fn eslint_beholder_on_done_chunks_split_across_multiple_calls() {
use crate::types::TaskRunId;
let json = r#"[{"filePath":"/a.ts","messages":[{"ruleId":"eqeqeq","severity":2,"message":"Use ===.","line":3,"column":7}],"errorCount":1,"warningCount":0}]"#;
let mid = json.len() / 2;
let mut b = EslintBeholder::default();
b.parse_chunk(&dummy_chunk(json[..mid].as_bytes()));
b.parse_chunk(&dummy_chunk(json[mid..].as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1, "split-chunk delivery must still parse correctly");
assert_eq!(events[0].fields["error"]["code"], "eqeqeq");
}
#[test]
fn eslint_factory_declares_tool_version_range() {
let f = EslintBeholderFactory;
let range = f.tool_version_range().expect("eslint factory must declare a version range");
assert_eq!(range.min, Some("8.0.0"));
assert!(range.max.is_none());
}
// ─── BiomeBeholderFactory tests ───────────────────────────────────────────
#[test]
fn biome_factory_matches_check() {
let f = BiomeBeholderFactory;
let argv: Vec<String> = vec!["biome".into(), "check".into(), "src/".into()];
assert!(f.matches(&argv));
}
#[test]
fn biome_factory_matches_lint() {
let f = BiomeBeholderFactory;
let argv: Vec<String> = vec!["biome".into(), "lint".into(), "src/".into()];
assert!(f.matches(&argv));
}
#[test]
fn biome_factory_matches_ci() {
let f = BiomeBeholderFactory;
let argv: Vec<String> = vec!["biome".into(), "ci".into(), "src/".into()];
assert!(f.matches(&argv));
}
#[test]
fn biome_factory_declines_format_subcommand() {
let f = BiomeBeholderFactory;
let argv: Vec<String> = vec!["biome".into(), "format".into(), "src/".into()];
assert!(!f.matches(&argv));
}
#[test]
fn biome_factory_declines_version() {
let f = BiomeBeholderFactory;
let argv: Vec<String> = vec!["biome".into(), "--version".into()];
assert!(!f.matches(&argv));
}
#[test]
fn biome_factory_declines_non_json_reporter() {
let f = BiomeBeholderFactory;
let argv: Vec<String> = vec!["biome".into(), "check".into(), "--reporter=github".into()];
assert!(!f.matches(&argv));
}
#[test]
fn biome_factory_matches_when_reporter_json_present() {
let f = BiomeBeholderFactory;
let argv: Vec<String> = vec!["biome".into(), "check".into(), "--reporter=json".into()];
assert!(f.matches(&argv), "must match when --reporter=json already set");
}
#[test]
fn biome_factory_declines_non_biome_command() {
let f = BiomeBeholderFactory;
let argv: Vec<String> = vec!["eslint".into(), "src/".into()];
assert!(!f.matches(&argv));
}
#[test]
fn biome_factory_rewriter_adds_reporter_json() {
let registry = default_registry();
let result = registry.attach("biome check src/", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "biome beholder must attach");
assert!(
result.argv.contains(&"--reporter=json".to_string()),
"rewriter must inject --reporter=json; got: {:?}",
result.argv
);
assert!(
result.status.text.contains("rewrite="),
"rewrite must be surfaced in status; got: {}",
result.status.text
);
}
#[test]
fn biome_factory_no_rewrite_when_reporter_json_present() {
let registry = default_registry();
let result = registry.attach("biome lint --reporter=json src/", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "biome beholder must attach");
let count = result.argv.iter().filter(|a| a.as_str() == "--reporter=json").count();
assert_eq!(count, 1, "--reporter=json must appear exactly once; got: {:?}", result.argv);
}
#[test]
fn biome_beholder_parse_chunk_buffers_only() {
let json = r#"{"diagnostics":[],"summary":{}}"#;
let mut b = BiomeBeholder::default();
let events = b.parse_chunk(&dummy_chunk(json.as_bytes()));
assert!(events.is_empty(), "parse_chunk must not emit events; Biome parses at EOF");
}
#[test]
fn biome_beholder_on_done_parses_error() {
use crate::types::TaskRunId;
let json = r#"{
"diagnostics": [
{
"category": "lint/suspicious/noDoubleEquals",
"severity": "error",
"description": "Use === instead of ==",
"location": {
"path": {"file": "src/foo.ts"}
}
}
],
"summary": {"changed": 0, "unchanged": 1, "errors": 1}
}"#;
let mut b = BiomeBeholder::default();
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1);
let ev = &events[0];
assert_eq!(ev.level, Level::Error);
assert_eq!(ev.target, "biome");
assert_eq!(ev.msg, "Use === instead of ==");
assert_eq!(ev.fields["error"]["code"], "lint/suspicious/noDoubleEquals");
assert_eq!(ev.fields["file"]["path"], "src/foo.ts");
}
#[test]
fn biome_beholder_on_done_parses_warning() {
use crate::types::TaskRunId;
let json = r#"{
"diagnostics": [
{
"category": "lint/style/useConst",
"severity": "warning",
"description": "Prefer const over let.",
"location": {"path": {"file": "src/bar.ts"}}
}
],
"summary": {}
}"#;
let mut b = BiomeBeholder::default();
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, Level::Warn);
assert_eq!(events[0].fields["file"]["path"], "src/bar.ts");
}
#[test]
fn biome_beholder_on_done_empty_diagnostics() {
use crate::types::TaskRunId;
let json = r#"{"diagnostics":[],"summary":{"changed":0,"unchanged":5,"errors":0}}"#;
let mut b = BiomeBeholder::default();
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
assert!(events.is_empty(), "zero diagnostics must produce no events");
}
#[test]
fn biome_beholder_on_done_invalid_json_flags_unknown_format() {
use crate::types::TaskRunId;
let mut b = BiomeBeholder::default();
b.parse_chunk(&dummy_chunk(b"not json"));
let events = b.on_done(&TaskRunId::new(), 0);
assert!(events.is_empty());
assert!(b.unknown_format_reason().is_some());
}
#[test]
fn biome_factory_declares_tool_version_range() {
let f = BiomeBeholderFactory;
let range = f.tool_version_range().expect("biome factory must declare a version range");
assert_eq!(range.min, Some("1.0.0"));
assert!(range.max.is_none());
}
// ─── VitestBeholderFactory tests ──────────────────────────────────────────
#[test]
fn vitest_factory_matches_bare_vitest() {
let f = VitestBeholderFactory;
let argv: Vec<String> = vec!["vitest".into(), "--run".into()];
assert!(f.matches(&argv));
}
#[test]
fn vitest_factory_matches_with_reporter_json_already_set() {
let f = VitestBeholderFactory;
let argv: Vec<String> = vec!["vitest".into(), "--reporter=json".into()];
assert!(f.matches(&argv), "must match when --reporter=json already present");
}
#[test]
fn vitest_factory_declines_non_json_reporter_eq() {
let f = VitestBeholderFactory;
let argv: Vec<String> = vec!["vitest".into(), "--reporter=verbose".into()];
assert!(!f.matches(&argv));
}
#[test]
fn vitest_factory_declines_non_json_reporter_space() {
let f = VitestBeholderFactory;
let argv: Vec<String> = vec!["vitest".into(), "--reporter".into(), "verbose".into()];
assert!(!f.matches(&argv));
}
#[test]
fn vitest_factory_declines_version() {
let f = VitestBeholderFactory;
let argv: Vec<String> = vec!["vitest".into(), "--version".into()];
assert!(!f.matches(&argv));
}
#[test]
fn vitest_factory_declines_non_vitest_command() {
let f = VitestBeholderFactory;
let argv: Vec<String> = vec!["jest".into(), "--run".into()];
assert!(!f.matches(&argv));
}
#[test]
fn vitest_factory_rewriter_adds_reporter_json() {
let registry = default_registry();
let result = registry.attach("vitest --run", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "vitest beholder must attach");
assert!(
result.argv.contains(&"--reporter=json".to_string()),
"rewriter must inject --reporter=json; got: {:?}",
result.argv
);
assert!(
result.status.text.contains("rewrite="),
"rewrite must be surfaced in status; got: {}",
result.status.text
);
}
#[test]
fn vitest_factory_no_rewrite_when_reporter_json_present() {
let registry = default_registry();
let result = registry.attach("vitest --reporter=json --run", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some());
let count = result.argv.iter().filter(|a| a.as_str() == "--reporter=json").count();
assert_eq!(count, 1, "--reporter=json must appear exactly once; got: {:?}", result.argv);
}
#[test]
fn vitest_factory_declares_tool_version_range() {
let f = VitestBeholderFactory;
let range = f.tool_version_range().expect("vitest factory must declare a version range");
assert_eq!(range.min, Some("1.0.0"));
assert!(range.max.is_none());
}
// ─── JestBeholderFactory tests ────────────────────────────────────────────
#[test]
fn jest_factory_matches_bare_jest() {
let f = JestBeholderFactory;
let argv: Vec<String> = vec!["jest".into()];
assert!(f.matches(&argv));
}
#[test]
fn jest_factory_matches_jest_with_path() {
let f = JestBeholderFactory;
let argv: Vec<String> = vec!["jest".into(), "src/foo.test.ts".into()];
assert!(f.matches(&argv));
}
#[test]
fn jest_factory_matches_when_json_already_set() {
let f = JestBeholderFactory;
let argv: Vec<String> = vec!["jest".into(), "--json".into()];
assert!(f.matches(&argv), "must match when --json already present");
}
#[test]
fn jest_factory_declines_version() {
let f = JestBeholderFactory;
let argv: Vec<String> = vec!["jest".into(), "--version".into()];
assert!(!f.matches(&argv));
}
#[test]
fn jest_factory_declines_output_file() {
let f = JestBeholderFactory;
let argv: Vec<String> = vec!["jest".into(), "--outputFile=results.json".into()];
assert!(!f.matches(&argv), "must decline when --outputFile is set (JSON goes to file)");
}
#[test]
fn jest_factory_declines_non_jest_command() {
let f = JestBeholderFactory;
let argv: Vec<String> = vec!["vitest".into()];
assert!(!f.matches(&argv));
}
#[test]
fn jest_factory_rewriter_adds_json_flag() {
let registry = default_registry();
let result = registry.attach("jest src/", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "jest beholder must attach");
assert!(
result.argv.contains(&"--json".to_string()),
"rewriter must inject --json; got: {:?}",
result.argv
);
assert!(
result.status.text.contains("rewrite="),
"rewrite must be surfaced in status; got: {}",
result.status.text
);
}
#[test]
fn jest_factory_no_rewrite_when_json_present() {
let registry = default_registry();
let result = registry.attach("jest --json src/", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some());
let count = result.argv.iter().filter(|a| a.as_str() == "--json").count();
assert_eq!(count, 1, "--json must appear exactly once; got: {:?}", result.argv);
}
#[test]
fn jest_factory_declares_tool_version_range() {
let f = JestBeholderFactory;
let range = f.tool_version_range().expect("jest factory must declare a version range");
assert_eq!(range.min, Some("27.0.0"));
assert!(range.max.is_none());
}
// ─── BunTestBeholderFactory tests ─────────────────────────────────────────
#[test]
fn bun_test_factory_matches_bun_test() {
let f = BunTestBeholderFactory;
let argv: Vec<String> = vec!["bun".into(), "test".into()];
assert!(f.matches(&argv));
}
#[test]
fn bun_test_factory_matches_bun_test_with_path() {
let f = BunTestBeholderFactory;
let argv: Vec<String> = vec!["bun".into(), "test".into(), "src/".into()];
assert!(f.matches(&argv));
}
#[test]
fn bun_test_factory_matches_when_reporter_json_already_set() {
let f = BunTestBeholderFactory;
let argv: Vec<String> = vec!["bun".into(), "test".into(), "--reporter=json".into()];
assert!(f.matches(&argv));
}
#[test]
fn bun_test_factory_declines_bare_bun() {
let f = BunTestBeholderFactory;
let argv: Vec<String> = vec!["bun".into(), "run".into(), "build.ts".into()];
assert!(!f.matches(&argv));
}
#[test]
fn bun_test_factory_declines_non_json_reporter() {
let f = BunTestBeholderFactory;
let argv: Vec<String> = vec!["bun".into(), "test".into(), "--reporter=junit".into()];
assert!(!f.matches(&argv));
}
#[test]
fn bun_test_factory_declines_version() {
let f = BunTestBeholderFactory;
let argv: Vec<String> = vec!["bun".into(), "test".into(), "--version".into()];
assert!(!f.matches(&argv));
}
#[test]
fn bun_test_factory_rewriter_adds_reporter_json() {
let registry = default_registry();
let result = registry.attach("bun test", &BeholderSelect::Auto, OutputReader::Raw, &RunContext::default());
assert!(result.beholder.is_some(), "bun-test beholder must attach");
assert!(
result.argv.contains(&"--reporter=json".to_string()),
"rewriter must inject --reporter=json; got: {:?}",
result.argv
);
}
#[test]
fn bun_test_factory_declares_tool_version_range() {
let f = BunTestBeholderFactory;
let range = f.tool_version_range().expect("bun-test factory must declare a version range");
assert_eq!(range.min, Some("1.0.0"));
assert!(range.max.is_none());
}
// ─── JsTestBeholder on_done tests ─────────────────────────────────────────
fn js_test_report_json(success: bool, passed: u32, failed: u32) -> String {
format!(
r#"{{
"success": {success},
"numTotalTests": {total},
"numPassedTests": {passed},
"numFailedTests": {failed},
"numPendingTests": 0,
"testResults": []
}}"#,
total = passed + failed,
)
}
#[test]
fn js_test_beholder_on_done_success_summary() {
use crate::types::TaskRunId;
let json = js_test_report_json(true, 5, 0);
let mut b = JsTestBeholder::new("vitest", "1.0", vitest_adjust_argv);
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1, "success run must produce one summary event");
let ev = &events[0];
assert_eq!(ev.target, "vitest");
assert_eq!(ev.level, Level::Info);
assert_eq!(ev.msg, "5 passed");
assert_eq!(ev.fields["build"]["success"], true);
assert_eq!(ev.fields["test"]["passed"], 5u32);
assert_eq!(ev.fields["test"]["failed"], 0u32);
}
#[test]
fn js_test_beholder_on_done_failure_summary() {
use crate::types::TaskRunId;
let json = js_test_report_json(false, 3, 2);
let mut b = JsTestBeholder::new("jest", "27.0", jest_adjust_argv);
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1, "no assertion results → only summary event");
let ev = &events[0];
assert_eq!(ev.target, "jest");
assert_eq!(ev.level, Level::Error);
assert_eq!(ev.msg, "2 failed, 3 passed");
}
#[test]
fn js_test_beholder_on_done_emits_failed_assertion_events() {
use crate::types::TaskRunId;
let json = r#"{
"success": false,
"numTotalTests": 2,
"numPassedTests": 1,
"numFailedTests": 1,
"numPendingTests": 0,
"testResults": [
{
"testFilePath": "src/foo.test.ts",
"assertionResults": [
{
"fullName": "suite > passes",
"status": "passed",
"failureMessages": []
},
{
"fullName": "suite > fails",
"status": "failed",
"failureMessages": ["Error: expected 1 to equal 2\n at foo (src/foo.test.ts:10)"]
}
]
}
]
}"#;
let mut b = JsTestBeholder::new("vitest", "1.0", vitest_adjust_argv);
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let run_id = TaskRunId::new();
let events = b.on_done(&run_id, 0);
// summary + one failure event (passed assertion is skipped)
assert_eq!(events.len(), 2, "expected summary + one failure event; got: {events:?}");
let failure = &events[1];
assert_eq!(failure.level, Level::Error);
assert_eq!(failure.target, "vitest::test");
assert_eq!(failure.msg, "Error: expected 1 to equal 2");
assert_eq!(failure.fields["test"]["name"], "suite > fails");
assert_eq!(failure.fields["file"]["path"], "src/foo.test.ts");
}
#[test]
fn js_test_beholder_on_done_multiple_suites_and_failures() {
use crate::types::TaskRunId;
let json = r#"{
"success": false,
"numTotalTests": 4,
"numPassedTests": 2,
"numFailedTests": 2,
"numPendingTests": 0,
"testResults": [
{
"testFilePath": "src/a.test.ts",
"assertionResults": [
{ "fullName": "A passes", "status": "passed", "failureMessages": [] },
{ "fullName": "A fails", "status": "failed", "failureMessages": ["err A"] }
]
},
{
"testFilePath": "src/b.test.ts",
"assertionResults": [
{ "fullName": "B passes", "status": "passed", "failureMessages": [] },
{ "fullName": "B fails", "status": "failed", "failureMessages": ["err B"] }
]
}
]
}"#;
let mut b = JsTestBeholder::new("vitest", "1.0", vitest_adjust_argv);
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
// summary + 2 failure events
assert_eq!(events.len(), 3);
assert_eq!(events[1].fields["file"]["path"], "src/a.test.ts");
assert_eq!(events[2].fields["file"]["path"], "src/b.test.ts");
}
#[test]
fn js_test_beholder_on_done_empty_buffer_returns_no_events() {
use crate::types::TaskRunId;
let b = JsTestBeholder::new("vitest", "1.0", vitest_adjust_argv);
// Do not call parse_chunk — buffer is empty.
let mut b = b;
let events = b.on_done(&TaskRunId::new(), 0);
assert!(events.is_empty());
}
#[test]
fn js_test_beholder_on_done_invalid_json_flags_unknown_format() {
use crate::types::TaskRunId;
let mut b = JsTestBeholder::new("vitest", "1.0", vitest_adjust_argv);
b.parse_chunk(&dummy_chunk(b"not json"));
let events = b.on_done(&TaskRunId::new(), 0);
assert!(events.is_empty());
assert!(b.unknown_format_reason().is_some());
}
#[test]
fn js_test_beholder_on_done_chunks_split_across_calls() {
use crate::types::TaskRunId;
let json = r#"{"success":true,"numTotalTests":1,"numPassedTests":1,"numFailedTests":0,"numPendingTests":0,"testResults":[]}"#;
let mid = json.len() / 2;
let mut b = JsTestBeholder::new("jest", "27.0", jest_adjust_argv);
b.parse_chunk(&dummy_chunk(json[..mid].as_bytes()));
b.parse_chunk(&dummy_chunk(json[mid..].as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1, "split chunks must still parse correctly");
assert_eq!(events[0].level, Level::Info);
}
#[test]
fn js_test_beholder_failure_msg_uses_first_line_only() {
use crate::types::TaskRunId;
let json = r#"{
"success": false,
"numTotalTests": 1, "numPassedTests": 0, "numFailedTests": 1, "numPendingTests": 0,
"testResults": [{
"testFilePath": "x.test.ts",
"assertionResults": [{
"fullName": "fails",
"status": "failed",
"failureMessages": ["Error: oops\n at Object.<anonymous> (x.test.ts:5:5)\n at ...]"]
}]
}]
}"#;
let mut b = JsTestBeholder::new("vitest", "1.0", vitest_adjust_argv);
b.parse_chunk(&dummy_chunk(json.as_bytes()));
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 2);
// Failure event msg must be the first line only, not the full stack trace.
assert_eq!(events[1].msg, "Error: oops");
}
// ─── PytestBeholderFactory tests ──────────────────────────────────────────
fn args(s: &str) -> Vec<String> { s.split_whitespace().map(str::to_owned).collect() }
#[test]
fn pytest_factory_matches_direct() {
let f = PytestBeholderFactory;
assert!(f.matches(&args("pytest")));
assert!(f.matches(&args("pytest tests/")));
assert!(f.matches(&args("py.test -v")));
}
#[test]
fn pytest_factory_matches_python_m_pytest() {
let f = PytestBeholderFactory;
assert!(f.matches(&args("python -m pytest")));
assert!(f.matches(&args("python3 -m pytest tests/")));
}
#[test]
fn pytest_factory_declines_version_and_help() {
let f = PytestBeholderFactory;
assert!(!f.matches(&args("pytest --version")));
assert!(!f.matches(&args("pytest -V")));
assert!(!f.matches(&args("pytest --help")));
assert!(!f.matches(&args("pytest -h")));
}
#[test]
fn pytest_factory_declines_collect_only() {
let f = PytestBeholderFactory;
assert!(!f.matches(&args("pytest --collect-only")));
assert!(!f.matches(&args("pytest --co")));
}
#[test]
fn pytest_factory_declines_non_pytest() {
let f = PytestBeholderFactory;
assert!(!f.matches(&args("cargo test")));
assert!(!f.matches(&args("python script.py")));
assert!(!f.matches(&args("python -m flask run")));
}
#[test]
fn pytest_factory_mode_is_parser() {
assert!(matches!(PytestBeholderFactory.mode(), BeholderMode::Parser));
}
// ─── PytestBeholder parse tests ───────────────────────────────────────────
fn pytest_output_chunk(lines: &str, seq: u32) -> OutputChunk {
use crate::types::TaskRunId;
OutputChunk {
run_id: TaskRunId::new(),
seq,
offset_ms: 0,
stream: crate::types::Stream::Stdout,
bytes: lines.as_bytes().to_vec(),
}
}
#[test]
fn pytest_beholder_parses_failure_lines() {
let mut b = PytestBeholder::default();
let chunk = pytest_output_chunk(
"FAILED tests/test_foo.py::test_bar - AssertionError: assert 1 == 2\n\
FAILED tests/test_baz.py::test_qux - ZeroDivisionError: division by zero\n",
1,
);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 2);
assert_eq!(events[0].level, Level::Error);
assert_eq!(events[0].target, "pytest::test");
assert_eq!(events[0].msg, "AssertionError: assert 1 == 2");
assert_eq!(events[0].fields["test"]["name"], "test_bar");
assert_eq!(events[0].fields["file"]["path"], "tests/test_foo.py");
assert_eq!(events[1].msg, "ZeroDivisionError: division by zero");
}
#[test]
fn pytest_beholder_parses_error_lines() {
let mut b = PytestBeholder::default();
let chunk = pytest_output_chunk(
"ERROR tests/test_broken.py - ImportError: No module named 'foo'\n",
1,
);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
assert_eq!(events[0].msg, "ImportError: No module named 'foo'");
assert_eq!(events[0].fields["file"]["path"], "tests/test_broken.py");
}
#[test]
fn pytest_beholder_failure_without_reason() {
let mut b = PytestBeholder::default();
let chunk = pytest_output_chunk("FAILED tests/test_foo.py::test_bar\n", 1);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
assert_eq!(events[0].msg, "FAILED tests/test_foo.py::test_bar");
}
#[test]
fn pytest_beholder_ignores_lines_without_py() {
let mut b = PytestBeholder::default();
// These should not produce events — no `.py` in the nodeid.
let chunk = pytest_output_chunk(
"FAILED some_other_thing\nERROR not_a_module\n",
1,
);
let events = b.parse_chunk(&chunk);
assert!(events.is_empty());
}
#[test]
fn pytest_beholder_on_done_success_summary() {
use crate::types::TaskRunId;
let mut b = PytestBeholder::default();
let chunk = pytest_output_chunk(
"collected 5 items\n\
========================= 5 passed in 0.85s ==========================\n",
1,
);
b.parse_chunk(&chunk);
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1);
let ev = &events[0];
assert_eq!(ev.level, Level::Info);
assert_eq!(ev.target, "pytest");
assert_eq!(ev.msg, "5 passed");
assert_eq!(ev.fields["test"]["passed"], 5u32);
assert_eq!(ev.fields["test"]["failed"], 0u32);
assert_eq!(ev.fields["build"]["success"], true);
}
#[test]
fn pytest_beholder_on_done_failure_summary() {
use crate::types::TaskRunId;
let mut b = PytestBeholder::default();
let chunk = pytest_output_chunk(
"FAILED tests/a.py::t1 - err\n\
======= 1 failed, 4 passed in 1.23s =======\n",
1,
);
b.parse_chunk(&chunk);
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1);
let ev = &events[0];
assert_eq!(ev.level, Level::Error);
assert_eq!(ev.msg, "1 failed, 4 passed");
assert_eq!(ev.fields["test"]["failed"], 1u32);
assert_eq!(ev.fields["test"]["passed"], 4u32);
assert_eq!(ev.fields["build"]["success"], false);
}
#[test]
fn pytest_beholder_on_done_mixed_summary() {
use crate::types::TaskRunId;
let mut b = PytestBeholder::default();
let chunk = pytest_output_chunk(
"====== 2 failed, 1 error, 5 passed, 1 skipped in 3.14s ======\n",
1,
);
b.parse_chunk(&chunk);
let events = b.on_done(&TaskRunId::new(), 0);
assert_eq!(events.len(), 1);
let ev = &events[0];
assert_eq!(ev.level, Level::Error);
assert_eq!(ev.msg, "2 failed, 1 error, 5 passed, 1 skipped");
assert_eq!(ev.fields["test"]["failed"], 2u32);
assert_eq!(ev.fields["test"]["errors"], 1u32);
assert_eq!(ev.fields["test"]["passed"], 5u32);
assert_eq!(ev.fields["test"]["skipped"], 1u32);
}
#[test]
fn pytest_beholder_on_done_no_summary_returns_empty() {
use crate::types::TaskRunId;
let mut b = PytestBeholder::default();
let events = b.on_done(&TaskRunId::new(), 0);
assert!(events.is_empty());
}
#[test]
fn pytest_beholder_chunks_split_across_calls() {
let line = "FAILED tests/foo.py::bar - AssertionError\n";
let mid = line.len() / 2;
let mut b = PytestBeholder::default();
b.parse_chunk(&pytest_output_chunk(&line[..mid], 1));
let ev1 = b.parse_chunk(&pytest_output_chunk(&line[mid..], 2));
// The complete FAILED line should be emitted from the second chunk.
assert_eq!(ev1.len(), 1);
assert_eq!(ev1[0].msg, "AssertionError");
}
// ─── parse_pytest_summary unit tests ──────────────────────────────────────
#[test]
fn parse_pytest_summary_all_passed() {
let s = parse_pytest_summary("5 passed in 0.85s").unwrap();
assert_eq!(s.passed, 5);
assert_eq!(s.failed, 0);
}
#[test]
fn parse_pytest_summary_mixed() {
let s = parse_pytest_summary("2 failed, 5 passed, 1 skipped in 3.14s").unwrap();
assert_eq!(s.failed, 2);
assert_eq!(s.passed, 5);
assert_eq!(s.skipped, 1);
assert_eq!(s.errors, 0);
}
#[test]
fn parse_pytest_summary_with_errors() {
let s = parse_pytest_summary("1 error, 3 passed in 1.0s").unwrap();
assert_eq!(s.errors, 1);
assert_eq!(s.passed, 3);
}
#[test]
fn parse_pytest_summary_rejects_garbage() {
assert!(parse_pytest_summary("not a summary").is_err());
assert!(parse_pytest_summary("").is_err());
}
// ─── ViteBuildBeholderFactory tests ───────────────────────────────────────
#[test]
fn vite_build_factory_matches_bare() {
let f = ViteBuildBeholderFactory;
assert!(f.matches(&args("vite build")));
assert!(f.matches(&args("vite build --outDir dist")));
}
#[test]
fn vite_build_factory_declines_no_build_subcommand() {
let f = ViteBuildBeholderFactory;
assert!(!f.matches(&args("vite")));
assert!(!f.matches(&args("vite preview")));
assert!(!f.matches(&args("vite dev")));
}
#[test]
fn vite_build_factory_declines_help_and_version() {
let f = ViteBuildBeholderFactory;
assert!(!f.matches(&args("vite build --help")));
assert!(!f.matches(&args("vite build -h")));
assert!(!f.matches(&args("vite build --version")));
}
#[test]
fn vite_build_factory_declines_non_vite() {
let f = ViteBuildBeholderFactory;
assert!(!f.matches(&args("cargo build")));
assert!(!f.matches(&args("tsc")));
}
#[test]
fn vite_build_factory_mode_is_parser() {
assert!(matches!(ViteBuildBeholderFactory.mode(), BeholderMode::Parser));
}
// ─── ViteBuildBeholder parse tests ────────────────────────────────────────
fn vite_chunk(lines: &str, seq: u32) -> OutputChunk {
use crate::types::TaskRunId;
OutputChunk {
run_id: TaskRunId::new(),
seq,
offset_ms: 100,
stream: crate::types::Stream::Stdout,
bytes: lines.as_bytes().to_vec(),
}
}
#[test]
fn vite_beholder_parses_warning_line() {
let mut b = ViteBuildBeholder::default();
let chunk = vite_chunk(
"(!) Some chunks are larger than 500 kB after minification.\n",
1,
);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, Level::Warn);
assert_eq!(events[0].target, "vite-build::warning");
assert_eq!(events[0].msg, "Some chunks are larger than 500 kB after minification.");
assert_eq!(events[0].fields["build"]["warning"], true);
}
#[test]
fn vite_beholder_parses_bundle_line_with_gzip() {
let mut b = ViteBuildBeholder::default();
let chunk = vite_chunk(
"dist/assets/index-CKBFsjV8.js 141.01 kB \u{2502} gzip: 45.33 kB\n",
1,
);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
let ev = &events[0];
assert_eq!(ev.level, Level::Info);
assert_eq!(ev.target, "vite-build::bundle");
assert_eq!(ev.fields["file"]["path"], "dist/assets/index-CKBFsjV8.js");
assert!((ev.fields["build"]["size_kb"].as_f64().unwrap() - 141.01).abs() < 0.01);
assert!((ev.fields["build"]["gzip_kb"].as_f64().unwrap() - 45.33).abs() < 0.01);
}
#[test]
fn vite_beholder_parses_bundle_line_no_gzip() {
let mut b = ViteBuildBeholder::default();
let chunk = vite_chunk("dist/index.html 0.46 kB\n", 1);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
assert_eq!(events[0].target, "vite-build::bundle");
assert!((events[0].fields["build"]["size_kb"].as_f64().unwrap() - 0.46).abs() < 0.01);
assert!(events[0].fields["build"].get("gzip_kb").is_none()
|| events[0].fields["build"]["gzip_kb"].is_null());
}
#[test]
fn vite_beholder_parses_built_timing() {
let mut b = ViteBuildBeholder::default();
let chunk = vite_chunk("\u{2713} built in 2.43s\n", 1);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
let ev = &events[0];
assert_eq!(ev.level, Level::Info);
assert_eq!(ev.target, "vite-build");
assert_eq!(ev.fields["build"]["duration_ms"], 2430u64);
assert_eq!(ev.fields["build"]["success"], true);
}
#[test]
fn vite_beholder_parses_built_timing_no_checkmark() {
let mut b = ViteBuildBeholder::default();
let chunk = vite_chunk("built in 0.99s\n", 1);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 1);
assert_eq!(events[0].fields["build"]["duration_ms"], 990u64);
}
#[test]
fn vite_beholder_ignores_unrelated_lines() {
let mut b = ViteBuildBeholder::default();
let chunk = vite_chunk(
"vite v5.4.0 building for production...\n\
\u{2713} 1234 modules transformed.\n",
1,
);
let events = b.parse_chunk(&chunk);
assert!(events.is_empty(), "got unexpected events: {events:?}");
}
#[test]
fn vite_beholder_multiple_events_one_chunk() {
let mut b = ViteBuildBeholder::default();
let chunk = vite_chunk(
"(!) Use of eval is strongly discouraged.\n\
dist/assets/index.js 50.00 kB \u{2502} gzip: 15.00 kB\n\
\u{2713} built in 1.50s\n",
1,
);
let events = b.parse_chunk(&chunk);
assert_eq!(events.len(), 3);
assert_eq!(events[0].level, Level::Warn);
assert_eq!(events[1].target, "vite-build::bundle");
assert_eq!(events[2].target, "vite-build");
}
#[test]
fn vite_beholder_chunks_split_across_calls() {
let line = "(!) Large chunk warning.\n";
let mid = line.len() / 2;
let mut b = ViteBuildBeholder::default();
let ev1 = b.parse_chunk(&vite_chunk(&line[..mid], 1));
let ev2 = b.parse_chunk(&vite_chunk(&line[mid..], 2));
assert!(ev1.is_empty(), "partial line should not emit");
assert_eq!(ev2.len(), 1);
assert_eq!(ev2[0].level, Level::Warn);
}
// ─── parse_vite_built_ms unit tests ───────────────────────────────────────
#[test]
fn parse_vite_built_ms_with_checkmark() {
assert_eq!(parse_vite_built_ms("\u{2713} built in 2.43s"), Some(2430));
}
#[test]
fn parse_vite_built_ms_bare() {
assert_eq!(parse_vite_built_ms("built in 0.50s"), Some(500));
}
#[test]
fn parse_vite_built_ms_not_a_timing_line() {
assert!(parse_vite_built_ms("vite v5.0.0 building for production...").is_none());
assert!(parse_vite_built_ms("").is_none());
}
}