package main import ( "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/controlname" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) // A canary name has to satisfy TWO alphabets at once: release.ValidChannel's // segment ([a-zA-Z0-9._-], 64) and a DNS label, because the same string is // both the channel's second segment and the leftmost label of the canary // host. The DNS label is the stricter of the two, so it is the one enforced // — lowercase, no dots (a dot would silently add a level to the host). func TestCanarySlugTurnsBranchesIntoDNSLabels(t *testing.T) { for _, tc := range []struct{ branch, want string }{ {"new-queue", "new-queue"}, {"feature/new-queue", "feature-new-queue"}, {"PAUL/JAM-42", "paul-jam-42"}, {"paul@example.com/fix", "paul-example-com-fix"}, {"a//b", "a-b"}, {"--leading-and-trailing--", "leading-and-trailing"}, {"v1.2.3", "v1-2-3"}, {"", ""}, {"///", ""}, } { if got := canarySlug(tc.branch); got != tc.want { t.Errorf("canarySlug(%q) = %q, want %q", tc.branch, got, tc.want) } } } // 63 is the DNS label cap, and it is tighter than the channel segment's 64, // so it is the one that binds. A slug that overran it would mint a host the // edge could never serve. func TestCanarySlugTruncatesToTheDNSLabelCap(t *testing.T) { got := canarySlug(strings.Repeat("x", 100)) if len(got) != 63 { t.Fatalf("len = %d, want 63", len(got)) } if !validCanaryName(got) { t.Errorf("a truncated slug %q is not a valid canary name", got) } } // Whatever the slugger produces must satisfy the platform's own channel // check too — the two alphabets are only useful if the tighter one is // genuinely inside the looser one. func TestCanarySlugIsAlwaysAValidChannel(t *testing.T) { for _, branch := range []string{"feature/new-queue", "PAUL/JAM-42", "v1.2.3", strings.Repeat("ab/", 40)} { name := canarySlug(branch) if !release.ValidChannel(canaryChannel(name)) { t.Errorf("canarySlug(%q) = %q, which makes an invalid channel", branch, name) } } } // Truncation must not leave a trailing separator: "canary/foo-" is a valid // segment but an invalid DNS label, and the host is built from the same // string. func TestCanarySlugTruncationDoesNotStrandASeparator(t *testing.T) { got := canarySlug(strings.Repeat("ab/", 40)) if strings.HasSuffix(got, "-") { t.Errorf("slug %q ends in a separator", got) } } // A detached HEAD reports the literal "HEAD". Minting canary/head off that // would be a silent wrong answer, so it is refused and names the way out. func TestCanaryNameRefusesDetachedHead(t *testing.T) { _, err := canaryNameFrom("", "HEAD") if err == nil { t.Fatal("a detached HEAD was accepted; want a refusal") } if !strings.Contains(err.Error(), "--name") { t.Errorf("refusal %q does not name --name", err) } } func TestCanaryNameFlagBeatsTheBranch(t *testing.T) { got, err := canaryNameFrom("chosen", "some-branch") if err != nil { t.Fatalf("canaryNameFrom: %v", err) } if got != "chosen" { t.Errorf("name = %q, want chosen — --name must win over the branch", got) } } // An explicit --name is taken as typed, never slugified: silently rewriting // what someone deliberately asked for is how you end up promoting to a // channel you never named. func TestCanaryNameFlagIsRefusedNotRepaired(t *testing.T) { _, err := canaryNameFrom("feature/new-queue", "") if err == nil { t.Fatal("--name feature/new-queue was accepted; want a refusal") } if !strings.Contains(err.Error(), "feature/new-queue") { t.Errorf("refusal %q does not quote the name given", err) } } // No --name and no usable branch: refuse, rather than invent one. func TestCanaryNameWithNothingToGoOnIsRefused(t *testing.T) { _, err := canaryNameFrom("", "") if err == nil { t.Fatal("an empty name and empty branch were accepted; want a refusal") } if !strings.Contains(err.Error(), "--name") { t.Errorf("refusal %q does not name --name", err) } } // The host is -..: one label before // the domain, because the instance create door takes exactly one (discussion // 207). The manifest's domain is already ".", the same // value deploy's mintPlatformHost builds "." from. func TestCanaryHostIsOneLabelBeforeTheDomain(t *testing.T) { const domain = "bab.oncarlos.com" got := canaryHost("new-queue", "jam", domain) want := "new-queue-jam.bab.oncarlos.com" if got != want { t.Errorf("canaryHost = %q, want %q", got, want) } // The create door's own predicate, not a restatement of it. if !provision.ValidHost(got, domain) { t.Errorf("provision.ValidHost(%q, %q) = false: the create door would refuse this canary", got, domain) } // The widest label that fits: name + "-" + app = 63. name := strings.Repeat("n", 63-1-len("jam")) if h := canaryHost(name, "jam", domain); !canaryLabelFits(name, "jam") || !provision.ValidHost(h, domain) { t.Errorf("a 63-byte label was refused: fits=%v host=%q", canaryLabelFits(name, "jam"), h) } } // name + "-" + app is one hostname label: 63 bytes, the dash counted. A name // validCanaryName accepts on its own can still be too long beside the app. func TestCanaryLabelFitsOneLabel(t *testing.T) { for _, tc := range []struct { name, app string want bool }{ {"new-queue", "jam", true}, {strings.Repeat("n", 59), "jam", true}, // 59 + 1 + 3 = 63 {strings.Repeat("n", 60), "jam", false}, // 64 {strings.Repeat("n", 63), "a", false}, // a valid name, no room for the app {"a", strings.Repeat("p", 61), true}, // 1 + 1 + 61 = 63 {"a", strings.Repeat("p", 62), false}, } { if got := canaryLabelFits(tc.name, tc.app); got != tc.want { t.Errorf("canaryLabelFits(%d bytes, %d bytes) = %v, want %v", len(tc.name), len(tc.app), got, tc.want) } } msg := canaryLabelTooLong(strings.Repeat("n", 60), "jam") for _, want := range []string{"64 characters", "63", "--name"} { if !strings.Contains(msg, want) { t.Errorf("refusal %q does not say %q", msg, want) } } } func TestCanaryChannelIsNamespaced(t *testing.T) { if got := canaryChannel("new-queue"); got != "canary/new-queue" { t.Errorf("canaryChannel = %q, want canary/new-queue", got) } } // Step 4 of the deploy: create, re-channel, or write nothing at all. The // no-write case matters — a redeploy of the same canary must not PATCH a // record that is already correct. func TestCanaryRecordPlan(t *testing.T) { const host = "new-queue-jam.bab.oncarlos.com" const ch = "canary/new-queue" other := apiInstanceRecord{Host: "jam.bab.oncarlos.com", Channel: "stable"} for _, tc := range []struct { name string recs []apiInstanceRecord want canaryPlan }{ {"no records at all", nil, canaryPlanCreate}, {"only the app's stable instance", []apiInstanceRecord{other}, canaryPlanCreate}, {"canary host already on the canary channel", []apiInstanceRecord{other, {Host: host, Channel: ch}}, canaryPlanReady}, {"canary host parked on another channel", []apiInstanceRecord{other, {Host: host, Channel: "canary/old"}}, canaryPlanRechannel}, // One label now: the canary's host can be an ordinary instance of // the same app. Rechannelling is for canaries; an instance that is // not on a canary/* channel is refused, whatever it follows. {"an instance of the app on the canary's host", []apiInstanceRecord{{Host: host, Channel: "stable"}}, canaryPlanTaken}, {"a bare canary channel is not a canary/* channel", []apiInstanceRecord{{Host: host, Channel: "canary"}}, canaryPlanTaken}, } { t.Run(tc.name, func(t *testing.T) { if got := canaryRecordPlan(tc.recs, host, ch); got != tc.want { t.Errorf("plan = %v, want %v", got, tc.want) } }) } } // ls reports what is left of the seven-day lease, and says plainly when one // has lapsed — that is the whole reason the TTL is visible at all. func TestCanaryLeaseLabel(t *testing.T) { now := time.Date(2026, 9, 4, 12, 0, 0, 0, time.UTC) rfc := func(d time.Duration) string { return now.Add(-d).Format(time.RFC3339) } for _, tc := range []struct{ name, at, want string }{ {"promoted moments ago", rfc(time.Minute), "7d left"}, {"one day in", rfc(24 * time.Hour), "6d left"}, {"final hours", rfc(7*24*time.Hour - 3*time.Hour), "3h left"}, {"just lapsed", rfc(7*24*time.Hour + 30*time.Minute), "expired <1h"}, {"long lapsed", rfc(9 * 24 * time.Hour), "expired 2d"}, {"never promoted", "", "—"}, {"unparseable", "last tuesday", "—"}, } { t.Run(tc.name, func(t *testing.T) { if got := canaryLeaseLabel(tc.at, now); got != tc.want { t.Errorf("canaryLeaseLabel(%q) = %q, want %q", tc.at, got, tc.want) } }) } } // rm must delete the record for the canary it was asked about and no other — // an app can have several canaries live at once. func TestCanaryRecordForNamePicksExactlyOne(t *testing.T) { recs := []apiInstanceRecord{ {Host: "jam.bab.oncarlos.com", Channel: "stable"}, {Host: "old-jam.bab.oncarlos.com", Channel: "canary/old"}, {Host: "new-queue-jam.bab.oncarlos.com", Channel: "canary/new-queue"}, } got, ok := canaryRecordForChannel(recs, "canary/new-queue") if !ok { t.Fatal("no record found for canary/new-queue") } if got.Host != "new-queue-jam.bab.oncarlos.com" { t.Errorf("host = %q, want the new-queue record", got.Host) } if _, ok := canaryRecordForChannel(recs, "canary/nope"); ok { t.Error("a canary that does not exist was reported found") } } // ls lists canaries, not every instance the app has. func TestCanaryRecordsFiltersToCanaryChannels(t *testing.T) { recs := []apiInstanceRecord{ {Host: "jam.bab.oncarlos.com", Channel: "stable"}, {Host: "old-jam.bab.oncarlos.com", Channel: "canary/old"}, {Host: "eu-jam.bab.oncarlos.com", Channel: "edge"}, {Host: "new-queue-jam.bab.oncarlos.com", Channel: "canary/new-queue"}, // A bare "canary" is NOT a canary/* channel — the same boundary // internal/host/agent.go's isCanaryRoute draws. {Host: "bare-jam.bab.oncarlos.com", Channel: "canary"}, } got := canaryRecords(recs) if len(got) != 2 { t.Fatalf("got %d canary records, want 2: %+v", len(got), got) } if got[0].Channel != "canary/new-queue" && got[0].Channel != "canary/old" { t.Errorf("unexpected first row %+v", got[0]) } } // The name is what a member types into rm, so ls has to be able to derive it // back out of the channel it lists. func TestCanaryNameOfChannel(t *testing.T) { if got := canaryNameOf("canary/new-queue"); got != "new-queue" { t.Errorf("canaryNameOf = %q, want new-queue", got) } if got := canaryNameOf("stable"); got != "" { t.Errorf("canaryNameOf(stable) = %q, want empty", got) } } // The whole point of the change: main.go's canary case dispatches instead of // exiting "not implemented". internal/docsite's TopLevelCommands scan already // insists the verb has a doc heading, but it cannot tell a real dispatch from // a stub — and a stub with a heading is exactly the state that made a session // conclude the platform could not canary at all. func TestCanaryIsDispatchedNotStubbed(t *testing.T) { src, err := os.ReadFile("main.go") if err != nil { t.Fatalf("read main.go: %v", err) } _, after, ok := strings.Cut(string(src), "case \"canary\":") if !ok { t.Fatal("main.go no longer dispatches on \"canary\"") } body, _, _ := strings.Cut(after, "case \"") if !strings.Contains(body, "runCanary(args)") { t.Errorf("the canary case does not call runCanary: %q", body) } if strings.Contains(body, "not implemented") { t.Error("the canary case still exits \"not implemented\"") } } // canary is a member verb now, not a reserved word, so it takes the // once-a-day update hint like deploy and ship do. func TestCanaryTakesTheUpdateHint(t *testing.T) { if noHintVerbs["canary"] { t.Error("canary is still in noHintVerbs — it was listed there while it was a stub") } } // The refusal names the channel the record on the host follows, from the // same source the plan was decided from: the direct read under --region, the // list otherwise. func TestCanaryHostChannelReadsWhatThePlanDecidedFrom(t *testing.T) { const host = "probe-jam.bab.oncarlos.com" recs := []apiInstanceRecord{{Host: host, Channel: "stable"}, {Host: "other-jam.bab.oncarlos.com", Channel: "edge"}} if got := canaryHostChannel(canaryPreflight{}, recs, host); got != "stable" { t.Errorf("from the list = %q, want stable", got) } pre := canaryPreflight{Host: host, Rec: apiInstanceRecord{Host: host, Channel: "beta"}, Found: true} if got := canaryHostChannel(pre, recs, host); got != "beta" { t.Errorf("from the direct read = %q, want beta", got) } if got := canaryHostChannel(canaryPreflight{Host: host}, recs, host); got != "" { t.Errorf("direct read found nothing, got %q", got) } if msg := canaryHostTaken(host, "stable"); !strings.Contains(msg, host) || !strings.Contains(msg, "stable") { t.Errorf("refusal %q names neither the host nor its channel", msg) } } // Every app name the platform accepts has to map to a canary host that is // either valid or refused up front. App names allow a trailing dash // (controlname.ValidApp: ^[a-z][a-z0-9-]{0,31}$) and DNS labels do not, so // app "jam-" would make "probe-jam-" for EVERY canary name, and choosing // another --name cannot help. Exhaustive over a small alphabet that holds // every character class and the dash positions: when the refusal is empty // the host must pass the create door's own predicate. func TestCanaryHostIsValidOrRefusedForEveryAppName(t *testing.T) { const domain = "bab.oncarlos.com" var words func(n int) []string words = func(n int) []string { if n == 0 { return []string{""} } var out []string for _, w := range words(n - 1) { for _, c := range "a1-" { out = append(out, w+string(c)) } } return out } var names, apps []string for n := 1; n <= 4; n++ { for _, w := range words(n) { if validCanaryName(w) { names = append(names, w) } if controlname.ValidApp(w) { apps = append(apps, w) } } } // A real-length app ending in a dash, and the one at the 32-byte limit. apps = append(apps, "jam-", strings.Repeat("a", 31)+"-", strings.Repeat("a", 32)) refused, accepted := 0, 0 for _, app := range apps { for _, name := range names { why := canaryHostRefusal(name, app) ok := provision.ValidHost(canaryHost(name, app, domain), domain) switch { case why == "" && !ok: t.Fatalf("app %q, canary %q: host %q is not valid and nothing refused it", app, name, canaryHost(name, app, domain)) case why != "" && ok && canaryLabelFits(name, app): t.Fatalf("app %q, canary %q: refused (%s) though %q is a valid host", app, name, why, canaryHost(name, app, domain)) } if why == "" { accepted++ } else { refused++ } } } if accepted == 0 || refused == 0 { t.Fatalf("the sweep saw %d accepted and %d refused; it proves nothing", accepted, refused) } }