623d304b7f
The op-script gains its tenth verb end-to-end: OpDoc::Use carries a tagged ref
({"content_id": id-or-unique-prefix} | {"name": label}); DTO conversion resolves
it (label sidecar / #302 prefix semantics / verbatim id), fetches, deserializes
eagerly (a vocab/parse failure fails fast naming the id prefix — review
finding, no swallowed .ok()), re-walks the C29 doc gate over the fetched
composite (a doc-less store entry cannot enter a NEW composition; existing
reads stay valid — no retroactive invalidation), echoes every resolution as
the existing benign marker (aura: note: use "<instance>": <ref> -> <id>,
stderr; stdout stays clean payload), and hands the session a pure cached
lookup closure.
graph register <file> --name <label> records the label and echoes repoints
(label "x" -> <new> (was <old>)); graph introspect --registered lists
label / 12-char id prefix / root doc line — the discovery surface for what
use can reference. Refusals are by-identifier, name their rule, and enumerate
the label set (empty store reads as prose); all op-list content faults are
runtime exit 1 per the pinned #175 attribution, argv misuse stays exit 2.
Ten new/extended e2e tests cover label round-trip + repoint, echo discipline,
unknown-label enumeration, the C29 refusal shape, end-to-end sweep axes
through a spliced instance (graph.<instance>.<node>.<param>), the open-pattern
build flow, and the standalone-run bootstrap refusal (asserts the existing
Debug-form fault; an index->name rendering at the run boundary is recorded
residue, not silently claimed). OP_REFERENCE and its help pin move 9 -> 10.
Empirical note for the record: aura run's bias arm re-roots the loaded signal
via wrap_r, so the standalone-run refusal for an open pattern is exercised on
the bare-tap measurement path; the bias path surfaces openness as a role-
binding refusal instead (pre-existing shape, untouched).
closes #317
248 lines
11 KiB
Rust
248 lines
11 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.
|
|
/// #317: the `use` op joins the roster (nine -> ten).
|
|
#[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""#,
|
|
r#"{"op":"use""#,
|
|
] {
|
|
assert!(out.contains(op), "op reference carries {op}: {out}");
|
|
}
|
|
assert!(out.contains("graph introspect --vocabulary"), "points at the node roster: {out}");
|
|
}
|
|
|
|
/// #317: `graph introspect --registered` lists every registry label — row
|
|
/// shape `<label> <id prefix> <root doc>` — and the empty-store form reads
|
|
/// as prose, not a blank/garbled listing.
|
|
#[test]
|
|
fn graph_introspect_registered_lists_labels() {
|
|
let dir = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join("aura-help-registered");
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).expect("create temp cwd");
|
|
|
|
// Empty store: prose, not a blank listing, exit 0.
|
|
let out_empty = Command::new(BIN)
|
|
.args(["graph", "introspect", "--registered"])
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn aura");
|
|
assert!(out_empty.status.success(), "empty store still exits 0");
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&out_empty.stdout),
|
|
"no labels registered\n",
|
|
"the empty-store form reads as prose"
|
|
);
|
|
|
|
// Register + label a described open pattern, then list it.
|
|
let pattern = r#"[
|
|
{"op":"doc","text":"a reusable smoothing pattern"},
|
|
{"op":"input","role":"x"},
|
|
{"op":"add","type":"SMA","name":"sma"},
|
|
{"op":"feed","role":"x","into":["sma.series"]},
|
|
{"op":"expose","from":"sma.value","as":"out"}
|
|
]"#;
|
|
let bp_path = dir.join("pattern.ops.json");
|
|
std::fs::write(&bp_path, pattern).expect("write pattern fixture");
|
|
let built = Command::new(BIN)
|
|
.args(["graph", "build"])
|
|
.current_dir(&dir)
|
|
.stdin(std::process::Stdio::piped())
|
|
.stdout(std::process::Stdio::piped())
|
|
.spawn()
|
|
.and_then(|mut child| {
|
|
use std::io::Write;
|
|
child.stdin.take().unwrap().write_all(pattern.as_bytes())?;
|
|
child.wait_with_output()
|
|
})
|
|
.expect("build the pattern");
|
|
assert!(built.status.success(), "the pattern builds");
|
|
let envelope = dir.join("pattern.bp.json");
|
|
std::fs::write(&envelope, &built.stdout).expect("write built envelope");
|
|
|
|
let reg = Command::new(BIN)
|
|
.args(["graph", "register", envelope.to_str().unwrap(), "--name", "smooth"])
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn aura register");
|
|
assert!(reg.status.success(), "register --name succeeds: {}", String::from_utf8_lossy(®.stderr));
|
|
|
|
let out = Command::new(BIN)
|
|
.args(["graph", "introspect", "--registered"])
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn aura");
|
|
assert!(out.status.success(), "listing succeeds");
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
assert!(stdout.contains("smooth"), "lists the label: {stdout}");
|
|
assert!(stdout.contains("a reusable smoothing pattern"), "lists the root doc: {stdout}");
|
|
let row = stdout.lines().find(|l| l.starts_with("smooth")).expect("smooth row");
|
|
let fields: Vec<&str> = row.split_whitespace().collect();
|
|
assert_eq!(fields[1].len(), 12, "the id prefix is 12 hex chars: {row}");
|
|
assert!(fields[1].chars().all(|c| c.is_ascii_hexdigit()), "the prefix is hex: {row}");
|
|
}
|
|
|
|
/// #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);
|
|
}
|