Skip to content

test(render): cover the app-show topology renderer's graph and label branches - #466

Merged
pawellisowski merged 2 commits into
mainfrom
routine/test-hygiene-2026-08-27
Aug 27, 2026
Merged

test(render): cover the app-show topology renderer's graph and label branches#466
pawellisowski merged 2 commits into
mainfrom
routine/test-hygiene-2026-08-27

Conversation

@pawellisowski

Copy link
Copy Markdown
Contributor

Summary

  • cli/src/render/topology.rs — the renderer behind aware app show — had two tests, both reading a well-formed substrate fixture. Real .app fixtures have one source, one edge out of each node, no orphans, no loops, and nodes declared in flow order, so every branch that exists for the shapes a hand-written app can actually have was unexercised. Adds 8 hermetic tests; deletes 1 test that could not fail for the reason its name gave.
  • Every added test was proven red by mutating the code it covers, then the code restored. Every mutation is recorded below.
  • Tests only. No production code changed in this PR.

Type of change

  • Other (specify): test coverage

Decalog check

  • This change respects all five decalog truths (app=text, AI=runtime, OSS=inherent, no vendor in the loop, AECO=wedge-not-limit).

Quality bar

Not an agent / skill / app change — those sections deleted.

What was untested

format_topology fans out to three helpers. topological_order walks the edge
chain from the one node that is never a destination, then sweeps up whatever the
walk missed; node_label has three shapes; format_dag renders a grid position
and a connection line, both from Option fields. The two fixture tests reached
the happy path of each and nothing else — no fan-out, no orphan, no cycle, no
node without a command, no inline node, no bare primitive node, no partial
row/col, no unlabelled edge.

Tests added, and the mutation that proves each one bites

Each mutation was applied alone to src/render/topology.rs, the named test run,
and the file restored from a pristine copy before the next.

Test Mutation applied Result
every_node_is_listed_exactly_once_however_the_edges_run deleted the "append unreached nodes" sweep at the end of topological_order FAILED — the orphan node vanished from a render whose header still claimed 4 nodes
linear_flow_follows_the_edges_rather_than_declaration_order current = next_by_id.get(id).copied()current = None (edge walk removed) FAILED
arrows_join_consecutive_nodes_and_never_lead_the_first if i == 0if false && i == 0 in format_linear FAILED — 3 arrows instead of 2, one leading the first node
a_closed_loop_of_edges_still_lists_every_node wrapped the unreached-node sweep in if source.is_some() { … } FAILED — and note the orphan test above stayed green under this same mutation, so this test is the only thing catching it
a_path_that_re_enters_itself_terminates_without_repeating_a_node if !seen.insert(id) { break; }seen.insert(id); plus by_id.removeby_id.get(…).copied(), with a bounded break so the run terminates FAILEDb listed twice. (Removing the guard alone makes the walk non-terminating; I confirmed that too, but a hang is a poor demonstration, so the recorded mutation is the bounded variant that reddens cleanly.)
a_node_label_names_its_agent_command_its_inline_kind_or_neither node.command.as_deref().unwrap_or("?")unwrap_or("") FAILED
dag_shows_a_grid_position_only_when_both_row_and_col_are_given match arm (Some(r), Some(c))(Some(r), c) with c.unwrap_or(0) FAILED — a node given only row: 3 rendered as (row 3, col 0)
dag_connections_carry_label_and_input_and_degrade_to_empty_brackets dropped {input} from the connection-line format string FAILED

On the anti-patterns worth naming in Rust: none of these asserts on a value it
built two lines earlier — each builds a graph and asserts on rendered text.
None round-trips serde. The only unwrap()s are on serde_yaml::from_str of a
literal fixture and on str::find for a substring the same test already
asserted is present, so neither can panic ahead of a real assertion.

Test deleted, and the proof it was not carrying anything

linear_topology_lists_nodes_in_flow_order asserted that welded-to-tc.app
renders tekla-watch before filter-welded before tc-upload. That fixture
declares those three nodes in exactly that order, and topological_order
falls back to declaration order for everything its edge walk does not reach.

Reverse discipline, as required: I broke what it claimed to cover — replaced
current = next_by_id.get(id).copied() with current = None, deleting the edge
walk outright — and it still passed. Under that same mutation the new
linear_flow_follows_the_edges_rather_than_declaration_order failed. So the
ordering claim in the old test's name was never testable by it.

Its residue (the fixture parses, renders linear, names all three nodes) is
asserted end-to-end through the same code path by
tests/app_show.rs::shows_welded_to_tc_linear_topology, so nothing is lost. A
comment at the deletion site records all of this in the file.

dag_topology_lists_nodes_and_connections was put through the same check and
kept: mutating the connection-line format string (->) reddens it, so
it does bite, and its fan-in assertion (excel-lookup → match-build) is not
duplicated in tests/app_show.rs.

Gates

Run from cli/ on the repo-pinned toolchain (1.95.0, per rust-toolchain.toml),
with clang libsecret-1-dev libdbus-1-dev pkg-config installed as CI does:

  • cargo fmt --all -- --check — pass
  • cargo clippy --all-targets -- -D warnings — pass
  • cargo test — pass, 52 test binaries, 0 failures

Notes for reviewers

The two mutations worth your attention are the fourth and fifth rows. The fourth
shows the closed-loop test is load-bearing on its own — the orphan test does not
cover it, because a graph with an orphan still has a source and a closed loop
does not. The fifth is the one place I could not produce a clean red from a
single-line mutation, and the PR body says so rather than dressing it up.


Generated by Claude Code

…branches

`src/render/topology.rs` had two tests, both reading a well-formed substrate
fixture. Those fixtures have one source, one edge out of each node, no orphans,
no loops, and nodes declared in flow order — so every branch that exists for the
shapes a hand-written .app can actually have went unexercised.

Adds eight hermetic tests over topological_order, node_label and format_dag:
fan-out plus an orphan node, a closed edge loop (no source at all), a path that
re-enters itself, edge-driven order against declaration order, arrow placement,
the three node-label forms, partial row/col coordinates, and the degraded
connection line.

Deletes linear_topology_lists_nodes_in_flow_order. Its fixture declares the
three nodes in the order it asserted, and topological_order falls back to
declaration order — so replacing the edge walk with `current = None` left it
green. Its residue is covered end-to-end by
tests/app_show.rs::shows_welded_to_tc_linear_topology.

Copy link
Copy Markdown
Contributor Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: ec01878d8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

A local review panel run against the first commit found defects in the tests it
added. Fixes, each re-proven by the mutation that previously survived:

- dag_shows_a_grid_position_... asserted 'is not positioned' only for lines it
  found, so a format_dag that stopped emitting unpositioned nodes left the loop
  iterating over nothing and the test green. Replacing the '_ => String::new()'
  arm with '_ => continue' survived the whole suite; it now fails. Presence is
  asserted before the property, the (None, Some(col)) arm is covered, and the
  dag header's node count is pinned (len() + 1 also survived before).
- New a_connection_naming_an_unknown_node_... covers the None arm of
  'by_id.remove(id)' — a typo'd connection id, which no test reached. Replacing
  the 'if let' with '.expect(..)' passed every other test in the file.
- listed() matched '[id]' anywhere, and format_dag renders connection labels in
  the same brackets, so a node would double-count in any dag with an edge
  labelled after it. Now anchored to the start of a line.
- The inline fixture used 'kind: python', which validate_app rejects
  (E_APP_INLINE_KIND, only 'predicate' is runnable). The renderer is
  kind-agnostic, so the valid value costs no coverage.
- a_path_that_re_enters_itself_... claimed the seen-guard keeps a node off the
  list twice. 'by_id.remove' does that; the guard supplies termination. Comment
  corrected, and the fixture now declares its nodes out of flow order so the
  added order assertion gives the test a failure mode other than hanging.
- Opaque '.unwrap()' on str::find replaced with a panic naming the missing node.
- Comment corrections: example apps do contain fan-out and multiple sources
  (qa-drawings-to-tekla has both), unlabelled dag edges are the common case, and
  the linear header count comes from app.nodes.len() rather than from the walk.

Copy link
Copy Markdown
Contributor Author

Round 2 — what changed and why

Codex reviewed ec01878d8f and found no major issues. Before that landed I had also started the local pr-review-toolkit panel (I had mis-measured the elapsed time and thought Codex had timed out). It ran anyway, briefed to refute rather than bless — and it found real defects in the tests this PR adds. Since this routine exists specifically to stop vacuous tests reaching main, shipping them to save a review round would have been the wrong trade. So: fixes pushed as f8b3b5d3, and this is a fresh review request on that commit.

Acted on — each was a mutation that previously SURVIVED the whole suite

Finding Mutation that used to survive Now
dag_shows_a_grid_position_… asserted its property only for lines it found. If format_dag stopped emitting unpositioned nodes, the loop iterated over nothing and the test passed. _ => String::new()_ => continue in format_dag FAILS. Presence asserted before the property.
The dag header's node count was asserted nowhere (the linear one was). app.nodes.len()app.nodes.len() + 1 at format_dag FAILS.
The None arm of by_id.remove(id) — a connection naming an id absent from nodes: — was reached by no test. if let Some(node) = ….expect("…") FAILS, via a new test a_connection_naming_an_unknown_node_renders_the_real_nodes_and_no_ghost.
listed() matched [id] anywhere; format_dag renders connection labels in the same brackets, so a node double-counts in any dag with an edge labelled after it. latent — demonstrated directly, not live at any call site Anchored to line start.
The inline fixture used kind: python, which validate_app rejects (E_APP_INLINE_KIND — only predicate is runnable). It was an app the substrate refuses to install, reachable only because app show skips validation. kind: predicate. The renderer is kind-agnostic, so no coverage is lost.
a_path_that_re_enters_itself_… credited the seen guard with keeping a node off the list twice. by_id.remove does that (removeget left the suite green); the guard supplies termination, and deleting it hangs rather than failing. current = next_by_id.get(id).copied()None FAILS. Comment corrected, and the fixture now declares its nodes out of flow order so the added order assertion gives the test a failure mode other than hanging.
s.find("[a]").unwrap() was the "node was rendered" assertion, but reported regressions as an opaque unwrap() on a None value. Panics naming the missing node and showing the render.

Comment corrections in the same commit: the example apps do contain fan-out and multiple sources (qa-drawings-to-tekla.app has both, and says so in its own description:), unlabelled dag edges are the common case rather than the exotic one, and the linear header count comes from app.nodes.len() — not from the walk, as I had written.

While fixing the grid-position test, its new presence assertion immediately caught a flaw in my own fixture: node ids row-only / col-only contain the substrings the check greps for. Renamed to half-a / half-b.

Recorded, deliberately NOT acted on

  • format_linear misrepresents non-linear graphs. It joins topological_order's output with │ ▼ arrows, so a fan-out renders arrows between nodes with no edge between them, and a cyclic app renders as a straight pipeline with the closing edge invisible. validate_app never inspects layout, and show() never validates, so a user can reach this. It also disagrees with orchestrator::topo_order, which is a real Kahn sort. This is a production-behaviour defect, not a test defect, and fixing it is beyond a test-hygiene PR. The affected tests assert membership only, and a comment now says explicitly that they do not bless today's arrow drawing — so a later fix will not have to fight a test that froze it.
  • Which branch of a fan-out survives (next_by_id last-write-wins) is left unasserted on purpose. It is an implementation detail, not a promise; pinning it would freeze an arbitrary choice. The doc comment now says so instead of implying coverage.
  • Exact-format-string coupling in two dag assertions, and residual gaps (zero-node apps, the Nodes:/Connections: section headers, the sweep's documented ordering). Taste and scope respectively — recorded, not swept.
  • One panel member called a_closed_loop_of_edges_still_lists_every_node redundant; another proved it is not, by a mutation (sweep only when source.is_some()) that only that test kills. I verified that mutation myself. Kept.

Gates on f8b3b5d3

cargo fmt --all -- --check, cargo clippy --all-targets -- -D warnings, cargo test (52 binaries, 0 failures) — all pass locally on the repo-pinned 1.95.0 toolchain.

@codex review


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: f8b3b5d380

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pawellisowski
pawellisowski merged commit 78a3495 into main Aug 27, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant