8dbca82756
Two fold-selector/op-script surface-honesty bugs from the 2026-07-24 fieldtests, both RED-first. --folds (#332): graph introspect --folds rendered the aura-std FoldKind table — capitalized ids (Mean) and no record entry — while aura run --tap resolves labels against the fold-registry roster (lowercase, record included). The discovery surface the --tap help points at thus misdescribed the accepted input twice. It now renders FoldRegistry::core().roster() (label + doc per entry); the pinned FoldKind-rendering test was rewritten to pin the roster form (8 rows, no capitalized id leaks, record present). Unknown op-list keys (#326): a typo'd key in an op-list element ("params" for "bind") was silently dropped and the wrong graph built with zero signal, contradicting the closed-vocabulary posture (C25). OpDoc now carries serde deny_unknown_fields; the refusal names the offending key. Exit class is 1, not the issue's suggested 2: op-lists arrive on stdin, and the pinned #175 attribution principle classes stdin-content faults as runtime — the new refusal fires from that same deserialize. Blast radius verified: all 21 committed *.ops.json under fieldtests/ still build cleanly. closes #332 closes #326
176 lines
8.1 KiB
Rust
176 lines
8.1 KiB
Rust
//! End-to-end pins for the self-description surfaces (#315, #323): the help
|
|
//! opens with the two-layer concept, the sugar verbs name the document shape
|
|
//! they desugar to, every introspection roster carries per-entry meanings,
|
|
//! and `graph build --help` carries the op-list reference. Driven over the
|
|
//! built binary — the zero-setup surface a reader without repo access gets.
|
|
|
|
use std::process::Command;
|
|
|
|
const BIN: &str = env!("CARGO_BIN_EXE_aura");
|
|
|
|
/// Run `aura <args>` (no stdin); return (stdout, stderr, success).
|
|
fn run(args: &[&str]) -> (String, String, bool) {
|
|
let out = Command::new(BIN).args(args).output().expect("spawn aura");
|
|
(
|
|
String::from_utf8_lossy(&out.stdout).into_owned(),
|
|
String::from_utf8_lossy(&out.stderr).into_owned(),
|
|
out.status.success(),
|
|
)
|
|
}
|
|
|
|
/// #315: `aura --help` opens with the concepts paragraph — the two layers,
|
|
/// the bias-as-target-position + protective-stop execution model, and how
|
|
/// traces come to exist. Each pin sits on one authored line of the explicit
|
|
/// `long_about` string, so clap's wrapping cannot split it.
|
|
#[test]
|
|
fn top_level_help_opens_with_the_two_layer_concept() {
|
|
let (out, err, ok) = run(&["--help"]);
|
|
assert!(ok, "aura --help failed: {err}");
|
|
assert!(out.contains("research verbs"), "names the sugar layer: {out}");
|
|
assert!(out.contains("directly authorable"), "names the document data plane: {out}");
|
|
assert!(out.contains("bias in [-1,+1]"), "names the bias output: {out}");
|
|
assert!(out.contains("defines the risk unit"), "names the protective stop / R: {out}");
|
|
assert!(out.contains("introspect --unwired"), "names the bare-{{}} ramp: {out}");
|
|
assert!(out.contains("aura chart"), "names the trace consumers: {out}");
|
|
}
|
|
|
|
/// #315: each document-bridged sugar verb's long help names the process
|
|
/// shape it desugars to and points at the document layer. `run` (not
|
|
/// document-bridged) points at the canonical document-first form instead.
|
|
#[test]
|
|
fn sugar_verbs_name_their_document_shape() {
|
|
for (verb, needle) in [
|
|
("sweep", "std::sweep"),
|
|
("walkforward", "std::walk_forward"),
|
|
("mc", "std::monte_carlo"),
|
|
("generalize", "std::generalize"),
|
|
] {
|
|
let (out, err, ok) = run(&[verb, "--help"]);
|
|
assert!(ok, "aura {verb} --help failed: {err}");
|
|
assert!(out.contains("Sugar"), "{verb} --help names the sugar relation: {out}");
|
|
assert!(out.contains(needle), "{verb} --help names {needle}: {out}");
|
|
assert!(out.contains("aura campaign"), "{verb} --help points at the documents: {out}");
|
|
}
|
|
let (out, err, ok) = run(&["run", "--help"]);
|
|
assert!(ok, "aura run --help failed: {err}");
|
|
assert!(out.contains("document-first"), "run --help names the canonical form: {out}");
|
|
}
|
|
|
|
/// #323: `graph build --help` carries the op-list reference — the op kinds
|
|
/// and fields are learnable from the binary, not only from serde refusals.
|
|
#[test]
|
|
fn graph_build_help_carries_the_op_reference() {
|
|
let (out, err, ok) = run(&["graph", "build", "--help"]);
|
|
assert!(ok, "aura graph build --help failed: {err}");
|
|
for op in [
|
|
r#"{"op":"source""#,
|
|
r#"{"op":"input""#,
|
|
r#"{"op":"add""#,
|
|
r#"{"op":"feed""#,
|
|
r#"{"op":"connect""#,
|
|
r#"{"op":"expose""#,
|
|
r#"{"op":"tap""#,
|
|
r#"{"op":"gang""#,
|
|
r#"{"op":"doc""#,
|
|
] {
|
|
assert!(out.contains(op), "op reference carries {op}: {out}");
|
|
}
|
|
assert!(out.contains("graph introspect --vocabulary"), "points at the node roster: {out}");
|
|
}
|
|
|
|
/// #315: the node vocabulary listing carries each type's one-line meaning —
|
|
/// no more bare names that teach nothing.
|
|
#[test]
|
|
fn graph_introspect_vocabulary_carries_meanings() {
|
|
let (out, err, ok) = run(&["graph", "introspect", "--vocabulary"]);
|
|
assert!(ok, "vocabulary listing failed: {err}");
|
|
let sma = out
|
|
.lines()
|
|
.find(|l| l.split_whitespace().next() == Some("SMA"))
|
|
.unwrap_or_else(|| panic!("no SMA line: {out}"));
|
|
assert!(sma.contains("moving average"), "SMA line carries its meaning: {sma}");
|
|
for l in out.lines().filter(|l| !l.trim().is_empty()) {
|
|
assert!(l.split_whitespace().count() >= 2, "bare name without meaning: {l}");
|
|
}
|
|
}
|
|
|
|
/// #315: `--node <T>`'s head line carries the type's meaning beside its label.
|
|
#[test]
|
|
fn graph_introspect_node_head_line_carries_the_meaning() {
|
|
let (out, err, ok) = run(&["graph", "introspect", "--node", "SMA"]);
|
|
assert!(ok, "node introspect failed: {err}");
|
|
let head = out.lines().next().unwrap_or("");
|
|
assert!(head.contains("moving average"), "head line carries the meaning: {out}");
|
|
}
|
|
|
|
/// #332: `--folds` renders the fold-REGISTRY roster (the same roster
|
|
/// `aura run --tap TAP=FOLD` resolves labels against), not the aura-std
|
|
/// `FoldKind` table — lowercase, parseable labels, including the `record`
|
|
/// entry that has no `FoldKind` counterpart. Previously this surface printed
|
|
/// capitalized `FoldKind` ids (`Mean`) and omitted `record`, so the help
|
|
/// text's own discovery surface described input `--tap` refused.
|
|
#[test]
|
|
fn graph_introspect_folds_lists_the_fold_registry_roster() {
|
|
let (out, err, ok) = run(&["graph", "introspect", "--folds"]);
|
|
assert!(ok, "folds listing failed: {err}");
|
|
let lines: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
|
|
assert_eq!(lines.len(), 8, "one line per registry entry (record + 7 folds): {out}");
|
|
assert!(
|
|
!out.split_whitespace().any(|w| w == "Mean"),
|
|
"no capitalized FoldKind id leaks through: {out}"
|
|
);
|
|
let record = lines.iter().find(|l| l.starts_with("record ")).expect("record row");
|
|
assert!(record.contains("series"), "record row meaning: {record}");
|
|
let mean = lines.iter().find(|l| l.starts_with("mean ")).expect("mean row");
|
|
assert!(mean.contains("f64") && mean.contains("mean"), "mean row rules + meaning: {mean}");
|
|
let count = lines.iter().find(|l| l.starts_with("count ")).expect("count row");
|
|
assert!(count.contains("i64") && count.contains("any"), "count row rules: {count}");
|
|
}
|
|
|
|
/// #315: the metric roster carries each metric's one-line meaning beside its
|
|
/// applicability tags.
|
|
#[test]
|
|
fn process_introspect_metrics_carries_meanings() {
|
|
let (out, err, ok) = run(&["process", "introspect", "--metrics"]);
|
|
assert!(ok, "metric roster failed: {err}");
|
|
let er = out
|
|
.lines()
|
|
.find(|l| l.split_whitespace().next() == Some("expectancy_r"))
|
|
.unwrap_or_else(|| panic!("no expectancy_r line: {out}"));
|
|
assert!(er.contains("mean realized R"), "meaning text on the roster line: {er}");
|
|
for l in out.lines().filter(|l| !l.trim().is_empty()) {
|
|
assert!(l.contains(" — "), "roster line without meaning: {l}");
|
|
}
|
|
}
|
|
|
|
/// #315: the persisted-tap vocabulary's meanings are readable where the taps
|
|
/// are advertised — under the `std::presentation` block's slot listing.
|
|
#[test]
|
|
fn campaign_introspect_block_presentation_lists_tap_meanings() {
|
|
let (out, err, ok) = run(&["campaign", "introspect", "--block", "std::presentation"]);
|
|
assert!(ok, "presentation describe failed: {err}");
|
|
assert!(out.contains("cumulative pip equity"), "equity tap meaning: {out}");
|
|
assert!(out.contains("after the cost model"), "net_r_equity tap meaning: {out}");
|
|
}
|
|
|
|
/// #323: `introspect --unwired` enumerates the optional C29 `description`
|
|
/// slot for both document kinds — the authoring side of the gate is
|
|
/// advertised where every other slot is.
|
|
#[test]
|
|
fn introspect_unwired_lists_the_description_slot() {
|
|
let dir = std::env::temp_dir().join(format!("aura-selfdesc-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let bare = dir.join("bare.json");
|
|
std::fs::write(&bare, "{}").unwrap();
|
|
for family in ["process", "campaign"] {
|
|
let (out, err, ok) = run(&[family, "introspect", "--unwired", bare.to_str().unwrap()]);
|
|
assert!(ok, "{family} --unwired failed: {err}");
|
|
assert!(
|
|
out.contains("open slot: description"),
|
|
"{family} --unwired lists the description slot: {out}"
|
|
);
|
|
assert!(out.contains("C29-gated"), "{family} hint names the gate: {out}");
|
|
}
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|