// Tests for the `carlos sidecars` core — flag parsing/validation and // printing live in sidecars_cli.go's run* functions, which os.Exit() on a // bad invocation via log.Fatal and so aren't themselves a good unit-test // target (instances_cli_test.go's own doc comment: no argv test harness // exists in this package for os.Exit paths). What's tested directly // instead is sidecarsEnable, the core PutSidecarManifest wrapper; // parseArgv, the -args flag's whitespace-split helper; parseDurationSeconds, // the -idle/-max-run duration-to-seconds conversion; the two small render // helpers `ls`'s VERSION and PLACEMENT columns go through; and // TestSidecarsListTruncatedNotice, which drives the real runSidecarsList // against a fake console (apimode_test.go's newTestSession/loginTo/ // apiTestAccount verb-level fixtures) to pin the truncation honesty notice // added in review round 1. package main import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) func TestSidecarsEnableWritesManifest(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) no := false m := provision.SidecarManifest{ Schema: 1, App: "titogo", Pool: "tito", Channel: "stable", Placement: provision.PlacementNoInbound, Regions: []string{"ie"}, Args: []string{"sidecar", "run"}, Hibernate: &no, IdleSeconds: 300, MaxRunSeconds: 3600, } if err := sidecarsEnable(ctx, s, "bad", "titogo", m); err != nil { t.Fatalf("sidecarsEnable: %v", err) } got, err := provision.GetSidecarManifest(ctx, s, "bad", "titogo") if err != nil { t.Fatalf("GetSidecarManifest: %v", err) } if got.App != m.App || got.Pool != m.Pool || got.Channel != m.Channel || got.Placement != m.Placement || len(got.Regions) != 1 || got.Regions[0] != "ie" || len(got.Args) != 2 || got.Args[0] != "sidecar" || got.Args[1] != "run" || got.Hibernate == nil || *got.Hibernate != false || got.IdleSeconds != 300 || got.MaxRunSeconds != 3600 { t.Errorf("GetSidecarManifest after sidecarsEnable mismatch: got %+v, want %+v", got, m) } // Re-running with a changed pool must update the manifest in place — a // CAS upsert, not a create-only write, exactly as instancesEnable's own // re-run test pins (instances_cli_test.go). m.Pool = "other" if err := sidecarsEnable(ctx, s, "bad", "titogo", m); err != nil { t.Fatalf("sidecarsEnable (update): %v", err) } got, err = provision.GetSidecarManifest(ctx, s, "bad", "titogo") if err != nil { t.Fatalf("GetSidecarManifest after update: %v", err) } if got.Pool != "other" { t.Errorf("Pool after update = %q, want %q", got.Pool, "other") } } func TestParseArgv(t *testing.T) { cases := []struct { in string want []string }{ {"", nil}, {"sidecar", []string{"sidecar"}}, {"sidecar run", []string{"sidecar", "run"}}, {" sidecar run ", []string{"sidecar", "run"}}, } for _, c := range cases { got := parseArgv(c.in) if len(got) != len(c.want) { t.Errorf("parseArgv(%q) = %v, want %v", c.in, got, c.want) continue } for i := range got { if got[i] != c.want[i] { t.Errorf("parseArgv(%q) = %v, want %v", c.in, got, c.want) break } } } } // TestParseDurationSeconds pins the wire's own contract (design §4.1): // idle/max-run cross as whole SECONDS, never a duration string — "5m" must // become exactly 300, "1h" exactly 3600. func TestParseDurationSeconds(t *testing.T) { cases := []struct { in string want int wantErr bool }{ {"5m", 300, false}, {"1h", 3600, false}, {"0s", 0, false}, {"30s", 30, false}, {"-5m", 0, true}, {"banana", 0, true}, } for _, c := range cases { got, err := parseDurationSeconds(c.in) if c.wantErr { if err == nil { t.Errorf("parseDurationSeconds(%q) = %d, , want an error", c.in, got) } continue } if err != nil { t.Errorf("parseDurationSeconds(%q) unexpected error: %v", c.in, err) continue } if got != c.want { t.Errorf("parseDurationSeconds(%q) = %d, want %d", c.in, got, c.want) } } } // TestSidecarVersionCell pins `ls`'s honesty rule directly (design §9.2, // binding on this task): a sidecar that has never reported a version must // render "unknown", never the record's own channel or anything else that // would read as a current, confirmed version. func TestSidecarVersionCell(t *testing.T) { cases := []struct{ in, want string }{ {"", "unknown"}, {"abc1234", "abc1234"}, } for _, c := range cases { if got := sidecarVersionCell(c.in); got != c.want { t.Errorf("sidecarVersionCell(%q) = %q, want %q", c.in, got, c.want) } } } func TestSidecarPlacementCell(t *testing.T) { cases := []struct{ in, want string }{ {"", provision.PlacementAny}, {provision.PlacementNoInbound, provision.PlacementNoInbound}, {provision.PlacementAny, provision.PlacementAny}, } for _, c := range cases { if got := sidecarPlacementCell(c.in); got != c.want { t.Errorf("sidecarPlacementCell(%q) = %q, want %q", c.in, got, c.want) } } } // TestSidecarsListTruncatedNotice pins the truncation honesty rule added in // Task 14 review round 1: the console caps one `ls` answer at // sidecarListMax (500, internal/console/apisidecars.go) and reports it via // the response's `truncated` flag; a caller that prints the records and // drops that flag on the floor renders a list that quietly stops at a // prefix and says nothing — the same overclaim VERSION's "unknown" exists // to prevent, and not hypothetical for the motivating customer (Eleven: a // bot per enrolled member, so exceeding 500 is growth, not misuse). // // This drives the real runSidecarsList — not just the client method — // against a fake console built the way apimode_test.go's other verb-level // tests are (newTestSession/loginTo/apiTestAccount), asserting the notice // appears for a truncated answer and stays silent for a whole one. Those // two cases are the mutation check the brief asked for: a change that // ignores the flag entirely (always prints it, or never does) fails // exactly one of the two subtests. func TestSidecarsListTruncatedNotice(t *testing.T) { for _, tc := range []struct { name string truncated bool }{ {"truncated", true}, {"not truncated", false}, } { t.Run(tc.name, func(t *testing.T) { useTempCredentials(t) clearDeploymentEnv(t) checkNoRegistry := requireNoRegistryTouch(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]any{ "sidecars": []map[string]any{ {"name": "bot-acme", "host": "bot-acme.titogo.bab.sidecar.internal", "channel": "stable"}, }, "truncated": tc.truncated, }) })) defer ts.Close() loginTo(t, ts.URL, "vicky@keymail.dev") buf := captureStdout(t) runSidecarsList([]string{"-app", "titogo", "-account", apiTestAccount}) out := buf.String() const notice = "more exist" if tc.truncated && !strings.Contains(out, notice) { t.Fatalf("ls stdout = %q, want a truncation notice containing %q", out, notice) } if !tc.truncated && strings.Contains(out, notice) { t.Fatalf("ls stdout = %q, must NOT claim more exist for a whole (untruncated) answer", out) } checkNoRegistry() }) } }