console: prune architecture edges whose endpoints are not drawn
A partial inventory is normal — infraview.Inventory.Partial exists for it — so an EIP whose instance read failed, or an alarm naming a machine that has since been terminated, would leave an arrow pointing at nothing. One rule, applied last, so every join above can read whatever it reads. Also drops an unused parameter from addBoxNodes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 files changed,
+50
−2
internal/console/archgraph.go+25 −2internal/console/archgraph_test.go+25 −0
diff --git a/internal/console/archgraph.go b/internal/console/archgraph.go| index 4db0a14e5..1e52a133f 100644 |
| --- a/internal/console/archgraph.go |
| +++ b/internal/console/archgraph.go |
| @@ -143,13 +143,36 @@ func BuildArch(in ArchInput) ArchView { |
| } |
| addDNSNodes(&v, in.Inv, claimed) |
| - addBoxNodes(&v, in, claimed) |
| + addBoxNodes(&v, in) |
| addMachineNodes(&v, in.Inv, claimed) |
| addAlarmNodes(&v, in.Inv, claimed) |
| v.Groups = resourceGroups(in.Inv) |
| + pruneDanglingEdges(&v) |
| return v |
| } |
| +// pruneDanglingEdges drops any edge whose endpoints this view does not |
| +// draw. One rule in one place, applied last, so every pass above can join on |
| +// whatever it reads without each of them checking. It matters because a |
| +// PARTIAL inventory is normal (infraview.Inventory.Partial): an EIP whose |
| +// instance read failed, or an alarm naming a machine that has since been |
| +// terminated, would otherwise leave an arrow pointing at nothing — and an |
| +// arrow pointing at nothing is exactly the kind of claim this page exists |
| +// to avoid making. |
| +func pruneDanglingEdges(v *ArchView) { |
| + drawn := make(map[string]bool, len(v.Nodes)) |
| + for _, n := range v.Nodes { |
| + drawn[n.ID] = true |
| + } |
| + kept := v.Edges[:0] |
| + for _, e := range v.Edges { |
| + if drawn[e.From] && drawn[e.To] { |
| + kept = append(kept, e) |
| + } |
| + } |
| + v.Edges = kept |
| +} |
| + |
| // ---- the joins --------------------------------------------------------- |
| // machineFor matches a machine's Name tag against a box label, case- |
| @@ -399,7 +422,7 @@ func addDNSNodes(v *ArchView, inv *infraview.Inventory, claimed map[string]strin |
| // |
| // One apps node per box, never one per instance: a fleet of a hundred |
| // tenants must still produce a diagram a person can read (spec §6.4). |
| -func addBoxNodes(v *ArchView, in ArchInput, claimed map[string]string) { |
| +func addBoxNodes(v *ArchView, in ArchInput) { |
| for _, b := range v.Boxes { |
| boxID := "box:" + b.Label |
| v.Nodes = append(v.Nodes, Node{ |
diff --git a/internal/console/archgraph_test.go b/internal/console/archgraph_test.go| index 20f4bd402..a4f072c6e 100644 |
| --- a/internal/console/archgraph_test.go |
| +++ b/internal/console/archgraph_test.go |
| @@ -352,6 +352,31 @@ func TestArchBucketEdgeOnlyWhenNamed(t *testing.T) { |
| } |
| } |
| +// A partial inventory is normal, so an attribute naming a resource the |
| +// estate read never returned must not leave an arrow pointing at nothing. |
| +func TestArchDrawsNoEdgeToAnUndrawnNode(t *testing.T) { |
| + in := archFixture() |
| + in.Inv.Resources = append(in.Inv.Resources, |
| + infraview.Resource{Kind: infraview.KindIP, ID: "eipalloc-2", |
| + Attrs: map[string]string{"address": "52.0.0.2", "attached_to": "i-gone"}}, |
| + infraview.Resource{Kind: infraview.KindAlarm, ID: "a-gone", |
| + Attrs: map[string]string{"target": "i-gone"}}, |
| + ) |
| + v := BuildArch(in) |
| + drawn := map[string]bool{} |
| + for _, n := range v.Nodes { |
| + drawn[n.ID] = true |
| + } |
| + for _, e := range v.Edges { |
| + if !drawn[e.From] || !drawn[e.To] { |
| + t.Errorf("edge %+v points at a node this view does not draw", e) |
| + } |
| + } |
| + if !hasNode(v, "ip:eipalloc-2") { |
| + t.Error("an IP attached to a machine the read missed is still part of the estate") |
| + } |
| +} |
| + |
| // An alarm watching a machine is an edge to the BOX running on it — the |
| // operator's question is "is this box watched?", not "is i-123 watched?". |
| func TestArchAlarmWatchesItsBox(t *testing.T) { |