package release // The pipeline object (2026-08-20 release pipelines v2 spec §2): a // root-signed per-app ordered channel list with per-channel bake and // approval rules, replacing the implicit edge/beta/stable ladder assumption. // The properties tested here: validation catches the reserved/duplicate/ // malformed-channel cases before anything is signed; the accessors read the // object the way later tasks will (Entry, Index, ChannelNamed, // MaxApprovals); the object round-trips through the same root-only signed // wrapper discipline as production.go, including monotonic SetAt and // refusal of a copied object at the wrong app. import ( "context" "crypto/ed25519" "strings" "testing" "time" ) func TestPipelineValidation(t *testing.T) { good := []PipelineChannel{{Name: "edge"}, {Name: "production", Bake: "24h", Rules: ChannelRules{Passkey: true, PromoteApprovals: 2, ChangeApprovals: 2}}} for _, tc := range []struct { name string channels []PipelineChannel wantErr string // "" = valid }{ {"default single edge", DefaultPipelineChannels(), ""}, {"two channels with rules", good, ""}, {"empty list", nil, "at least one channel"}, {"reserved canary", []PipelineChannel{{Name: "canary"}}, "reserved"}, {"slash in name", []PipelineChannel{{Name: "canary/x"}}, "may not contain"}, {"duplicate names", []PipelineChannel{{Name: "edge"}, {Name: "edge"}}, "duplicate"}, {"bad duration", []PipelineChannel{{Name: "edge", Bake: "soon"}}, "bake"}, {"negative bake", []PipelineChannel{{Name: "edge", Bake: "-1h"}}, "bake"}, {"negative approvals", []PipelineChannel{{Name: "edge", Rules: ChannelRules{PromoteApprovals: -1}}}, "approvals"}, } { p := &Pipeline{Schema: 1, Account: "bae", App: "web", Channels: tc.channels, SetAt: time.Now()} err := validatePipeline(p, "bae", "web") if tc.wantErr == "" && err != nil { t.Errorf("%s: unexpected error %v", tc.name, err) } if tc.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) { t.Errorf("%s: err = %v, want mention of %q", tc.name, err, tc.wantErr) } } } func TestPipelineAccessors(t *testing.T) { p := &Pipeline{Schema: 1, Channels: []PipelineChannel{ {Name: "edge"}, {Name: "staging", Bake: "1h"}, {Name: "production", Bake: "24h"}}} if p.Entry() != "edge" { t.Errorf("Entry() = %q, want edge", p.Entry()) } if i := p.Index("staging"); i != 1 { t.Errorf("Index(staging) = %d, want 1", i) } if i := p.Index("canary/x"); i != -1 { t.Errorf("Index(canary/x) = %d, want -1 (canary is never a pipeline channel)", i) } if i := p.Index("nope"); i != -1 { t.Errorf("Index(nope) = %d, want -1", i) } c, ok := p.ChannelNamed("production") if !ok || c.Bake != "24h" { t.Errorf("ChannelNamed(production) = %+v, %v", c, ok) } if d, err := c.BakeWindow(); err != nil || d != 24*time.Hour { t.Errorf("BakeWindow() = %v, %v", d, err) } var zero PipelineChannel if d, err := zero.BakeWindow(); err != nil || d != 0 { t.Errorf("zero BakeWindow() = %v, %v; want 0, nil", d, err) } } func TestSetGetPipelineRoundTrip(t *testing.T) { // Mirror TestSetProductionMonotonicSetAtOnRapidFlip's harness: // dir store + root key. Missing object reads as (nil, nil). s, priv, pub := pipelineTestHarness(t) ctx := context.Background() if p, err := GetPipeline(ctx, s, pub, "bae", "web"); p != nil || err != nil { t.Fatalf("missing pipeline = %v, %v; want nil, nil", p, err) } set, err := SetPipeline(ctx, s, priv, "bae", "web", DefaultPipelineChannels(), "owner@example.test") if err != nil { t.Fatal(err) } got, err := GetPipeline(ctx, s, pub, "bae", "web") if err != nil || got == nil || got.Entry() != "edge" || got.Actor != "owner@example.test" { t.Fatalf("round trip = %+v, %v", got, err) } // Monotonic SetAt: an immediate rewrite must stamp strictly later. set2, err := SetPipeline(ctx, s, priv, "bae", "web", []PipelineChannel{{Name: "edge"}, {Name: "production", Bake: "24h"}}, "owner@example.test") if err != nil { t.Fatal(err) } if !set2.SetAt.After(set.SetAt) { t.Fatalf("SetAt not monotonic: %v then %v", set.SetAt, set2.SetAt) } } func TestGetPipelineRefusesTamperAndWrongApp(t *testing.T) { s, priv, pub := pipelineTestHarness(t) ctx := context.Background() if _, err := SetPipeline(ctx, s, priv, "bae", "web", DefaultPipelineChannels(), ""); err != nil { t.Fatal(err) } // Copy the object to another app's key: signed payload names bae/web, // so it must refuse at bae/other (same trap production.go documents). raw, _, err := s.Get(ctx, PipelineKey("bae", "web")) if err != nil { t.Fatal(err) } if _, err := s.Put(ctx, PipelineKey("bae", "other"), raw, ""); err != nil { t.Fatal(err) } if _, err := GetPipeline(ctx, s, pub, "bae", "other"); err == nil { t.Fatal("copied pipeline verified at the wrong app; want refusal") } } func TestPipelinePromotionAllowed(t *testing.T) { p := &Pipeline{Schema: 1, Channels: []PipelineChannel{ {Name: "edge"}, {Name: "staging"}, {Name: "production"}}} for _, tc := range []struct { name, from, to string wantErr string // substring; "" = allowed }{ {"canary always", "production", "canary/x", ""}, {"fresh build enters at entry", "", "edge", ""}, {"nothing else enters at entry", "staging", "edge", "-hotfix"}, {"edge to staging", "edge", "staging", ""}, {"staging to production", "staging", "production", ""}, {"skip refused", "edge", "production", "staging"}, {"unknown target names the list", "edge", "beta", "edge, staging, production"}, } { err := PipelinePromotionAllowed(p, tc.from, tc.to) if tc.wantErr == "" && err != nil { t.Errorf("%s: unexpected refusal %v", tc.name, err) } if tc.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) { t.Errorf("%s: err = %v, want mention of %q", tc.name, err, tc.wantErr) } } } func TestPipelineSingleChannelPromoteNamesSetup(t *testing.T) { p := &Pipeline{Schema: 1, Channels: DefaultPipelineChannels()} err := PipelinePromotionAllowed(p, "edge", "production") if err == nil || !strings.Contains(err.Error(), "carlos pipeline") { t.Fatalf("one-channel refusal = %v; want it to point at carlos pipeline setup (spec §5)", err) } } // pipelineTestHarness builds a fresh store and root key pair, copying // exactly how production_test.go's tests build theirs (NewDirStore over a // temp dir, testKeys for the signing pair) — there is no in-memory store in // this package; NewDirStore(t.TempDir()) is the existing convention. func pipelineTestHarness(t *testing.T) (Store, ed25519.PrivateKey, ed25519.PublicKey) { t.Helper() s := NewDirStore(t.TempDir()) priv, pub := testKeys(t) return s, priv, pub } // VersionStage is VersionRung for a pipeline, and PromoteOnLadder must // consult it — order gate, entry, hotfix bypass, and canary staying // unconditional — the moment an app has one set (task 3, spec §3/§5). func TestVersionStageAndPipelinePromote(t *testing.T) { s, priv, pub := pipelineTestHarness(t) ctx := context.Background() chans := []PipelineChannel{{Name: "edge"}, {Name: "production"}} if _, err := SetPipeline(ctx, s, priv, "bae", "web", chans, ""); err != nil { t.Fatal(err) } shipRelease(t, s, "bae", "web", "v1", "linux-arm64", []byte("bin v1")) // Fresh build promotes onto the entry channel. if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "edge", "v1", PromoteOptions{}); err != nil { t.Fatalf("promote v1 onto entry: %v", err) } pipe, err := GetPipeline(ctx, s, pub, "bae", "web") if err != nil || pipe == nil { t.Fatalf("GetPipeline: %+v, %v", pipe, err) } if st, err := VersionStage(ctx, s, pub, "bae", "web", "v1", pipe); err != nil || st != "edge" { t.Fatalf("VersionStage = %q, %v; want edge", st, err) } // edge -> production allowed; a second fresh promote onto production refused. if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v1", PromoteOptions{}); err != nil { t.Fatalf("edge->production: %v", err) } shipRelease(t, s, "bae", "web", "v2", "linux-arm64", []byte("bin v2")) if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v2", PromoteOptions{}); err == nil { t.Fatal("v2 (on no channel) promoted straight onto production; want order refusal") } // Hotfix bypasses order. if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v2", PromoteOptions{Hotfix: true}); err != nil { t.Fatalf("hotfix: %v", err) } // Rung names outside the pipeline are refused for a pipelined app. if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "stable", "v2", PromoteOptions{}); err == nil { t.Fatal("stable promoted on a pipelined app whose pipeline has no stable") } // Canary stays unconditional. if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "canary/x", "v2", PromoteOptions{}); err != nil { t.Fatalf("canary on pipelined app: %v", err) } } // A grant names frozen rungs only (grant.go), so "production" is // uncoverable: the scoped signer must refuse (spec §6), and -hotfix — which // bypasses only the ORDER gate — can never widen a delegation's reach. func TestScopedKeyCannotSignPipelineChannel(t *testing.T) { s, priv, _ := pipelineTestHarness(t) ctx := context.Background() if _, err := SetPipeline(ctx, s, priv, "bae", "web", []PipelineChannel{{Name: "edge"}, {Name: "production"}}, ""); err != nil { t.Fatal(err) } rootPub := priv.Public().(ed25519.PublicKey) key, blob := mintGrant(t, priv, "builder", []string{RungEdge}, time.Hour) sg, err := ScopedSigner(key, blob, rootPub) if err != nil { t.Fatalf("ScopedSigner: %v", err) } shipRelease(t, s, "bae", "web", "v1", "linux-arm64", []byte("bin v1")) if _, err := sg.PromoteOnLadder(ctx, s, "bae", "web", "production", "v1", PromoteOptions{Hotfix: true}); err == nil { t.Fatal("scoped key signed a pipeline channel pointer; want refusal even with --hotfix") } } // TestPipelinePromoteNamesTheChannelAsTo: PromoteResult.To was Rung(channel) // with no fallback, so a promotion onto an app-defined channel reported "" // — an empty "to" in the API's answer and, worse, an empty To in the // Promotion record written into the pointer's own audit trail, which is the // permanent record of where a build went. From already carried the channel // name on this path (VersionStage answers in channel names), so the trail // read "edge -> " for the app's whole history. The fallback is the channel's // own name; frozen rungs keep answering with the rung. func TestPipelinePromoteNamesTheChannelAsTo(t *testing.T) { s, priv, pub := pipelineTestHarness(t) ctx := context.Background() if _, err := SetPipeline(ctx, s, priv, "bae", "web", []PipelineChannel{{Name: "edge"}, {Name: "production"}}, ""); err != nil { t.Fatal(err) } shipRelease(t, s, "bae", "web", "v1", "linux-arm64", []byte("bin v1")) if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "edge", "v1", PromoteOptions{}); err != nil { t.Fatalf("promote to edge: %v", err) } res, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v1", PromoteOptions{Actor: "paul@keymail.dev"}) if err != nil { t.Fatalf("promote to production: %v", err) } if res.To != "production" { t.Errorf("PromoteResult.To = %q, want %q — an app-defined channel names itself", res.To, "production") } if res.From != "edge" { t.Errorf("PromoteResult.From = %q, want %q", res.From, "edge") } p, err := Channel(ctx, s, pub, "bae", "web", "production") if err != nil { t.Fatalf("read pointer: %v", err) } if len(p.Audit) == 0 { t.Fatalf("pointer carries no audit trail: %+v", p) } if got := p.Audit[len(p.Audit)-1]; got.To != "production" || got.From != "edge" { t.Errorf("promotion record = %+v, want from edge to production", got) } // The repeat (a no-op) names the channel on BOTH sides: the build is on // production, which is exactly what makes this a repeat, so reporting "" // for where it sits would be the same empty answer one rung down. again, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v1", PromoteOptions{}) if err != nil { t.Fatalf("repeat promote: %v", err) } if !again.NoOp { t.Errorf("repeat promote = %+v, want NoOp", again) } if again.From != "production" || again.To != "production" { t.Errorf("repeat promote from/to = %q/%q, want production/production", again.From, again.To) } } // The frozen rungs are untouched: a legacy promote still reports the RUNG, // which is not always the channel name (canary/x reports "canary"). func TestLegacyPromoteStillReportsTheRung(t *testing.T) { s, priv, _ := pipelineTestHarness(t) ctx := context.Background() shipRelease(t, s, "bae", "web", "v1", "linux-arm64", []byte("bin v1")) res, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "canary/x", "v1", PromoteOptions{}) if err != nil { t.Fatalf("promote to canary/x: %v", err) } if res.To != RungCanary { t.Errorf("PromoteResult.To = %q, want the rung %q", res.To, RungCanary) } // A different build for edge: v1 is on canary now, and the legacy ladder // only lets a build with no rung yet enter at edge. shipRelease(t, s, "bae", "web", "v2", "linux-arm64", []byte("bin v2")) res, err = PromoteOnLadder(ctx, s, priv, "bae", "web", "edge", "v2", PromoteOptions{}) if err != nil { t.Fatalf("promote to edge: %v", err) } if res.To != RungEdge { t.Errorf("PromoteResult.To = %q, want the rung %q", res.To, RungEdge) } }