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, "