carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/status"
10)
11
12func f64(v float64) *float64 { return &v }
13func i64(v int64) *int64 { return &v }
14
15// TestPrintLiveBoxesSuppressesStaleMetrics is the CLI half of the rule the
16// console page enforces: a host that stopped reporting prints as UNREACHABLE
17// with how long it has been dark, and NONE of its last readings. The operator
18// running this has bucket credentials, so the numbers are theirs to see — the
19// point is that a dead box's numbers are not a current answer to anything.
20func TestPrintLiveBoxesSuppressesStaleMetrics(t *testing.T) {
21 now := time.Now()
22 boxes := []status.Box{
23 {
24 GeneratedAt: now, Label: "ada-stargazing", Region: "eu-west-1",
25 Metrics: &status.BoxMetrics{
26 MemTotalBytes: i64(2 << 30), MemAvailBytes: i64(1 << 30),
27 Load1: f64(0.42), DiskFreeBytes: i64(5 << 30),
28 Release: "abc1234", Tunnel: "connected",
29 },
30 },
31 {
32 GeneratedAt: now.Add(-90 * time.Minute), Label: "grace-kayaking", Region: "eu-west-2",
33 Metrics: &status.BoxMetrics{
34 MemTotalBytes: i64(9 << 30), MemAvailBytes: i64(7 << 30),
35 Load1: f64(8.25), DiskFreeBytes: i64(3 << 30),
36 Release: "beefcaf", Tunnel: "connected",
37 },
38 },
39 }
40 var buf bytes.Buffer
41 printLiveBoxes(&buf, boxes, now)
42 out := buf.String()
43
44 for _, want := range []string{
45 "ada-stargazing", "eu-west-1", "release=abc1234", "tunnel=connected",
46 "mem=1.0GB/2.0GB", "load=0.42", "disk-free=5.0GB",
47 "grace-kayaking", "UNREACHABLE", "1h30m0s",
48 } {
49 if !strings.Contains(out, want) {
50 t.Fatalf("missing %q in:\n%s", want, out)
51 }
52 }
53 for _, leaked := range []string{"beefcaf", "8.25", "7.0GB", "9.0GB", "3.0GB"} {
54 if strings.Contains(out, leaked) {
55 t.Fatalf("stale host's reading %q printed:\n%s", leaked, out)
56 }
57 }
58}
59
60// TestPrintLiveBoxesPreUpgradeHost: a box running code older than the metrics
61// block is fresh and fine — it says so, in the same columns, rather than
62// printing a row of fabricated zeros or dropping out of the listing.
63func TestPrintLiveBoxesPreUpgradeHost(t *testing.T) {
64 now := time.Now()
65 var buf bytes.Buffer
66 printLiveBoxes(&buf, []status.Box{
67 {GeneratedAt: now, Label: "alan-foraging"}, // no metrics, no region
68 {GeneratedAt: now, Label: "iris-sailing", Metrics: &status.BoxMetrics{Tunnel: "reconnecting"}},
69 }, now)
70 out := buf.String()
71 if !strings.Contains(out, "alan-foraging") || !strings.Contains(out, "no metrics block") {
72 t.Fatalf("pre-upgrade host row wrong:\n%s", out)
73 }
74 // A metrics block with only some fields known dashes the rest — never a
75 // zero, the same "unknown is not a number" rule the status package holds.
76 if !strings.Contains(out, "tunnel=reconnecting") || !strings.Contains(out, "mem=-/-") ||
77 !strings.Contains(out, "load=-") || !strings.Contains(out, "disk-free=-") ||
78 !strings.Contains(out, "release=-") {
79 t.Fatalf("partial metrics row wrong:\n%s", out)
80 }
81 for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
82 if !strings.HasPrefix(line, "alan-foraging") && !strings.HasPrefix(line, "iris-sailing") {
83 t.Fatalf("row does not start with its label (alignment broken):\n%s", out)
84 }
85 }
86}
87
88// TestPrintLiveBoxesSortsByLabel: the listing is stable across runs, so a
89// diff between two invocations is a change in the fleet, not in list order.
90func TestPrintLiveBoxesSortsByLabel(t *testing.T) {
91 now := time.Now()
92 var buf bytes.Buffer
93 printLiveBoxes(&buf, []status.Box{
94 {GeneratedAt: now, Label: "zoe-welding"},
95 {GeneratedAt: now, Label: "ada-stargazing"},
96 }, now)
97 if a, z := strings.Index(buf.String(), "ada-"), strings.Index(buf.String(), "zoe-"); a > z {
98 t.Fatalf("rows are not label-sorted:\n%s", buf.String())
99 }
100}
101
102// TestPrintLiveBoxesSaysWhetherAlertsAreDelivered is discussion #192's
103// visibility ask: a box with no alert sink must read as not delivered in the
104// operator's own listing, not as silence. A box that predates the field
105// prints a dash, since it did not say. A sink whose client could not be
106// built reads as unavailable, never as a working sink.
107func TestPrintLiveBoxesSaysWhetherAlertsAreDelivered(t *testing.T) {
108 now := time.Now()
109 var buf bytes.Buffer
110 printLiveBoxes(&buf, []status.Box{
111 {GeneratedAt: now, Label: "a-none", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{}}},
112 {GeneratedAt: now, Label: "b-sns", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{SNS: true}}},
113 {GeneratedAt: now, Label: "c-both", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{Webhook: true, SNS: true, SNSFailures: 3}}},
114 {GeneratedAt: now, Label: "d-old", Metrics: &status.BoxMetrics{Tunnel: "connected"}},
115 {GeneratedAt: now, Label: "e-unavail", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{SNSUnavailable: true, SNSFailures: 1}}},
116 {GeneratedAt: now, Label: "f-hook-unavail", Metrics: &status.BoxMetrics{Alerts: &status.AlertDelivery{Webhook: true, SNSUnavailable: true}}},
117 }, now)
118 want := map[string]string{
119 "a-none": "alerts=not-delivered",
120 "b-sns": "alerts=sns\n",
121 "c-both": "alerts=webhook+sns (sns failed 3)",
122 "d-old": "alerts=-",
123 "e-unavail": "alerts=sns-unavailable (sns failed 1)",
124 "f-hook-unavail": "alerts=webhook (sns-unavailable)",
125 }
126 lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
127 if len(lines) != len(want) {
128 t.Fatalf("got %d rows, want %d:\n%s", len(lines), len(want), buf.String())
129 }
130 for label, frag := range want {
131 var rows []string
132 for _, line := range lines {
133 if strings.HasPrefix(line, label+" ") {
134 rows = append(rows, line)
135 }
136 }
137 if len(rows) != 1 {
138 t.Errorf("%s: appears on %d rows, want exactly 1:\n%s", label, len(rows), buf.String())
139 continue
140 }
141 if !strings.Contains(rows[0]+"\n", frag) {
142 t.Errorf("%s: want %q in %q", label, frag, rows[0])
143 }
144 }
145}
146
147// TestPrintLiveBoxesShowsReleaseRefusals: a build the box rolled back (D1)
148// reads as its own line under the box, naming the host, the version, since
149// when and why, so the operator does not have to read the journal.
150func TestPrintLiveBoxesShowsReleaseRefusals(t *testing.T) {
151 now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
152 var buf bytes.Buffer
153 printLiveBoxes(&buf, []status.Box{
154 {GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{
155 Tunnel: "connected",
156 ReleaseRefusals: []status.ReleaseRefusal{{
157 Host: "console.example.com", Version: "v2",
158 RefusedAt: time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC), Reason: "exited: status=2",
159 }},
160 }},
161 }, now)
162 want := "refused console.example.com v2 since 2026-10-06T09:30:40Z: exited: status=2\n"
163 if !strings.Contains(buf.String(), want) {
164 t.Fatalf("missing %q in:\n%s", want, buf.String())
165 }
166}
167
168// Final review F6: a rollback that did not come up says so on the same line.
169func TestPrintLiveBoxesShowsAFailedRollback(t *testing.T) {
170 now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
171 var buf bytes.Buffer
172 printLiveBoxes(&buf, []status.Box{
173 {GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{
174 Tunnel: "connected",
175 ReleaseRefusals: []status.ReleaseRefusal{{
176 Host: "console.example.com", Version: "v2",
177 RefusedAt: time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC), Reason: "exited: status=2",
178 RecoveryError: "start console.service: exit status 1",
179 }},
180 }},
181 }, now)
182 want := "refused console.example.com v2 since 2026-10-06T09:30:40Z: exited: status=2 (rollback failed: start console.service: exit status 1)"
183 if !strings.Contains(buf.String(), want) {
184 t.Fatalf("missing %q in:\n%s", want, buf.String())
185 }
186}
187
188// A wrong-program refusal (D2) is not a failed start.
189func TestPrintLiveBoxesShowsAWrongProgramRefusal(t *testing.T) {
190 now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
191 var buf bytes.Buffer
192 printLiveBoxes(&buf, []status.Box{
193 {GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{
194 Tunnel: "connected",
195 ReleaseRefusals: []status.ReleaseRefusal{{
196 Host: "console.example.com", Version: "v2", Kind: "wrong-program",
197 RefusedAt: time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC), Reason: "wrong program: example.com/carlos",
198 }},
199 }},
200 }, now)
201 want := "refused console.example.com v2 since 2026-10-06T09:30:40Z: wrong program (example.com/carlos)\n"
202 if !strings.Contains(buf.String(), want) {
203 t.Fatalf("missing %q in:\n%s", want, buf.String())
204 }
205}
206
207// A not-proven refusal (D3, plan 3 ruling 3) is neither a failed start nor a
208// rollback: the staged bytes did not match dev's proof.
209func TestPrintLiveBoxesShowsANotProvenRefusal(t *testing.T) {
210 now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
211 var buf bytes.Buffer
212 printLiveBoxes(&buf, []status.Box{
213 {GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{
214 Tunnel: "connected",
215 ReleaseRefusals: []status.ReleaseRefusal{{
216 Host: "console.example.com", Version: "v2", Kind: "not-proven",
217 RefusedAt: time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC), Reason: "not proven: staged sha256 aaaa, dev proved bbbb",
218 }},
219 }},
220 }, now)
221 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"
222 if !strings.Contains(buf.String(), want) {
223 t.Fatalf("missing %q in:\n%s", want, buf.String())
224 }
225 if strings.Contains(buf.String(), "rollback") {
226 t.Fatalf("a not-proven row mentions a rollback:\n%s", buf.String())
227 }
228}
229
230// D3: a box holding a host on its current build for a dev proof says so, with
231// the short sha and when the wait began.
232func TestPrintLiveBoxesShowsAProofWait(t *testing.T) {
233 now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
234 var buf bytes.Buffer
235 printLiveBoxes(&buf, []status.Box{{GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{
236 ProofWaits: []status.ProofWait{{Host: "console.example.com", Version: "v2",
237 SHA256: "e47fcc1e0123456789abcdef", Prover: "dev", Since: now.Add(-time.Hour)}},
238 }}}, now)
239 want := " waiting for a dev proof: console.example.com v2 (sha256 e47fcc1e0123) since 2026-10-06T09:00:00Z"
240 if !strings.Contains(buf.String(), want) {
241 t.Fatalf("missing %q in:\n%s", want, buf.String())
242 }
243}
244
245// Ruling 7: with no prover named the wait is a policy problem. The line says
246// so and prints the policy's own error exactly as given, never "a proof".
247func TestPrintLiveBoxesShowsAPolicyProofWait(t *testing.T) {
248 now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
249 var buf bytes.Buffer
250 printLiveBoxes(&buf, []status.Box{{GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{
251 ProofWaits: []status.ProofWait{{Host: "console.example.com", Version: "v2",
252 Reason: "proof policy: line 3: unknown prover", Since: now.Add(-time.Hour)}},
253 }}}, now)
254 out := buf.String()
255 want := " waiting for a proof (policy: proof policy: line 3: unknown prover): console.example.com v2 (sha256 -) since 2026-10-06T09:00:00Z"
256 if !strings.Contains(out, want) {
257 t.Fatalf("missing %q in:\n%s", want, out)
258 }
259 if strings.Contains(out, "a proof") || strings.Contains(out, "a - proof") {
260 t.Fatalf("an empty prover printed as a blank or dash:\n%s", out)
261 }
262}
263
264// A named prover's wait carries the gate's reason through unchanged.
265func TestPrintLiveBoxesShowsAProofWaitReason(t *testing.T) {
266 now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
267 var buf bytes.Buffer
268 printLiveBoxes(&buf, []status.Box{{GeneratedAt: now, Label: "ada-stargazing", Metrics: &status.BoxMetrics{
269 ProofWaits: []status.ProofWait{{Host: "console.example.com", Version: "v2", SHA256: "abc",
270 Prover: "dev", Reason: "proof from dev: signature does not verify", Since: now}},
271 }}}, now)
272 want := "since 2026-10-06T10:00:00Z: proof from dev: signature does not verify\n"
273 if !strings.Contains(buf.String(), want) {
274 t.Fatalf("missing %q in:\n%s", want, buf.String())
275 }
276}
277
278func TestHumanBytesAndFloat(t *testing.T) {
279 for _, c := range []struct {
280 in *int64
281 want string
282 }{
283 {nil, "-"},
284 {i64(0), "0B"},
285 {i64(512), "512B"},
286 {i64(1536), "1.5KB"},
287 {i64(3 << 20), "3.0MB"},
288 {i64(2 << 30), "2.0GB"},
289 } {
290 if got := humanBytes(c.in); got != c.want {
291 t.Errorf("humanBytes(%v) = %q, want %q", c.in, got, c.want)
292 }
293 }
294 if got := humanFloat(nil); got != "-" {
295 t.Errorf("humanFloat(nil) = %q, want -", got)
296 }
297 if got := humanFloat(f64(1.5)); got != "1.50" {
298 t.Errorf("humanFloat(1.5) = %q, want 1.50", got)
299 }
300 if got := orDash(""); got != "-" {
301 t.Errorf("orDash(\"\") = %q, want -", got)
302 }
303}
304