feat(engine+cli): topology-identity hash — --identity-id beside --content-id
Engine (aura-engine/blueprint_serde): blueprint_to_json factored into build_doc/serialize_doc (byte-preserving — canonical golden unchanged) and blueprint_identity_json added: the canonical document with every non-load-bearing debug symbol (C23) blanked (composite name, instance names, bound-param names, role names, output re-export names) while everything load-bearing survives (type ids, node order, edges, role targets/order, output pairs/order, bound pos/kind/value — openness stays identity-bearing). Re-exported from lib.rs. Six property tests: renamed twins (equal identity, unequal canonical), open-vs-bound, bound-value, edge-swap, nested-composite interior names, role/output renames — the last two are additive beyond the plan and cover the recursion and role/output arms the planned four did not. CLI (aura-cli): graph introspect gains --identity-id, a sibling of --content-id through the same shared content_id SHA-256 primitive; composite_from_str factored out of build_from_str (fault strings byte-identical). The exactly-one introspect dispatch is DELIBERATELY relaxed: the two id flags form one group and may combine — one build, both ids, one per line, content id first. In-crate cross-path twin test (Rust sma_signal vs op-script twin: distinct content ids, one identity id) beside the existing cross-surface pins; four e2e tests incl. the previously uncovered count!=1 usage exit-2 path. topology_hash, the blueprint store, reproduce, and every --content-id byte stay untouched (spec acceptance 3; all existing pins green). Verification: cargo build clean; cargo test --workspace 884 passed / 0 failed (873 baseline + 11 new); clippy -D warnings clean; doc build 0 warnings. The implement-loop's Task-4 spec-compliance block was a plan-byte count mismatch only (plan under-counted pre-existing blueprint_serde tests 6-vs-7 and did not anticipate the two sibling-accepted extra tests); gates re-run by hand, all green. closes #171, refs #180
This commit is contained in:
@@ -296,3 +296,63 @@ fn graph_introspect_unknown_node_flag_is_usage_exit_2() {
|
||||
assert_eq!(code, Some(2), "an invalid --node flag value is a usage error -> exit 2; stderr: {stderr}");
|
||||
assert!(stderr.contains("Bogus"), "names the bad type: {stderr}");
|
||||
}
|
||||
|
||||
/// Property (#171 acc 1 at the binary seam): `--identity-id` bridges op-scripts
|
||||
/// that differ only in debug names — the renamed twin keeps the same identity id
|
||||
/// while its content id moves. Rename via blanket replace: "fast" occurs as the
|
||||
/// instance name and in every `fast.*` reference, so the replaced document stays
|
||||
/// internally consistent.
|
||||
#[test]
|
||||
fn graph_introspect_identity_id_bridges_renamed_op_scripts() {
|
||||
let renamed = SIGNAL_DOC.replace("fast", "speedy");
|
||||
let (ia, _e, ok) = run(&["graph", "introspect", "--identity-id"], SIGNAL_DOC);
|
||||
assert!(ok, "exit success");
|
||||
let ia = ia.trim().to_string();
|
||||
assert_eq!(ia.len(), 64, "a 64-hex identity id: {ia:?}");
|
||||
assert!(ia.chars().all(|c| c.is_ascii_hexdigit()), "hex only: {ia:?}");
|
||||
let (ib, _e2, ok2) = run(&["graph", "introspect", "--identity-id"], &renamed);
|
||||
assert!(ok2, "exit success");
|
||||
assert_eq!(ia, ib.trim(), "renamed twin -> same identity id");
|
||||
let (ca, _e3, ok3) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
|
||||
let (cb, _e4, ok4) = run(&["graph", "introspect", "--content-id"], &renamed);
|
||||
assert!(ok3 && ok4, "exit success");
|
||||
assert_ne!(ca.trim(), cb.trim(), "renamed twin -> different content ids");
|
||||
}
|
||||
|
||||
/// Property (combinable id flags): `--content-id --identity-id` prints both ids,
|
||||
/// one per line, content id first — each byte-equal to its single-flag output,
|
||||
/// and the two differ (SIGNAL_DOC carries debug names the identity form blanks).
|
||||
#[test]
|
||||
fn graph_introspect_content_and_identity_id_combine() {
|
||||
let (both, _e, ok) = run(&["graph", "introspect", "--content-id", "--identity-id"], SIGNAL_DOC);
|
||||
assert!(ok, "exit success");
|
||||
let lines: Vec<&str> = both.lines().collect();
|
||||
assert_eq!(lines.len(), 2, "two lines, one id each: {both:?}");
|
||||
let (content, _e2, ok2) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
|
||||
let (identity, _e3, ok3) = run(&["graph", "introspect", "--identity-id"], SIGNAL_DOC);
|
||||
assert!(ok2 && ok3, "exit success");
|
||||
assert_eq!(lines[0], content.trim(), "content id first");
|
||||
assert_eq!(lines[1], identity.trim(), "identity id second");
|
||||
assert_ne!(lines[0], lines[1], "the two ids differ on a debug-named document");
|
||||
}
|
||||
|
||||
/// Property (negative, mirror of the content-id rejection): `--identity-id` on a
|
||||
/// bad op-list exits non-zero with the cause on stderr and prints no id.
|
||||
#[test]
|
||||
fn graph_introspect_identity_id_rejects_a_bad_document() {
|
||||
let (stdout, stderr, ok) =
|
||||
run(&["graph", "introspect", "--identity-id"], r#"[{"op":"add","type":"Nope"}]"#);
|
||||
assert!(!ok, "non-zero exit on a bad op-list");
|
||||
assert!(stdout.is_empty(), "no identity id emitted on error: {stdout}");
|
||||
assert!(stderr.contains("Nope"), "names the cause: {stderr}");
|
||||
}
|
||||
|
||||
/// Property (usage gate — the previously untested count!=1 dispatch path):
|
||||
/// `graph introspect` with no selection flag is a usage fault — exit 2 with the
|
||||
/// usage line on stderr.
|
||||
#[test]
|
||||
fn graph_introspect_no_flag_is_usage_exit_2() {
|
||||
let (stderr, code) = run_code(&["graph", "introspect"], "");
|
||||
assert_eq!(code, Some(2), "no selection flag is a usage error -> exit 2; stderr: {stderr}");
|
||||
assert!(stderr.contains("Usage"), "prints the usage line: {stderr}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user