| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "errors" |
| 6 | "os/exec" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | func TestSystemVerbAcceptsTheComponentForm(t *testing.T) { |
| 13 | cases := []struct { |
| 14 | in []string |
| 15 | verb, comp string |
| 16 | rest int |
| 17 | }{ |
| 18 | {[]string{"update", "--channel", "canary"}, "update", "router", 2}, |
| 19 | {[]string{"router", "update"}, "update", "router", 0}, |
| 20 | {[]string{"router", "status"}, "status", "router", 0}, |
| 21 | {[]string{"apply", "--recover"}, "apply", "router", 1}, |
| 22 | // The system units' floor gate: both file paths stay in rest. |
| 23 | {[]string{"floor-allows-prev", "/data/.moves/floor.min-release", "/opt/carlos/carlos.prev.allowed"}, "floor-allows-prev", "router", 2}, |
| 24 | // antivirus is a family: its sub-verb stays in rest untouched. |
| 25 | {[]string{"antivirus", "status"}, "antivirus", "router", 1}, |
| 26 | {[]string{"antivirus", "enable", "--console", "x"}, "antivirus", "router", 3}, |
| 27 | {[]string{"antivirus"}, "antivirus", "router", 0}, |
| 28 | {[]string{"files", "seed", "--host", "b.example"}, "files", "router", 3}, |
| 29 | // layout is a family too: its sub-verb and flags stay in rest. |
| 30 | {[]string{"layout", "migrate", "--box", "x"}, "layout", "router", 3}, |
| 31 | {[]string{"layout", "revert", "--force", "--yes"}, "layout", "router", 3}, |
| 32 | {[]string{"layout", "status"}, "layout", "router", 1}, |
| 33 | // claims is a family: its command and flags stay in rest. |
| 34 | {[]string{"claims", "inspect", "--bucket", "b"}, "claims", "router", 3}, |
| 35 | // Region HA joining (spec 2 §5): host-env is a family; join and |
| 36 | // install-channel take no arguments. |
| 37 | {[]string{"host-env", "write", "--deployment", "flagship", "--region", "ie"}, "host-env", "router", 5}, |
| 38 | {[]string{"install-channel"}, "install-channel", "router", 0}, |
| 39 | {[]string{"join"}, "join", "router", 0}, |
| 40 | {[]string{}, "", "router", 0}, |
| 41 | {[]string{"router"}, "", "router", 0}, |
| 42 | } |
| 43 | for _, c := range cases { |
| 44 | verb, rest, comp := systemVerb(c.in) |
| 45 | if verb != c.verb || comp != c.comp || len(rest) != c.rest { |
| 46 | t.Errorf("systemVerb(%v) = %q %v %q", c.in, verb, rest, comp) |
| 47 | } |
| 48 | } |
| 49 | if verb, _, _ := systemVerb([]string{"console", "update"}); verb != "" { |
| 50 | t.Errorf("an unknown component must not be a verb: got %q", verb) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // TestSystemUnknownVerbMessageNamesTheVerbNotTheComponent holds the one thing |
| 55 | // the message must never do: report `carlos system router console` as an |
| 56 | // unknown verb "router". systemVerb has already eaten the component by then, |
| 57 | // so the message has to read what is LEFT, not what arrived. |
| 58 | func TestSystemUnknownVerbMessageNamesTheVerbNotTheComponent(t *testing.T) { |
| 59 | cases := []struct { |
| 60 | in []string |
| 61 | want string |
| 62 | }{ |
| 63 | {[]string{"console"}, `system: unknown verb "console"`}, |
| 64 | {[]string{"router", "console"}, `system: unknown verb "console"`}, |
| 65 | {[]string{"router"}, "system: no verb given"}, |
| 66 | {[]string{}, ""}, |
| 67 | } |
| 68 | for _, c := range cases { |
| 69 | _, rest, _ := systemVerb(c.in) |
| 70 | if got := systemUnknownVerbMessage(c.in, rest); got != c.want { |
| 71 | t.Errorf("carlos system %v: message = %q, want %q", c.in, got, c.want) |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | func TestPrintSystemStatusSaysWhatEachBoxIsDoing(t *testing.T) { |
| 77 | resp := systemStatusResponse{ |
| 78 | Pointers: map[string]*systemPointerRow{"canary": {Version: "v2", Spread: "5m0s"}, "stable": nil}, |
| 79 | Boxes: []systemBoxRow{ |
| 80 | {Label: "ie", Platform: "linux-arm64", Channel: "canary", Release: "v2", Target: "v2"}, |
| 81 | {Label: "us", Platform: "linux-arm64", Channel: "canary", Release: "v1", Target: "v2", |
| 82 | System: &systemStateRow{Version: "v2", Outcome: "rolled-back", Reason: "gate: carlos-edge is failed"}}, |
| 83 | {Label: "au", Platform: "", Channel: "stable", Release: "v1", Target: "", Stale: true}, |
| 84 | // Two separate facts, so two separate boxes: "au" is stale and |
| 85 | // nothing it last said can be trusted, including its blank |
| 86 | // platform; "old" is answering, on a build that predates the |
| 87 | // platform field. A box that is both must read as UNREACHABLE. |
| 88 | {Label: "old", Platform: "", Channel: "stable", Release: "v0", Target: "v1"}, |
| 89 | }, |
| 90 | } |
| 91 | var b strings.Builder |
| 92 | printSystemStatus(&b, resp, "") |
| 93 | out := b.String() |
| 94 | for _, want := range []string{"canary v2", "stable (no pointer)", "ie ", "current", "us ", "rolled-back", "gate: carlos-edge is failed", "au ", "UNREACHABLE", "pre-upgrade"} { |
| 95 | if !strings.Contains(out, want) { |
| 96 | t.Errorf("status output lacks %q:\n%s", want, out) |
| 97 | } |
| 98 | } |
| 99 | var only strings.Builder |
| 100 | printSystemStatus(&only, resp, "stable") |
| 101 | if strings.Contains(only.String(), "ie ") { |
| 102 | t.Error("--channel stable still listed a canary box") |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | // TestPrintSystemStatusShowsWhatItCouldNotRead is the other half: a pointer |
| 107 | // this console cannot verify and a fleet it could not reach are the two |
| 108 | // answers an operator must never read as "fine". Neither may render as the |
| 109 | // thing it is not — a bad pointer as "never promoted", a short box list as |
| 110 | // the whole fleet. |
| 111 | func TestPrintSystemStatusShowsWhatItCouldNotRead(t *testing.T) { |
| 112 | resp := systemStatusResponse{ |
| 113 | Pointers: map[string]*systemPointerRow{ |
| 114 | "canary": {Error: "signature does not verify"}, |
| 115 | "stable": {Version: "v1", Spread: "5m0s"}, |
| 116 | }, |
| 117 | Boxes: []systemBoxRow{{Label: "ie", Platform: "linux-arm64", Channel: "stable", Release: "v1", Target: "v1"}}, |
| 118 | Partial: true, |
| 119 | } |
| 120 | var b strings.Builder |
| 121 | printSystemStatus(&b, resp, "") |
| 122 | out := b.String() |
| 123 | for _, want := range []string{"canary (pointer does not verify: signature does not verify)", "fleet boxes could not all be read"} { |
| 124 | if !strings.Contains(out, want) { |
| 125 | t.Errorf("status output lacks %q:\n%s", want, out) |
| 126 | } |
| 127 | } |
| 128 | if strings.Contains(out, "no pointer") { |
| 129 | t.Errorf("an unverifiable pointer must not read as an absent one:\n%s", out) |
| 130 | } |
| 131 | |
| 132 | var quiet strings.Builder |
| 133 | printSystemStatus(&quiet, systemStatusResponse{Boxes: resp.Boxes}, "") |
| 134 | if strings.Contains(quiet.String(), "could not all be read") { |
| 135 | t.Errorf("a complete fleet warned anyway:\n%s", quiet.String()) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | func TestSystemLadderWarning(t *testing.T) { |
| 140 | onCanary := map[string]*systemPointerRow{"canary": {Version: "v9"}} |
| 141 | never := map[string]*systemPointerRow{} |
| 142 | older := map[string]*systemPointerRow{"canary": {Version: "v8"}} |
| 143 | const want = "warning: v9 has not been on canary; promoting straight to stable" |
| 144 | for _, c := range []struct { |
| 145 | name string |
| 146 | pointers map[string]*systemPointerRow |
| 147 | channel, version string |
| 148 | warn string |
| 149 | }{ |
| 150 | {"the version soaked on canary", onCanary, "stable", "v9", ""}, |
| 151 | {"canary has never been promoted", never, "stable", "v9", want}, |
| 152 | {"canary is on a different version", older, "stable", "v9", want}, |
| 153 | // Promoting TO canary is the rung below; nothing to skip. |
| 154 | {"promoting to canary", never, "canary", "v9", ""}, |
| 155 | // No --version: the tag is "the latest public release", which only |
| 156 | // the server resolves, so there is nothing to compare and a warning |
| 157 | // would be a guess. |
| 158 | {"the latest public release", never, "stable", "", ""}, |
| 159 | } { |
| 160 | t.Run(c.name, func(t *testing.T) { |
| 161 | if got := systemLadderWarning(c.pointers, c.channel, c.version); got != c.warn { |
| 162 | t.Errorf("systemLadderWarning = %q, want %q", got, c.warn) |
| 163 | } |
| 164 | }) |
| 165 | } |
| 166 | // A nil map is what an unreadable status body leaves behind: it warns |
| 167 | // rather than panicking. |
| 168 | if got := systemLadderWarning(nil, "stable", "v9"); got != want { |
| 169 | t.Errorf("nil pointers = %q, want %q", got, want) |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | // Wiring and policy are two facts, and the output keeps them apart. An |
| 174 | // operator debugging "nothing is scanning" needs to know which half is |
| 175 | // missing; one "healthy" line would send half of them to the wrong place. |
| 176 | func TestPrintSystemAntivirusSeparatesWiringFromPolicy(t *testing.T) { |
| 177 | fresh := "ClamAV 1.4.1/27500/" + time.Now().Format(time.ANSIC) |
| 178 | |
| 179 | // Not wired at all. |
| 180 | var b bytes.Buffer |
| 181 | printSystemAntivirus(&b, apiSystemAntivirusReply{}) |
| 182 | if out := b.String(); !strings.Contains(out, "no engine is wired") || |
| 183 | !strings.Contains(out, "CARLOS_CLAMD_ADDR") { |
| 184 | t.Fatalf("unwired output does not name the wiring:\n%s", out) |
| 185 | } |
| 186 | |
| 187 | // Wired, switched off. Names the COMMAND, not the variable — this half is |
| 188 | // restartless and an operator sent to a unit file would be sent wrong. |
| 189 | var b2 bytes.Buffer |
| 190 | printSystemAntivirus(&b2, apiSystemAntivirusReply{ |
| 191 | Wired: true, EngineWired: "clamav", Signature: fresh, Enabled: false, |
| 192 | }) |
| 193 | out2 := b2.String() |
| 194 | if !strings.Contains(out2, "scanning is OFF") || |
| 195 | !strings.Contains(out2, "carlos system antivirus enable") { |
| 196 | t.Fatalf("disabled output does not name the command:\n%s", out2) |
| 197 | } |
| 198 | if strings.Contains(out2, "CARLOS_CLAMD_ADDR") { |
| 199 | t.Fatalf("a wired-but-off deployment was told to fix its wiring:\n%s", out2) |
| 200 | } |
| 201 | |
| 202 | // Wired and on. |
| 203 | var b3 bytes.Buffer |
| 204 | printSystemAntivirus(&b3, apiSystemAntivirusReply{ |
| 205 | Wired: true, EngineWired: "clamav", Signature: fresh, |
| 206 | Enabled: true, By: "op@example.com", At: "2026-09-02T08:00:00Z", |
| 207 | }) |
| 208 | out3 := b3.String() |
| 209 | if !strings.Contains(out3, "scanning is ON") || !strings.Contains(out3, "op@example.com") { |
| 210 | t.Fatalf("enabled output:\n%s", out3) |
| 211 | } |
| 212 | // And it must not imply anything is being scanned yet. |
| 213 | if !strings.Contains(out3, "scans nothing") { |
| 214 | t.Fatalf("enabled output implies coverage it does not have:\n%s", out3) |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // An engine that is wired but not answering is its own state, distinct from |
| 219 | // both "no engine" and "switched off". |
| 220 | func TestPrintSystemAntivirusReportsUnreachableEngine(t *testing.T) { |
| 221 | var b bytes.Buffer |
| 222 | printSystemAntivirus(&b, apiSystemAntivirusReply{ |
| 223 | Wired: true, EngineError: "dial tcp scanner:3310: connection refused", Enabled: true, |
| 224 | }) |
| 225 | out := b.String() |
| 226 | if !strings.Contains(out, "not answering") { |
| 227 | t.Fatalf("an unreachable engine was not reported:\n%s", out) |
| 228 | } |
| 229 | // The policy half still reports, because it is still true. |
| 230 | if !strings.Contains(out, "scanning is ON") { |
| 231 | t.Fatalf("an engine failure hid the recorded policy:\n%s", out) |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | // A record the console could not read is not "off". Reporting it as off would |
| 236 | // be a guess, on the one command an operator runs to find out what is true. |
| 237 | func TestPrintSystemAntivirusReportsUnreadableRecord(t *testing.T) { |
| 238 | var b bytes.Buffer |
| 239 | printSystemAntivirus(&b, apiSystemAntivirusReply{ |
| 240 | Wired: true, EngineWired: "clamav", RecordError: "schema 99 is not 1", |
| 241 | }) |
| 242 | out := b.String() |
| 243 | if !strings.Contains(out, "could not be read") { |
| 244 | t.Fatalf("an unreadable record was not reported:\n%s", out) |
| 245 | } |
| 246 | if strings.Contains(out, "scanning is OFF") { |
| 247 | t.Fatalf("an unreadable record was reported as off:\n%s", out) |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | // Stale definitions are called out here too, not only in store scan status: a |
| 252 | // deployment-wide view is where an operator would notice freshclam had died. |
| 253 | func TestPrintSystemAntivirusWarnsOnStaleSignatures(t *testing.T) { |
| 254 | old := "ClamAV 1.4.1/27000/" + time.Now().Add(-90*24*time.Hour).Format(time.ANSIC) |
| 255 | var b bytes.Buffer |
| 256 | printSystemAntivirus(&b, apiSystemAntivirusReply{ |
| 257 | Wired: true, EngineWired: "clamav", Signature: old, Enabled: true, |
| 258 | MaxSignatureAgeSeconds: int64((168 * time.Hour) / time.Second), |
| 259 | }) |
| 260 | if !strings.Contains(b.String(), "days old") { |
| 261 | t.Fatalf("stale definitions were not called out:\n%s", b.String()) |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | func TestPrintSystemLayoutStatus(t *testing.T) { |
| 266 | var b strings.Builder |
| 267 | printSystemLayoutStatus(&b, systemLayoutResponse{Mode: "none"}) |
| 268 | for _, want := range []string{"no layout intent recorded; boxes use the nested layout by default once their isolation is ready", "CARLOS_LAYOUT_DEFAULT=flat", "carlos system layout revert", "carlos status --layout"} { |
| 269 | if !strings.Contains(b.String(), want) { |
| 270 | t.Errorf("none lacks %q: %q", want, b.String()) |
| 271 | } |
| 272 | } |
| 273 | b.Reset() |
| 274 | printSystemLayoutStatus(&b, systemLayoutResponse{Mode: "nested", Box: "kass-1", RequestedAt: "2026-10-06T10:00:00Z", Force: true}) |
| 275 | for _, want := range []string{"intent: nested for box kass-1", "2026-10-06T10:00:00Z, force", "carlos status --layout"} { |
| 276 | if !strings.Contains(b.String(), want) { |
| 277 | t.Errorf("output lacks %q:\n%s", want, b.String()) |
| 278 | } |
| 279 | } |
| 280 | b.Reset() |
| 281 | printSystemLayoutStatus(&b, systemLayoutResponse{Mode: "flat", RequestedAt: "2026-10-06T10:00:00Z"}) |
| 282 | if !strings.Contains(b.String(), "intent: flat for every box (recorded 2026-10-06T10:00:00Z)") { |
| 283 | t.Errorf("fleet: %q", b.String()) |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | func TestSystemFilesSeedCommandEntryRefusesBeforeEffects(t *testing.T) { |
| 288 | bin := buildCarlos(t) |
| 289 | for _, flags := range []string{"", "1"} { |
| 290 | cmd := exec.Command(bin, "system", "files", "seed", "--host", "seed.example") |
| 291 | cmd.Dir = t.TempDir() |
| 292 | cmd.Env = carlosTestEnv(t, "CARLOS_HOST_CLAIM="+flags, "CARLOS_HOST_CLAIM_ORIGINS="+flags, "CARLOS_MOVES_PHASE1=", "CARLOS_DEPLOYMENT_BUCKET=", "CARLOS_DEPLOYMENT_DIR="+t.TempDir(), "AWS_EC2_METADATA_DISABLED=true") |
| 293 | out, err := cmd.CombinedOutput() |
| 294 | var exit *exec.ExitError |
| 295 | if !errors.As(err, &exit) || exit.ExitCode() != 1 || !strings.Contains(string(out), "Files seed failed:") { |
| 296 | t.Fatalf("dispatch: %v %s", err, out) |
| 297 | } |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | // Issue #316: status says when bootstrap/carlos is not stable's build, and is |
| 302 | // quiet when it is, when there is nothing to compare, and under --channel |
| 303 | // canary. |
| 304 | func TestPrintSystemStatusSaysWhenBootstrapLags(t *testing.T) { |
| 305 | stable := map[string]*systemPointerRow{"stable": {Version: "v0.28.0"}} |
| 306 | behind := "bootstrap/carlos is not stable's build (v0.28.0), so a new box would boot a different one. Copy bootstrap/router/v0.28.0/carlos-linux-arm64 over it." |
| 307 | for _, tc := range []struct { |
| 308 | name, channel, want string |
| 309 | row *systemBootstrapRow |
| 310 | }{ |
| 311 | {"behind", "", behind, &systemBootstrapRow{Version: "v0.28.0", Key: "bootstrap/router/v0.28.0/carlos-linux-arm64", State: "behind"}}, |
| 312 | {"behind, stable only", "stable", behind, &systemBootstrapRow{Version: "v0.28.0", Key: "bootstrap/router/v0.28.0/carlos-linux-arm64", State: "behind"}}, |
| 313 | {"unreadable", "", "bootstrap/carlos could not be read, so this can't say what a new box would boot: access denied", |
| 314 | &systemBootstrapRow{Version: "v0.28.0", State: "unreadable", Error: "access denied"}}, |
| 315 | {"current", "", "", &systemBootstrapRow{Version: "v0.28.0", State: "current"}}, |
| 316 | {"nothing to compare", "", "", nil}, |
| 317 | {"canary only", "canary", "", &systemBootstrapRow{Version: "v0.28.0", State: "behind"}}, |
| 318 | } { |
| 319 | t.Run(tc.name, func(t *testing.T) { |
| 320 | var b strings.Builder |
| 321 | printSystemStatus(&b, systemStatusResponse{Pointers: stable, Bootstrap: tc.row}, tc.channel) |
| 322 | out := b.String() |
| 323 | if tc.want == "" { |
| 324 | if strings.Contains(out, "bootstrap/carlos") { |
| 325 | t.Fatalf("output mentions bootstrap/carlos:\n%s", out) |
| 326 | } |
| 327 | return |
| 328 | } |
| 329 | if !strings.Contains(out, tc.want) { |
| 330 | t.Fatalf("output lacks %q:\n%s", tc.want, out) |
| 331 | } |
| 332 | }) |
| 333 | } |
| 334 | } |
| 335 | |