db09e5de52
New research_docs.rs module: role-addressed process verbs with house-style fault prose (Display-free enums phrased at the seam), the introspect exactly-one-of usage guard (exit 2), and content-addressed registration. Seven binary seam tests pin intrinsic-ok/refusal prose, vocabulary/block/ unwired/content-id introspection, exit codes, and the stored file's location under the runs root. Quality-gate finding folded in (and back into the plan): register no longer re-derives the store path from runs_root — aura-registry now exposes process_path/campaign_path, the same single mapping its put/get pair routes through, so the printed path cannot drift from the store layout. Verification: research_docs seam tests 7/0, registry 49/0, clippy clean. refs #189
139 lines
5.4 KiB
Rust
139 lines
5.4 KiB
Rust
//! End-to-end coverage for the `aura process` verb family (#188/#189): drives
|
|
//! the built `aura` binary over argv/file, the exact surface a data-level
|
|
//! author uses.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// A fresh, unique working directory for a process test that persists
|
|
/// content-addressed documents under `./runs/` (so `aura process register`
|
|
/// never dirties the repo). Unique per test + per process; no external
|
|
/// tempfile dependency (mirrors `cli_run.rs` / `cli_broken_pipe.rs`).
|
|
fn temp_cwd(name: &str) -> PathBuf {
|
|
let dir = std::env::temp_dir().join(format!("aura-cli-{}-{}", std::process::id(), name));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).expect("create temp cwd");
|
|
dir
|
|
}
|
|
|
|
fn run_code_in(dir: &Path, args: &[&str]) -> (String, Option<i32>) {
|
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args(args)
|
|
.current_dir(dir)
|
|
.output()
|
|
.expect("binary runs");
|
|
let text = format!(
|
|
"{}{}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
(text, out.status.code())
|
|
}
|
|
|
|
fn run_code(args: &[&str]) -> (String, Option<i32>) {
|
|
run_code_in(Path::new("."), args)
|
|
}
|
|
|
|
const PROCESS_DOC: &str = r#"{
|
|
"format_version": 1,
|
|
"kind": "process",
|
|
"name": "wf-deflated-screen",
|
|
"pipeline": [
|
|
{ "block": "std::sweep", "metric": "net_expectancy_r", "select": "plateau:worst", "deflate": true },
|
|
{ "block": "std::gate", "all": [
|
|
{ "metric": "net_expectancy_r", "cmp": "gt", "value": 0.0 },
|
|
{ "metric": "overfit_probability", "cmp": "lt", "value": 0.1 } ] },
|
|
{ "block": "std::walk_forward", "folds": 4, "in_sample_bars": 4000,
|
|
"out_of_sample_bars": 1000, "metric": "net_expectancy_r", "select": "argmax" }
|
|
]
|
|
}"#;
|
|
|
|
fn write_doc(dir: &Path, name: &str, text: &str) -> PathBuf {
|
|
let p = dir.join(name);
|
|
std::fs::write(&p, text).expect("write doc");
|
|
p
|
|
}
|
|
|
|
#[test]
|
|
fn process_validate_reports_intrinsic_ok() {
|
|
let dir = temp_cwd("process-validate-ok");
|
|
write_doc(&dir, "p.process.json", PROCESS_DOC);
|
|
let (out, code) = run_code_in(&dir, &["process", "validate", "p.process.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(out.contains("process document valid (intrinsic): 3 pipeline blocks, 2 gate predicates"));
|
|
}
|
|
|
|
#[test]
|
|
fn process_validate_refuses_unknown_metric_as_prose_exit_1() {
|
|
let dir = temp_cwd("process-validate-bad-metric");
|
|
let bad = PROCESS_DOC.replacen(
|
|
"net_expectancy_r\", \"select\": \"plateau:worst",
|
|
"netto_r\", \"select\": \"plateau:worst",
|
|
1,
|
|
);
|
|
write_doc(&dir, "bad.process.json", &bad);
|
|
let (out, code) = run_code_in(&dir, &["process", "validate", "bad.process.json"]);
|
|
assert_eq!(code, Some(1));
|
|
assert!(out.contains("unknown metric \"netto_r\""));
|
|
assert!(!out.contains("UnknownMetric"), "Debug leak: {out}");
|
|
}
|
|
|
|
#[test]
|
|
fn process_introspect_vocabulary_block_and_content_id() {
|
|
let (out, code) = run_code(&["process", "introspect", "--vocabulary"]);
|
|
assert_eq!(code, Some(0));
|
|
for id in ["std::sweep", "std::gate", "std::walk_forward", "std::monte_carlo", "std::generalize"] {
|
|
assert!(out.contains(id), "vocabulary misses {id}: {out}");
|
|
}
|
|
let (out, code) = run_code(&["process", "introspect", "--block", "std::sweep"]);
|
|
assert_eq!(code, Some(0));
|
|
assert!(out.contains("metric"));
|
|
assert!(out.contains("required"));
|
|
|
|
let dir = temp_cwd("process-introspect-content-id");
|
|
write_doc(&dir, "p.process.json", PROCESS_DOC);
|
|
let (out, code) = run_code_in(&dir, &["process", "introspect", "--content-id", "p.process.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
let line = out.lines().find(|l| l.starts_with("content:")).expect("id line");
|
|
assert_eq!(line.len(), "content:".len() + 64);
|
|
}
|
|
|
|
#[test]
|
|
fn process_introspect_unknown_block_is_prose_exit_1() {
|
|
let (out, code) = run_code(&["process", "introspect", "--block", "std::nope"]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(out.contains("unknown block \"std::nope\""), "stdout/stderr: {out}");
|
|
}
|
|
|
|
#[test]
|
|
fn process_introspect_unwired_lists_open_slots() {
|
|
let dir = temp_cwd("process-introspect-unwired");
|
|
let partial = r#"{ "format_version": 1, "kind": "process",
|
|
"pipeline": [ { "block": "std::sweep", "metric": "sqn" } ] }"#;
|
|
write_doc(&dir, "partial.process.json", partial);
|
|
let (out, code) = run_code_in(&dir, &["process", "introspect", "--unwired", "partial.process.json"]);
|
|
assert_eq!(code, Some(0));
|
|
assert!(out.contains("open slot: name"));
|
|
assert!(out.contains("open slot: pipeline[0].select"));
|
|
}
|
|
|
|
#[test]
|
|
fn process_introspect_no_flag_is_usage_exit_2() {
|
|
let (_out, code) = run_code(&["process", "introspect"]);
|
|
assert_eq!(code, Some(2));
|
|
}
|
|
|
|
#[test]
|
|
fn process_register_stores_content_addressed_under_runs_root() {
|
|
let dir = temp_cwd("process-register");
|
|
write_doc(&dir, "p.process.json", PROCESS_DOC);
|
|
let (out, code) = run_code_in(&dir, &["process", "register", "p.process.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
let line = out.lines().find(|l| l.starts_with("registered process content:")).expect("line");
|
|
let id = line
|
|
.trim_start_matches("registered process content:")
|
|
.split(' ')
|
|
.next()
|
|
.expect("id");
|
|
assert!(dir.join("runs").join("processes").join(format!("{id}.json")).is_file());
|
|
}
|