Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions crates/aprender-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ jsonschema = "0.28"
# FALSIFY-MCP-008 harness reads the YAML contract directly to assert byte
# identity between codegen output and live tools/list schemas.
serde_yaml = { workspace = true }
# FALSIFY-MCP-DOGFOOD-001: locate the workspace-built `apr` binary in the
# end-to-end stdio conformance test. Already in the workspace lockfile via
# aprender-profile dev-deps; no new transitive cost.
assert_cmd = "2.0"
# NOTE: no `assert_cmd`. FALSIFY-MCP-DOGFOOD-001 and -MCP-010/-011 drive the
# real `apr` binary, and this used to locate it with
# `assert_cmd::cargo::cargo_bin("apr")`. That reads `CARGO_BIN_EXE_apr`, which
# cargo sets only for binaries the SAME package builds — this package is
# lib-only, so it was never set, and the target-dir guess it falls back to ran
# whichever commit's `apr` was lying in the shared target dir (or panicked when
# none was). Those tests now build `apr` and use the path cargo reports:
# tests/common/mod.rs.

[lints]
workspace = true
117 changes: 117 additions & 0 deletions crates/aprender-mcp/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! Declared resolution of the `apr` binary for the falsifiers that drive the
//! real CLI (`falsify_mcp_dogfood_001`, `falsify_mcp_stdio_protocol`).
//!
//! # Why this module exists
//!
//! `aprender-mcp` is a **lib-only** package: it declares no `[[bin]]`, so cargo
//! never sets `CARGO_BIN_EXE_apr` for these test targets. Both files used to
//! call `assert_cmd::cargo::cargo_bin("apr")`, which on assert_cmd 2.2 reads
//! `CARGO_BIN_EXE_apr` and, finding it unset, falls back to guessing
//! `<dir of current_exe>/../apr` — the target directory of *whoever happened to
//! build last*. Neither half is a declared dependency:
//!
//! * The env-var half can never fire here. Only the package that *builds* a
//! binary gets `CARGO_BIN_EXE_<name>`, and this package builds none.
//! * The guess half depends on another package having already built `apr` into
//! that exact directory. When it has, the test silently runs whatever commit's
//! binary is lying there; when it has not, `cargo_bin` **panics** with
//! "`CARGO_BIN_EXE_apr` is unset". Measured on a fresh worktree: all six
//! falsifiers in these two files failed that way, before a single assertion ran.
//!
//! The panic also made `falsify_mcp_dogfood_001`'s
//! `if candidate.is_file() { .. } else { build_apr_binary() }` unreachable —
//! `cargo_bin` returns a path only when the file already exists, so the
//! build-on-demand arm was dead code that could never repair the missing binary.
//!
//! # What replaces it
//!
//! Ask cargo to build the binary we name, then take the path **cargo reports**
//! for it. Same doctrine as `scripts/apr_bin.sh` ("Ask cargo; never guess"),
//! for the same reason: every strategy that *searches* for an `apr` eventually
//! finds the wrong one. `--message-format=json` emits a `compiler-artifact`
//! record whose `executable` field is the authoritative path, so this is
//! immune to `CARGO_TARGET_DIR`, to `.cargo/config.toml` target-dir redirects
//! (gitignored here, so main and a worktree build to different places), and to
//! cargo's `build-dir` split — the three things the directory guess gets wrong.
//!
//! `cargo build` is a cheap no-op when the binary is already current, so the
//! build is unconditional: short-circuiting on "a file exists there" is exactly
//! the stale-artifact hole documented above.
//!
//! # No `$APR_BIN` escape hatch, deliberately
//!
//! `aprender_mcp::apr_bin` honours `$APR_BIN` at *runtime*, and the spawned
//! `apr mcp` child inherits this process's environment. Reading `$APR_BIN` here
//! would therefore also redirect the server's own subprocess resolution, past
//! the mock shim the dogfood falsifier installs on `PATH` — the override would
//! silently change what is under test rather than just where it lives.

use std::path::PathBuf;
use std::process::{Command, Stdio};

/// The workspace package that owns the `apr` binary (root `Cargo.toml`,
/// `[[bin]] name = "apr"`). Pinned by version because crates.io ships older
/// `aprender` releases that can land in the dependency graph and make a bare
/// `-p aprender` spec ambiguous. `aprender-mcp` and the root package both take
/// `version.workspace = true`, so `CARGO_PKG_VERSION` here is the right one.
fn apr_package_spec() -> String {
format!("aprender@{}", env!("CARGO_PKG_VERSION"))
}

/// Build `apr` and return the path cargo reports for it.
///
/// Panics with the cargo failure surfaced on stderr if the build fails — a
/// broken `apr` is a real failure these falsifiers must report, not skip.
pub fn apr_binary() -> PathBuf {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let pkg_spec = apr_package_spec();

// `json-render-diagnostics` keeps the machine-readable artifact records on
// stdout while compiler errors stay human-readable on the inherited stderr,
// so a build failure here is as legible as a normal `cargo build`.
let output = Command::new(&cargo)
.args([
"build",
"--bin",
"apr",
"-p",
&pkg_spec,
"--message-format=json-render-diagnostics",
])
.stderr(Stdio::inherit())
.output()
.unwrap_or_else(|e| panic!("invoke `{cargo} build --bin apr -p {pkg_spec}`: {e}"));
assert!(
output.status.success(),
"`cargo build --bin apr -p {pkg_spec}` failed with {:?}",
output.status
);

let stdout = String::from_utf8(output.stdout).expect("cargo --message-format=json emits UTF-8");
let mut executables: Vec<PathBuf> = stdout
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.filter(|msg| msg["reason"] == "compiler-artifact" && msg["target"]["name"] == "apr")
.filter_map(|msg| msg["executable"].as_str().map(PathBuf::from))
.collect();
executables.sort();
executables.dedup();

// Exactly one, or we do not know which `apr` we are testing. `--bin apr -p
// <one package>` compiles a single bin target, so two distinct paths means
// the graph grew a second `apr` and a "pick the last one" rule would decide
// it by luck.
assert_eq!(
executables.len(),
1,
"expected exactly one `apr` executable from `cargo build --bin apr -p {pkg_spec}`, \
cargo reported {executables:?}"
);
let path = executables.remove(0);
assert!(
path.is_file(),
"cargo reported `apr` at {} but nothing is there",
path.display()
);
path
}
102 changes: 28 additions & 74 deletions crates/aprender-mcp/tests/falsify_mcp_dogfood_001.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
//!
//! # How it works
//!
//! - Locate the workspace-built `apr` binary via `assert_cmd::cargo_bin`.
//! Cargo builds workspace binaries before running integration tests, so
//! the binary is on disk when this test executes.
//! - Build the `apr` binary and take the path cargo reports for it
//! (`tests/common/mod.rs`). This package declares no `[[bin]]`, so cargo
//! does NOT build `apr` before running these tests and does not set
//! `CARGO_BIN_EXE_apr` — the dependency has to be stated, not assumed.
//! - Drop a mock `apr` shell shim into a tempdir and PREPEND it to the
//! spawned process's `PATH`. The mock handles `validate`, `tensors`,
//! `bench`, `qa`, `trace`, `run`, `serve`, `finetune` — every subcommand
Expand All @@ -56,7 +57,11 @@ use std::path::{Path, PathBuf};
use std::process::{ChildStdin, ChildStdout, Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use std::time::Duration;

/// Declared resolution of the `apr` binary — see `tests/common/mod.rs`.
mod common;
use common::apr_binary;

/// Names of every tool the M3 server registers via `AprMcpServer::tool_definitions`.
/// Kept in lock-step with `crates/aprender-mcp/src/server.rs`.
Expand All @@ -76,40 +81,6 @@ const EXPECTED_TOOLS: &[&str] = &[
/// almost certainly a deadlock — fail loudly so CI surfaces it immediately.
const READ_TIMEOUT: Duration = Duration::from_secs(2);

/// Build `apr` on demand if `assert_cmd::cargo_bin` couldn't find it.
///
/// This happens when the test crate is exercised in isolation
/// (`cargo test -p aprender-mcp`) without a prior workspace build of the
/// root `aprender` package's `apr` binary. We invoke `cargo build --bin
/// apr -p aprender@<workspace-version>` and then re-resolve via
/// `cargo_bin`. The version qualifier is required because crates.io ships
/// older `aprender` packages that get pulled into the dependency graph,
/// making the bare `-p aprender` spec ambiguous.
///
/// Panics with a clear message if the build itself fails — that's a real
/// failure mode the test must surface, not paper over.
fn build_apr_binary() -> PathBuf {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
// env!() resolves at compile time so we always match the workspace
// root's `aprender` version exactly, regardless of registry deps.
let pkg_spec = format!("aprender@{}", env!("CARGO_PKG_VERSION"));
let status = Command::new(&cargo)
.args(["build", "--bin", "apr", "-p", &pkg_spec, "--quiet"])
.status()
.expect("invoke `cargo build --bin apr`");
assert!(
status.success(),
"cargo build --bin apr -p {pkg_spec} failed with status {status:?}"
);
let path = assert_cmd::cargo::cargo_bin("apr");
assert!(
path.is_file(),
"expected apr binary at {} after `cargo build`",
path.display()
);
path
}

/// Tiny tempdir helper — same pattern as
/// `tests/falsify_mcp_progress_001.rs::tempdir_fallback`. Avoids pulling
/// `tempfile` into this crate for one test.
Expand Down Expand Up @@ -328,35 +299,22 @@ fn minimal_args(tool: &str) -> serde_json::Value {
#[test]
#[cfg(unix)]
fn falsify_mcp_dogfood_001_full_client_session() {
let session_start = Instant::now();

// 1. Mock apr shim on a private PATH for the spawned process only.
let tmp = tempdir_fallback();
write_mock_apr_shim(&tmp);
let path_value = path_with_mock_first(&tmp);

// 2. Locate the real apr binary. assert_cmd::cargo::cargo_bin walks up
// from the test executable into the workspace target dir and looks
// for `apr`. If cargo hasn't built it yet (e.g. running
// `cargo test -p aprender-mcp` in isolation), fall back to invoking
// `cargo build` inline so the test is self-contained and CI doesn't
// have to remember an extra pre-step. The workspace member name for
// the root `apr` binary is `aprender` (per root Cargo.toml
// `[[bin]] name = "apr"`).
let bin_path = {
let candidate = assert_cmd::cargo::cargo_bin("apr");
if candidate.is_file() {
candidate
} else {
build_apr_binary()
}
};
// 2. Build the real apr binary and take cargo's own path for it. Nothing
// else in this package builds `apr`, so this is the only thing that
// guarantees one exists — and it is unconditional, because "reuse the
// file already sitting in the target dir" is how a stray commit's
// binary gets tested instead of this one.
let bin_path = apr_binary();
let mut cmd = Command::new(&bin_path);
cmd.arg("mcp")
.env("PATH", &path_value)
// Keep the binary's stderr visible for postmortem if the test fails;
// assert_cmd-style inheritance is fine here because we never assert
// on stderr content.
// inheriting it is fine because we never assert on stderr content.
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
Expand Down Expand Up @@ -543,14 +501,17 @@ fn falsify_mcp_dogfood_001_full_client_session() {
// Reader thread should join now that stdout closed.
reader_handle.join().expect("stdout reader joins cleanly");

// 10. Whole-session budget. Spec asks for <2s; in practice this runs in
// well under 1s on any reasonable machine. Generous slack to absorb
// CI noise without masking real regressions.
let elapsed = session_start.elapsed();
assert!(
elapsed < Duration::from_secs(10),
"full dogfood session must complete in <10s (spec budget 2s + CI slack), took {elapsed:?}"
);
// NO whole-session wall-clock budget. There used to be an
// `assert!(session_start.elapsed() < 10s)` here, and it is deleted rather
// than widened: wall-clock assertions are banned in this repo's required
// checks because they measure the machine, not the code. It failed on the
// first run that ever reached it — 72s, all of it spent blocked on cargo's
// build-directory lock while the sibling test binary built `apr`, with
// every one of the ~14 protocol round-trips still inside its own 2s bound.
// Nothing about MCP conformance is lost: `recv`'s READ_TIMEOUT already
// bounds every single message, which is a strictly sharper liveness check
// than one budget over the whole session, and it is a hang detector rather
// than a performance claim.
}

/// Build a JSON-RPC 2.0 *notification* — a Request object with NO `id`
Expand Down Expand Up @@ -598,14 +559,7 @@ fn falsify_mcp_009_no_reply_to_notification() {
write_mock_apr_shim(&tmp);
let path_value = path_with_mock_first(&tmp);

let bin_path = {
let candidate = assert_cmd::cargo::cargo_bin("apr");
if candidate.is_file() {
candidate
} else {
build_apr_binary()
}
};
let bin_path = apr_binary();

let mut cmd = Command::new(&bin_path);
cmd.arg("mcp")
Expand Down
40 changes: 7 additions & 33 deletions crates/aprender-mcp/tests/falsify_mcp_stdio_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,50 +20,24 @@
#![allow(clippy::disallowed_methods)] // serde_json::json! expands to code that hits unwrap()

use std::io::{Read, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;

/// Declared resolution of the `apr` binary. This package builds no binaries,
/// so `CARGO_BIN_EXE_apr` never exists for this target and the target-dir
/// guess that used to stand in for it either ran a stray commit's binary or
/// panicked outright — see `tests/common/mod.rs` for the measurement.
mod common;
use common::apr_binary;

/// Hard cap on a whole stdio session. Anything slower is a hang, not a slow
/// machine — `apr.version` is answered in-process with no subprocess spawn.
const SESSION_TIMEOUT: Duration = Duration::from_secs(30);

const INITIALIZE: &str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"falsifier","version":"1"}}}"#;
const TOOLS_CALL_VERSION: &str = r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"apr.version","arguments":{}}}"#;

/// Locate the workspace-built `apr`, building it on demand when the test
/// crate is exercised in isolation. Same approach as
/// `falsify_mcp_dogfood_001.rs`.
fn apr_binary() -> PathBuf {
// ALWAYS build; never short-circuit on "the file exists".
//
// Returning an existing `cargo_bin("apr")` unconditionally means a binary
// left in the shared target dir by ANY other commit is silently preferred.
// That happened: these six falsifiers all failed against
// `apr 0.63.0 (d16c608b1)` while the worktree was at 11f958f25 — the exact
// pre-fix symptom ("stream did not contain valid UTF-8", exit 1), so the
// fix under test looked broken when it was simply not the code running.
// All six pass once the binary's embedded SHA matches HEAD.
//
// `cargo build` is a cheap no-op when the binary is already current, so
// this costs nothing in the common case and removes the failure mode.
// Same doctrine as scripts/apr_bin.sh, which hard-fails on a stale SHA.
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let pkg_spec = format!("aprender@{}", env!("CARGO_PKG_VERSION"));
let status = Command::new(&cargo)
.args(["build", "--bin", "apr", "-p", &pkg_spec, "--quiet"])
.status()
.expect("invoke `cargo build --bin apr`");
assert!(
status.success(),
"cargo build --bin apr -p {pkg_spec} failed"
);
let path = assert_cmd::cargo::cargo_bin("apr");
assert!(path.is_file(), "apr binary missing after cargo build");
path
}

/// One complete stdio session: write `input`, CLOSE stdin (the whole point —
/// this is the EOF the server used to exit through), then read stdout to EOF.
///
Expand Down
23 changes: 17 additions & 6 deletions crates/aprender-orchestrate/src/bug_hunter/tests_coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,13 +998,24 @@ fn test_bh_mod_018_nonexistent_file() {

#[test]
fn test_bh_mod_019_hunt_quick_mode() {
let config = HuntConfig {
mode: HuntMode::Quick,
targets: vec![PathBuf::from("src")],
..Default::default()
};
let result = hunt(Path::new("."), config);
let fixture = hunt_fixture("mod_019_quick");

let result = hunt(&fixture, hunt_fixture_config(HuntMode::Quick));

assert_eq!(result.mode, HuntMode::Quick);
// Quick mode is pattern-only: it must find the fixture's unwrap() and must
// NOT run the coverage/lcov phase that Hunt mode owns.
assert!(
result.findings.iter().any(|f| f.title.contains("unwrap()")),
"Quick mode missed the planted unwrap(): {:?}",
result.findings.iter().map(|f| &f.title).collect::<Vec<_>>()
);
assert!(
!result.findings.iter().any(|f| f.discovered_by == HuntMode::Hunt),
"Quick mode must not run Hunt-mode coverage analysis"
);

let _ = std::fs::remove_dir_all(&fixture);
}

// =========================================================================
Expand Down
Loading
Loading