package main import ( "bytes" "strings" "testing" "time" "github.com/carlosframework/platform/internal/status" ) func f64(v float64) *float64 { return &v } func i64(v int64) *int64 { return &v } // TestPrintLiveBoxesSuppressesStaleMetrics is the CLI half of the rule the // console page enforces: a host that stopped reporting prints as UNREACHABLE // with how long it has been dark, and NONE of its last readings. The operator // running this has bucket credentials, so the numbers are theirs to see — the // point is that a dead box's numbers are not a current answer to anything. func TestPrintLiveBoxesSuppressesStaleMetrics(t *testing.T) { now := time.Now() boxes := []status.Box{ { GeneratedAt: now, Label: "ada-stargazing", Region: "eu-west-1", Metrics: &status.BoxMetrics{ MemTotalBytes: i64(2 << 30), MemAvailBytes: i64(1 << 30), Load1: f64(0.42), DiskFreeBytes: i64(5 << 30), Release: "abc1234", Tunnel: "connected", }, }, { GeneratedAt: now.Add(-90 * time.Minute), Label: "grace-kayaking", Region: "eu-west-2", Metrics: &status.BoxMetrics{ MemTotalBytes: i64(9 << 30), MemAvailBytes: i64(7 << 30), Load1: f64(8.25), DiskFreeBytes: i64(3 << 30), Release: "beefcaf", Tunnel: "connected", }, }, } var buf bytes.Buffer printLiveBoxes(&buf, boxes, now) out := buf.String() for _, want := range []string{ "ada-stargazing", "eu-west-1", "release=abc1234", "tunnel=connected", "mem=1.0GB/2.0GB", "load=0.42", "disk-free=5.0GB", "grace-kayaking", "UNREACHABLE", "1h30m0s", } { if !strings.Contains(out, want) { t.Fatalf("missing %q in:\n%s", want, out) } } for _, leaked := range []string{"beefcaf", "8.25", "7.0GB", "9.0GB", "3.0GB"} { if strings.Contains(out, leaked) { t.Fatalf("stale host's reading %q printed:\n%s", leaked, out) } } } // TestPrintLiveBoxesPreUpgradeHost: a box running code older than the metrics // block is fresh and fine — it says so, in the same columns, rather than // printing a row of fabricated zeros or dropping out of the listing. func TestPrintLiveBoxesPreUpgradeHost(t *testing.T) { now := time.Now() var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{ {GeneratedAt: now, Label: "alan-foraging"}, // no metrics, no region {GeneratedAt: now, Label: "iris-sailing", Metrics: &status.BoxMetrics{Tunnel: "reconnecting"}}, }, now) out := buf.String() if !strings.Contains(out, "alan-foraging") || !strings.Contains(out, "no metrics block") { t.Fatalf("pre-upgrade host row wrong:\n%s", out) } // A metrics block with only some fields known dashes the rest — never a // zero, the same "unknown is not a number" rule the status package holds. if !strings.Contains(out, "tunnel=reconnecting") || !strings.Contains(out, "mem=-/-") || !strings.Contains(out, "load=-") || !strings.Contains(out, "disk-free=-") || !strings.Contains(out, "release=-") { t.Fatalf("partial metrics row wrong:\n%s", out) } for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") { if !strings.HasPrefix(line, "alan-foraging") && !strings.HasPrefix(line, "iris-sailing") { t.Fatalf("row does not start with its label (alignment broken):\n%s", out) } } } // TestPrintLiveBoxesSortsByLabel: the listing is stable across runs, so a // diff between two invocations is a change in the fleet, not in list order. func TestPrintLiveBoxesSortsByLabel(t *testing.T) { now := time.Now() var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{ {GeneratedAt: now, Label: "zoe-welding"}, {GeneratedAt: now, Label: "ada-stargazing"}, }, now) if a, z := strings.Index(buf.String(), "ada-"), strings.Index(buf.String(), "zoe-"); a > z { t.Fatalf("rows are not label-sorted:\n%s", buf.String()) } } // TestPrintLiveBoxesSaysWhetherAlertsAreDelivered is discussion #192's // visibility ask: a box with no alert sink must read as not delivered in the // operator's own listing, not as silence. A box that predates the field // prints a dash, since it did not say. A sink whose client could not be // built reads as unavailable, never as a working sink. func TestPrintLiveBoxesSaysWhetherAlertsAreDelivered(t *testing.T) { now := time.Now() var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{ {GeneratedAt: now, Label: "a-none", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{}}}, {GeneratedAt: now, Label: "b-sns", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{SNS: true}}}, {GeneratedAt: now, Label: "c-both", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{Webhook: true, SNS: true, SNSFailures: 3}}}, {GeneratedAt: now, Label: "d-old", Metrics: &status.BoxMetrics{Tunnel: "connected"}}, {GeneratedAt: now, Label: "e-unavail", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{SNSUnavailable: true, SNSFailures: 1}}}, {GeneratedAt: now, Label: "f-hook-unavail", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{Webhook: true, SNSUnavailable: true}}}, }, now) want := map[string]string{ "a-none": "alerts=not-delivered", "b-sns": "alerts=sns\n", "c-both": "alerts=webhook+sns (sns failed 3)", "d-old": "alerts=-", "e-unavail": "alerts=sns-unavailable (sns failed 1)", "f-hook-unavail": "alerts=webhook (sns-unavailable)", } lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") if len(lines) != len(want) { t.Fatalf("got %d rows, want %d:\n%s", len(lines), len(want), buf.String()) } for label, frag := range want { var rows []string for _, line := range lines { if strings.HasPrefix(line, label+" ") { rows = append(rows, line) } } if len(rows) != 1 { t.Errorf("%s: appears on %d rows, want exactly 1:\n%s", label, len(rows), buf.String()) continue } if !strings.Contains(rows[0]+"\n", frag) { t.Errorf("%s: want %q in %q", label, frag, rows[0]) } } } // TestPrintLiveBoxesShowsReleaseRefusals: a build the box rolled back (D1) // reads as its own line under the box, naming the host, the version, since // when and why, so the operator does not have to read the journal. func TestPrintLiveBoxesShowsReleaseRefusals(t *testing.T) { now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{ {GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{ Tunnel: "connected", ReleaseRefusals: []status.ReleaseRefusal{{ Host: "console.example.com", Version: "v2", RefusedAt: time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC), Reason: "exited: status=2", }}, }}, }, now) want := "refused console.example.com v2 since 2026-10-06T09:30:40Z: exited: status=2\n" if !strings.Contains(buf.String(), want) { t.Fatalf("missing %q in:\n%s", want, buf.String()) } } // Final review F6: a rollback that did not come up says so on the same line. func TestPrintLiveBoxesShowsAFailedRollback(t *testing.T) { now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{ {GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{ Tunnel: "connected", ReleaseRefusals: []status.ReleaseRefusal{{ Host: "console.example.com", Version: "v2", RefusedAt: time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC), Reason: "exited: status=2", RecoveryError: "start console.service: exit status 1", }}, }}, }, now) want := "refused console.example.com v2 since 2026-10-06T09:30:40Z: exited: status=2 (rollback failed: start console.service: exit status 1)" if !strings.Contains(buf.String(), want) { t.Fatalf("missing %q in:\n%s", want, buf.String()) } } // A wrong-program refusal (D2) is not a failed start. func TestPrintLiveBoxesShowsAWrongProgramRefusal(t *testing.T) { now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{ {GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{ Tunnel: "connected", ReleaseRefusals: []status.ReleaseRefusal{{ Host: "console.example.com", Version: "v2", Kind: "wrong-program", RefusedAt: time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC), Reason: "wrong program: example.com/carlos", }}, }}, }, now) want := "refused console.example.com v2 since 2026-10-06T09:30:40Z: wrong program (example.com/carlos)\n" if !strings.Contains(buf.String(), want) { t.Fatalf("missing %q in:\n%s", want, buf.String()) } } // A not-proven refusal (D3, plan 3 ruling 3) is neither a failed start nor a // rollback: the staged bytes did not match dev's proof. func TestPrintLiveBoxesShowsANotProvenRefusal(t *testing.T) { now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{ {GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{ Tunnel: "connected", ReleaseRefusals: []status.ReleaseRefusal{{ Host: "console.example.com", Version: "v2", Kind: "not-proven", RefusedAt: time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC), Reason: "not proven: staged sha256 aaaa, dev proved bbbb", }}, }}, }, now) want := "refused console.example.com v2 since 2026-10-06T09:30:40Z: staged bytes do not match dev's proof (staged sha256 aaaa, dev proved bbbb)\n" if !strings.Contains(buf.String(), want) { t.Fatalf("missing %q in:\n%s", want, buf.String()) } if strings.Contains(buf.String(), "rollback") { t.Fatalf("a not-proven row mentions a rollback:\n%s", buf.String()) } } // D3: a box holding a host on its current build for a dev proof says so, with // the short sha and when the wait began. func TestPrintLiveBoxesShowsAProofWait(t *testing.T) { now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{{GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{ ProofWaits: []status.ProofWait{{Host: "console.example.com", Version: "v2", SHA256: "e47fcc1e0123456789abcdef", Prover: "dev", Since: now.Add(-time.Hour)}}, }}}, now) want := " waiting for a dev proof: console.example.com v2 (sha256 e47fcc1e0123) since 2026-10-06T09:00:00Z" if !strings.Contains(buf.String(), want) { t.Fatalf("missing %q in:\n%s", want, buf.String()) } } // Ruling 7: with no prover named the wait is a policy problem. The line says // so and prints the policy's own error exactly as given, never "a proof". func TestPrintLiveBoxesShowsAPolicyProofWait(t *testing.T) { now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{{GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{ ProofWaits: []status.ProofWait{{Host: "console.example.com", Version: "v2", Reason: "proof policy: line 3: unknown prover", Since: now.Add(-time.Hour)}}, }}}, now) out := buf.String() want := " waiting for a proof (policy: proof policy: line 3: unknown prover): console.example.com v2 (sha256 -) since 2026-10-06T09:00:00Z" if !strings.Contains(out, want) { t.Fatalf("missing %q in:\n%s", want, out) } if strings.Contains(out, "a proof") || strings.Contains(out, "a - proof") { t.Fatalf("an empty prover printed as a blank or dash:\n%s", out) } } // A named prover's wait carries the gate's reason through unchanged. func TestPrintLiveBoxesShowsAProofWaitReason(t *testing.T) { now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) var buf bytes.Buffer printLiveBoxes(&buf, []status.Box{{GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{ ProofWaits: []status.ProofWait{{Host: "console.example.com", Version: "v2", SHA256: "abc", Prover: "dev", Reason: "proof from dev: signature does not verify", Since: now}}, }}}, now) want := "since 2026-10-06T10:00:00Z: proof from dev: signature does not verify\n" if !strings.Contains(buf.String(), want) { t.Fatalf("missing %q in:\n%s", want, buf.String()) } } func TestHumanBytesAndFloat(t *testing.T) { for _, c := range []struct { in *int64 want string }{ {nil, "-"}, {i64(0), "0B"}, {i64(512), "512B"}, {i64(1536), "1.5KB"}, {i64(3 << 20), "3.0MB"}, {i64(2 << 30), "2.0GB"}, } { if got := humanBytes(c.in); got != c.want { t.Errorf("humanBytes(%v) = %q, want %q", c.in, got, c.want) } } if got := humanFloat(nil); got != "-" { t.Errorf("humanFloat(nil) = %q, want -", got) } if got := humanFloat(f64(1.5)); got != "1.50" { t.Errorf("humanFloat(1.5) = %q, want 1.50", got) } if got := orDash(""); got != "-" { t.Errorf("orDash(\"\") = %q, want -", got) } }