console: clamp layout dimensions for an empty graph, pin grouped-record label text
Review fixes on Task 6:
- archlayout.go: layers and maxRows were 0 for a graph with no drawable
nodes (an ungranted console with no boxes and no inventory, day one),
driving Width negative (padding*2 + 0*nodeW + (0-1)*gapX = -42) and
producing an invalid viewBox on the emptiest page rather than the
busiest. Clamp both to at least 1 before computing Width/Height.
TestLayoutEmptyGraphHasValidViewBox builds BuildArch(ArchInput{})
(zero nodes) and asserts positive Width/Height plus a blunt string
check that viewBox never carries a negative dimension.
- archgraph_test.go: TestArchGroupsRecordsBySameZoneAndTarget now also
asserts the grouped node's Title ("21 records") and Subtitle (3
sorted example names + "+18"), pinning the half of Ruling 19 that
was previously only exercised by node/row counts.
Verified: go build ./..., go vet ./..., go test ./... -count=1 (all 28
packages), gofmt -l internal/console, GOOS=darwin go build ./... — all
clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3 files changed,
+49
−0
internal/console/archgraph_test.go+19 −0internal/console/archlayout.go+10 −0internal/console/archlayout_test.go+20 −0
diff --git a/internal/console/archgraph_test.go b/internal/console/archgraph_test.go| index b3dce8c09..272ee4b89 100644 |
| --- a/internal/console/archgraph_test.go |
| +++ b/internal/console/archgraph_test.go |
| @@ -425,6 +425,25 @@ func TestArchGroupsRecordsBySameZoneAndTarget(t *testing.T) { |
| t.Errorf("grouped node must use its lowest-id member's id: %+v", v.Nodes) |
| } |
| + // Ruling 19 asked the grouped node to carry the count AND up to 3 |
| + // example names — pin the label text, not just the node count. |
| + var recordNode *Node |
| + for i := range v.Nodes { |
| + if v.Nodes[i].Kind == "record" { |
| + recordNode = &v.Nodes[i] |
| + } |
| + } |
| + if recordNode == nil { |
| + t.Fatal("no record node drawn") |
| + } |
| + if recordNode.Title != "21 records" { |
| + t.Errorf("Title = %q, want %q", recordNode.Title, "21 records") |
| + } |
| + wantSub := "app00.carloku.com, app01.carloku.com, app02.carloku.com, +18" |
| + if recordNode.Subtitle != wantSub { |
| + t.Errorf("Subtitle = %q, want %q (3 sorted example names + the remainder)", recordNode.Subtitle, wantSub) |
| + } |
| + |
| var dnsRows int |
| for _, g := range v.Groups { |
| if g.Title == "DNS" { |
diff --git a/internal/console/archlayout.go b/internal/console/archlayout.go| index dd37133f5..fa5ddb166 100644 |
| --- a/internal/console/archlayout.go |
| +++ b/internal/console/archlayout.go |
| @@ -139,6 +139,16 @@ func layout(v *ArchView) { |
| layers = l + 1 |
| } |
| } |
| + // An empty graph (no boxes, no inventory yet — an ungranted console on |
| + // day one) still has to carry a valid, positive viewBox: it is the |
| + // FIRST page an operator sees, not the busiest. Without this clamp |
| + // layers==0 and maxRows==0 drive Width/Height negative. |
| + if layers < 1 { |
| + layers = 1 |
| + } |
| + if maxRows < 1 { |
| + maxRows = 1 |
| + } |
| v.Width = padding*2 + layers*nodeW + (layers-1)*gapX |
| v.Height = padding*2 + maxRows*nodeH + (maxRows-1)*gapY |
| } |
diff --git a/internal/console/archlayout_test.go b/internal/console/archlayout_test.go| index d113e5792..579be79fc 100644 |
| --- a/internal/console/archlayout_test.go |
| +++ b/internal/console/archlayout_test.go |
| @@ -70,3 +70,23 @@ func TestSVGIsSelfContainedAndEscaped(t *testing.T) { |
| t.Error("a box label must never reach the SVG unescaped") |
| } |
| } |
| + |
| +// A deployment with no boxes and no inventory yet — an ungranted console on |
| +// day one — must still draw a valid, positive viewBox. This is the FIRST |
| +// page an operator sees after this ships, not a contrived corner case. |
| +func TestLayoutEmptyGraphHasValidViewBox(t *testing.T) { |
| + v := BuildArch(ArchInput{}) |
| + if len(v.Nodes) != 0 { |
| + t.Fatalf("this fixture is expected to draw no nodes at all, got %d", len(v.Nodes)) |
| + } |
| + if v.Width <= 0 || v.Height <= 0 { |
| + t.Fatalf("Width/Height must be positive even with zero nodes: %dx%d", v.Width, v.Height) |
| + } |
| + out := string(RenderArchSVG(v)) |
| + if !strings.Contains(out, "viewBox=") { |
| + t.Error("SVG must carry a viewBox even when the graph is empty") |
| + } |
| + if strings.Contains(out, `viewBox="0 0 -`) { |
| + t.Error("viewBox must never carry a negative dimension") |
| + } |
| +} |