package console // The app page's ladder section (pipelines spec §3, §5, §7). Four properties, // in the order they would hurt if lost: the four rungs say what is on them, // the promote control is owner-only, `stable` cannot be reached without a // passkey, and a skip up the ladder is refused however it is posted. import ( "bytes" "context" "crypto/ed25519" "encoding/base64" "encoding/json" "net/http" "net/url" "strings" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // shipVersion adds one more shipped release to the fixture app — fixture // state, built through the real primitive so the console reads it exactly as // it reads a real one. func shipVersion(t *testing.T, fx *consoleFixture, app, version string) { t.Helper() m := release.Manifest{ Schema: 1, App: app, Version: version, Kind: "binary", BuiltAt: "2026-07-03T10:00:00Z", Artifacts: map[string]release.Artifact{"linux-amd64": {File: app}}, } if err := release.Ship(context.Background(), fx.rel, release.OpsAccount, m, map[string][]byte{"linux-amd64": []byte("binary " + version)}); err != nil { t.Fatal(err) } } // seedChannel points a channel at a version through the UNCHECKED primitive. // That is deliberate: these are arrangements of the world the console then has // to read and gate, not promotions under test. func seedChannel(t *testing.T, fx *consoleFixture, app, channel, version string, rec ...release.Promotion) { t.Helper() if err := release.Promote(context.Background(), fx.rel, fx.priv, release.OpsAccount, app, channel, version, rec...); err != nil { t.Fatal(err) } } // rungBlock cuts one rung's markup out of the rendered page, so "the beta rung // shows v4" is an assertion about the beta rung and not about the page // containing "v4" somewhere. func rungBlock(t *testing.T, body, rung string) string { t.Helper() _, after, ok := strings.Cut(body, `id="rung-`+rung+`"`) if !ok { t.Fatalf("no %s rung in the ladder:\n%s", rung, body) } if before, _, ok := strings.Cut(after, `id="rung-`); ok { return before } before, _, _ := strings.Cut(after, "") return before } // routeLine narrows further, to the one line a host is listed on. func routeLine(t *testing.T, block, host string) string { t.Helper() for _, li := range strings.Split(block, ""+host+"") { return li } } t.Fatalf("host %s is not listed in:\n%s", host, block) return "" } // The four rungs, in order, each saying what it holds, when it got there and // who put it there. The audit trail is new, so most live pointers have none: // the rung that has one names the actor, and the rung that does not still // renders — saying so — rather than crashing or inventing a name. func TestLadderRendersFourRungs(t *testing.T) { fx := newConsoleFixture(t) shipVersion(t, fx, "kass", "v3") shipVersion(t, fx, "kass", "v4") seedChannel(t, fx, "kass", "edge", "v3", release.Promotion{Version: "v3", To: "edge", Actor: "ci@keymail.dev"}) seedChannel(t, fx, "kass", "beta", "v4", release.Promotion{Version: "v4", From: "edge", To: "beta", Actor: "kim@keymail.dev"}) c := signInPaul(t, fx) status, _, body := get(t, c, fx.u("/apps/kass/releases")) if status != http.StatusOK { t.Fatalf("app page = %d", status) } // Fixture: stable v1 and canary/x v2, both written before the audit trail // existed; edge and beta seeded above. for rung, version := range map[string]string{ "canary": "v2", "edge": "v3", "beta": "v4", "stable": "v1", } { block := rungBlock(t, body, rung) if !strings.Contains(block, ">"+version+"<") { t.Errorf("the %s rung does not show %s:\n%s", rung, version, block) } } if b := rungBlock(t, body, "beta"); !strings.Contains(b, "kim@keymail.dev") { t.Errorf("the beta rung does not say who promoted it:\n%s", b) } if b := rungBlock(t, body, "edge"); !strings.Contains(b, "ci@keymail.dev") { t.Errorf("the edge rung does not say who promoted it:\n%s", b) } // The pre-audit pointer: rendered, and honest about what it cannot say — // the template now omits the "by ..." clause entirely rather than // attributing the promotion to anybody, so the dishonest alternative // (inventing an actor) is what must not appear. if b := rungBlock(t, body, "stable"); strings.Contains(b, "ago by") { t.Errorf("a pointer with no audit trail invented an actor:\n%s", b) } // Routes sit under the rung they subscribe to. if b := rungBlock(t, body, "stable"); !strings.Contains(b, "kass.carlosapp.dev") { t.Errorf("the stable rung does not list the hosts following it:\n%s", b) } } // The promote control is the owner's. A member still sees the whole ladder — // visibility is not the thing being restricted — and is offered no button. func TestLadderPromoteControlIsOwnerOnly(t *testing.T) { fx := newConsoleFixture(t) shipVersion(t, fx, "kass", "v3") seedChannel(t, fx, "kass", "edge", "v3") c := signInPaul(t, fx) _, _, body := get(t, c, fx.u("/apps/kass/releases")) if !strings.Contains(body, "Promote v3 to beta") { t.Fatalf("owner is offered no edge -> beta control:\n%s", body) } // The control posts to the app's one promote endpoint — the same door, // with the same ladder and step-up gates behind it. if !strings.Contains(body, `action="/a/bab/apps/kass/promote"`) { t.Fatal("the ladder control does not post to the promote endpoint") } if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil { t.Fatal(err) } fx.setUser("sam@keymail.dev") cm := newBrowser(t) signUp(t, fx, cm, "code-sam-ladder") status, _, mbody := get(t, cm, fx.u("/apps/kass/releases")) if status != http.StatusOK { t.Fatalf("member app page = %d, want 200", status) } if !strings.Contains(mbody, `id="rung-stable"`) { t.Fatal("a member cannot see the ladder at all") } if strings.Contains(mbody, "Promote v3 to beta") { t.Error("a member is offered the ladder's promote control") } } // Stable is the rung that puts a build in front of every user, so a live // session is not enough on its own. Without the passkey the pointer does not // move; with it, the held promotion resumes and is recorded. func TestLadderPromoteToStableNeedsStepUp(t *testing.T) { fx := newConsoleFixture(t) shipVersion(t, fx, "kass", "v3") seedChannel(t, fx, "kass", "beta", "v3") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) status, loc, _ := postForm(t, c, fx.u("/apps/kass/promote"), url.Values{"csrf": {csrf}, "channel": {"stable"}, "version": {"v3"}, "confirmed": {"1"}}) if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) { t.Fatalf("un-stepped-up stable promotion = %d -> %q, want a redirect into Keymail", status, loc) } if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v1" { t.Fatalf("stable moved to %s with no passkey confirmation", p.Version) } if status, _, body := completeStepUp(t, fx, c, loc); status != http.StatusSeeOther { t.Fatalf("resumed promotion = %d:\n%s", status, body) } p := fx.channelVersion(t, "kass", "stable") if p.Version != "v3" { t.Fatalf("stable = %s after a confirmed promotion, want v3", p.Version) } if len(p.Audit) == 0 { t.Fatal("the promotion recorded nothing on the pointer it moved") } if a := p.Audit[0]; a.Version != "v3" || a.From != "beta" || a.To != "stable" || a.Actor != "paul@keymail.dev" || a.Hotfix { t.Fatalf("audit[0] = %+v, want v3 beta->stable by paul, no hotfix", a) } } // A skip is refused whoever asks and however confirmed: edge -> stable never // happens through the console, because "it was signed off on beta" is only // worth anything if everything on stable came through beta. func TestLadderRefusesASkip(t *testing.T) { fx := newConsoleFixture(t) shipVersion(t, fx, "kass", "v3") seedChannel(t, fx, "kass", "edge", "v3") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) // postGated walks the passkey round trip, so this is the refusal of a // FULLY confirmed owner — the ladder, not the gate, is what stops it. postGated(t, fx, c, fx.u("/apps/kass/promote"), url.Values{"csrf": {csrf}, "channel": {"stable"}, "version": {"v3"}, "confirmed": {"1"}}) if p := fx.channelVersion(t, "kass", "stable"); p.Version != "v1" { t.Fatalf("a skip from edge moved stable to %s", p.Version) } if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "stable is entered from beta only") { t.Fatalf("no ladder refusal flash:\n%s", body) } } // A canary is badged; a canary that binds the SAME ENVIRONMENT as the app's // stable route is badged unmissably, because that is an unreviewed build // wired to production glue (spec §4a, §5) — whichever host it happens to be // on. A canary bound to a genuinely different environment is the ordinary, // unremarkable case. Both badges are chrome, so both are icons, not glyphs. func TestProdWiredCanaryIsBadgedDistinctly(t *testing.T) { fx := newConsoleFixture(t) // The fixture's own kass.carlosapp.dev is already on stable, unbound // (Environment ""). A canary that is ALSO unbound binds the same // environment as stable ("default" both times) and is prod-wired; one // bound to a real, different environment is not — regardless of which // host either is on. fx.addRoute(t, registry.Route{Host: "rc.kass.carlosapp.dev", Kind: "instance", Status: "ready", Version: "v2", App: "kass", Channel: "canary/x", Addr: "127.0.0.1:9002"}) fx.addRoute(t, registry.Route{Host: "side.kass.carlosapp.dev", Kind: "instance", Status: "ready", Version: "v2", App: "kass", Channel: "canary/x", Environment: "sandbox", Addr: "127.0.0.1:9003"}) c := signInPaul(t, fx) _, _, body := get(t, c, fx.u("/apps/kass/releases")) block := rungBlock(t, body, "canary") sameEnv := routeLine(t, block, "rc.kass.carlosapp.dev") if !strings.Contains(sameEnv, "chip-prod") { t.Errorf("a canary binding the same (default) environment as stable is not flagged:\n%s", sameEnv) } diffEnv := routeLine(t, block, "side.kass.carlosapp.dev") if !strings.Contains(diffEnv, "chip-canary") { t.Errorf("a canary route carries no canary badge:\n%s", diffEnv) } if strings.Contains(diffEnv, "chip-prod") { t.Errorf("a canary bound to a DIFFERENT environment than stable is flagged as production-wired:\n%s", diffEnv) } if !strings.Contains(sameEnv, "rival.kass.example.com") && strings.Contains(li, "granted:") { t.Fatalf("another account's route rendered a granted: line:\n%s", li) } } } // Visibility, not permission (spec §5): a member sees the same environment // and prod-wired chrome an owner does. Only the promote control is // owner-gated — see TestLadderPromoteControlIsOwnerOnly. func TestLadderEnvironmentAndProdWiredVisibleToNonOwner(t *testing.T) { fx := newConsoleFixture(t) fx.addRoute(t, registry.Route{Host: "rc.kass.carlosapp.dev", Kind: "instance", Status: "ready", Version: "v2", App: "kass", Channel: "canary/x", Addr: "127.0.0.1:9002"}) if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil { t.Fatal(err) } fx.setUser("sam@keymail.dev") cm := newBrowser(t) signUp(t, fx, cm, "code-sam-envs") status, _, body := get(t, cm, fx.u("/apps/kass/releases")) if status != http.StatusOK { t.Fatalf("member app page = %d, want 200", status) } canaryBlock := rungBlock(t, body, "canary") line := routeLine(t, canaryBlock, "rc.kass.carlosapp.dev") if !strings.Contains(line, "chip-prod") { t.Errorf("a member does not see the prod-wired badge:\n%s", line) } stableBlock := rungBlock(t, body, "stable") if !strings.Contains(stableBlock, "chip-env") { t.Errorf("a member does not see route environment chrome:\n%s", stableBlock) } } // ---- Canary TTL (spec §5 "Canaries expire") -------------------------------- // A canary promoted moments ago — exactly what every real promotion through // this console produces, Promote always stamping "now" — must carry no // expired badge. This is the case AGENTS.md's own console route is in on // every deploy (canary/console, re-promoted each time): a false positive // here would badge the console's own production traffic as neglected. func TestLadderFreshCanaryCarriesNoExpiredBadge(t *testing.T) { fx := newConsoleFixture(t) fx.addRoute(t, registry.Route{Host: "rc.kass.carlosapp.dev", Kind: "instance", Status: "ready", Version: "v2", App: "kass", Channel: "canary/x", Addr: "127.0.0.1:9002"}) seedChannel(t, fx, "kass", "canary/x", "v2") // stamps PromotedAt = now c := signInPaul(t, fx) _, _, body := get(t, c, fx.u("/apps/kass/releases")) block := rungBlock(t, body, "canary") line := routeLine(t, block, "rc.kass.carlosapp.dev") if strings.Contains(line, "chip-expired") { t.Errorf("a freshly-promoted canary is badged expired:\n%s", line) } // This route binds the same (default) environment as the fixture's // stable route, so it is ALSO prod-wired (TestProdWiredCanaryIsBadgedDistinctly // covers that badge on its own) — chip-prod or chip-canary, either is the // "this is a canary" badge the expired flag would sit beside; the point // of this test is only that neither carries chip-expired. if !strings.Contains(line, "chip-prod") && !strings.Contains(line, "chip-canary") { t.Errorf("the fresh canary lost its canary-family badge entirely:\n%s", line) } } // assembleLadder is exercised directly here (no store, no signing) because // there is no way to get a genuinely aged, verifiably-signed pointer through // the public Promote path — it always stamps the real clock, deliberately // (release.TestSignStampsPromotedAt pins that). Constructing the ChannelView // by hand is the same input assembleChannels would have produced from a // pointer that really was promoted eight days ago. func TestLadderExpiredCanaryIsBadgedWithAge(t *testing.T) { now := time.Now() old := now.Add(-(release.CanaryTTL + 8*24*time.Hour)) // 8 days past the TTL channels := []ChannelView{{Name: "canary/x", Version: "v9", PromotedAt: old}} // A stable route on a DIFFERENT, non-apex, non-matching-environment host // keeps this canary out of BOTH prod-wired signals (prodWiredCanaryHosts // is exercised on its own elsewhere) — this test is about the expired // badge specifically, so it stays clear of the prod-wired one. routes := []RouteView{ {Host: "a.example.com", Channel: "stable", Environment: "prod-env"}, {Host: "rc.example.com", Channel: "canary/x"}, } rungs := assembleLadder(channels, routes, nil, now) var canary *LadderRung for i := range rungs { if rungs[i].Rung == release.RungCanary { canary = &rungs[i] } } if canary == nil || len(canary.Channels) != 1 || len(canary.Channels[0].Routes) != 1 { t.Fatalf("canary rung = %+v, want one channel with one route", canary) } rt := canary.Channels[0].Routes[0] if !rt.Expired { t.Fatal("a canary promoted more than CanaryTTL ago must be marked Expired") } if rt.ExpiredFor != "8d" { t.Errorf("ExpiredFor = %q, want \"8d\"", rt.ExpiredFor) } if !rt.Canary { t.Error("an expired canary route must still carry the ordinary canary flag") } // Render it through the real template — proves the badge actually // reaches the page, with the icon and the age, beside the plain canary // badge rather than instead of it. p := page{Base: "/a/bab", Role: RoleOwner, CanPromote: true, Detail: &AppView{App: "demo", Ladder: rungs}} var buf bytes.Buffer if err := templates.ExecuteTemplate(&buf, "ladder", p); err != nil { t.Fatalf("render ladder: %v", err) } html := buf.String() line := routeLine(t, rungBlock(t, html, "canary"), "rc.example.com") if !strings.Contains(line, "chip-expired") { t.Errorf("rendered route carries no chip-expired badge:\n%s", line) } if !strings.Contains(line, "8d") { t.Errorf("rendered badge does not show the age:\n%s", line) } if !strings.Contains(line, "chip-canary") { t.Errorf("the expired badge replaced the ordinary canary badge instead of sitting beside it:\n%s", line) } if !strings.Contains(line, "