776bd5432a
Observed bug in the graph viewer shipped in 66dff88: clicking `[+]` to
inline-expand a composite in `aura graph`'s HTML output renders a broken graph —
the interior nodes' edges collapse onto a single phantom node "0" with uncoloured
arrows, and the real pin docking is lost. The collapsed view and the body-drill
view both render correctly; only inline-expand breaks.
Root cause: in graph-viewer.js, genDot's `build` forms a node id by path-joining
the model keys (`cid = cpath.join("__")`). The aura model keys interior nodes by
numeric index (C23), so an inline-expanded composite yields ids like `0__0`. A
DOT identifier that begins with a digit but is not a pure numeral is invalid —
Graphviz parses `0__0` as the numeral `0` plus a leftover token, collapsing
`0__0`/`0__1`/`0__2` onto one phantom node `0`. The collapsed view survives only
because its ids are single-digit numerals (accidentally valid); the prototype
survived because it used name keys, never digit-prefixed ones. The bug surfaced
only against the real index-keyed model.
Fix: letter-prefix the id (`cid = "n" + cpath.join("__")`) so every id is a valid
identifier by construction. The fix lives in the viewer (DOT-syntax is its
responsibility), not in model_to_json — the model's index keys are correct, and
leaking the Graphviz identifier constraint into the model would be a leaky
abstraction.
RED-first guard (this establishes the project's first JS test infra, since the
viewer now carries real logic — normalizeModel + genDot — that the model golden
does not cover):
- tests/viewer_dot_ids.mjs: a headless node guard that loads the real viewer
module, discovers the expandable composites from a collapsed pass, inline-
expands them, and asserts every node id in the produced DOT is a valid Graphviz
identifier (quoted | pure numeral | bare identifier). Version-independent — it
checks the identifier grammar, not a render. The expand keys are DISCOVERED via
genDot's `expandable`, never hard-coded, so the guard cannot go vacuously green
when the fix reshapes the id (a hard-coded "0" key would stop matching "n0" and
silently test only the collapsed view — a false-green this guard refuses).
- tests/viewer_dot.rs: a Rust integration test that shells out to `node`,
wiring the guard into `cargo test --workspace`. `node` is required: if it is
absent the test fails (a skipped guard is not a guard).
- tests/fixtures/sample-model.json: the triggering fixture (numeric index keys,
a nested composite).
- graph-viewer.js: made node-loadable (typeof window/document guards + a
module.exports) so the guard can exercise the pure generator headless. This is
a test-enabler, behaviour-neutral in the browser — verified by the existing
render_html / graph_emits self-containment tests staying green and by the
expand re-rendering correctly through the vendored WASM-Graphviz.
Verified RED against the unfixed viewer (the guard trips on `0__0`/`0__1`/`0__2`
and the Rust test fails), GREEN after the prefix; full `cargo test --workspace`
green; clippy clean. Rendered the expanded state through the vendored
WASM-Graphviz to confirm the interior wires up correctly (price -> both SMA
series, SMA value -> Sub lhs/rhs, Sub -> signal, all f64-blue).
49 lines
1.9 KiB
Rust
49 lines
1.9 KiB
Rust
//! Integration guard for the `aura graph` viewer's DOT generation.
|
|
//!
|
|
//! The viewer (`assets/graph-viewer.js`) builds the Graphviz DOT in JavaScript,
|
|
//! so the property "every node id genDot emits is a valid Graphviz identifier"
|
|
//! cannot be checked from Rust directly. This test shells out to `node`, running
|
|
//! the headless guard `tests/viewer_dot_ids.mjs`, which loads the real viewer
|
|
//! module and asserts the DOT-identifier rule over an inline-expanded nested
|
|
//! composite with numeric index keys (the shape `aura graph` emits).
|
|
//!
|
|
//! Why it matters: a path-joined id like `0__0` begins with a digit but is not a
|
|
//! pure numeral, so Graphviz misparses it and collapses the inline-expanded
|
|
//! interior onto a single phantom node — the inline-expand render bug. The guard
|
|
//! is Graphviz-version-independent (it checks the identifier grammar, not a
|
|
//! render).
|
|
//!
|
|
//! `node` is REQUIRED: if it is not on PATH this test FAILS (a skipped guard is
|
|
//! not a guard).
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
#[test]
|
|
fn viewer_emits_valid_dot_node_identifiers() {
|
|
let script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests")
|
|
.join("viewer_dot_ids.mjs");
|
|
assert!(
|
|
script.exists(),
|
|
"guard script missing at {}",
|
|
script.display()
|
|
);
|
|
|
|
let out = match Command::new("node").arg(&script).output() {
|
|
Ok(out) => out,
|
|
Err(e) => panic!(
|
|
"node is required for the viewer DOT-id guard but could not be run \
|
|
({e}); install Node.js or ensure `node` is on PATH"
|
|
),
|
|
};
|
|
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(
|
|
out.status.success(),
|
|
"viewer DOT-id guard failed (exit {:?}).\n--- node stdout ---\n{stdout}\n--- node stderr ---\n{stderr}",
|
|
out.status.code()
|
|
);
|
|
}
|