package release import ( "bytes" "context" "crypto/ed25519" "encoding/base64" "encoding/json" "reflect" "sort" "strings" "testing" "time" ) // The wire shape of a channel pointer, pinned. Every deployment bucket in // production holds objects that look EXACTLY like this right now, written by // binaries that predate whatever is being changed today; they have to keep // parsing and keep verifying, byte for byte, forever. So this fixture is not // regenerated when the shape changes — a diff here means live pointers stopped // verifying, which is a fleet that cannot resolve a channel. // // Captured from the signing path on f1cc914 with a deterministic seed // (bytes 1..32) and a frozen promoted_at, over a payload carrying every // optional field a real pointer has ever had: history and an audit trail. const goldenRootSignedWrapper = `{ "payload": "ewogICJzY2hlbWEiOiAxLAogICJhcHAiOiAia2FzcyIsCiAgImNoYW5uZWwiOiAiY2FuYXJ5L3BsYXRmb3JtLWFkb3B0aW9uIiwKICAidmVyc2lvbiI6ICI5ZjNjMWFkIiwKICAicHJvbW90ZWRfYXQiOiAiMjAyNi0wNy0zMFQxMTowNDoxN1oiLAogICJoaXN0b3J5IjogWwogICAgIjFiMmU0YzciLAogICAgIjBhYzkxZGUiCiAgXSwKICAiYXVkaXQiOiBbCiAgICB7CiAgICAgICJ2ZXJzaW9uIjogIjlmM2MxYWQiLAogICAgICAiZnJvbSI6ICJlZGdlIiwKICAgICAgImFjdG9yIjogIm93bmVyQGV4YW1wbGUudGVzdCIsCiAgICAgICJhdCI6ICIyMDI2LTA3LTMwVDExOjA0OjE3WiIKICAgIH0KICBdCn0=", "sig": "NZ8OU/xsAUtv5uSDzlTRJnktVXG2KD8T0ko5JogagJHdVLxC19tucSiul9nVKDNsldGdL4t09AC5nznBicuuDA==" }` // goldenRootPub is the public half of the fixture's signer. const goldenRootPub = "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=" // A pointer written by an older binary — no grant field at all — verifies // against the root public key and decodes to exactly what it said. // // Mutation-mindedness: if grant handling were ever made mandatory (require a // grant, or reject a wrapper without one), this test goes red immediately, // because the entire live fleet is on wrappers shaped like this one. func TestGoldenRootSignedWrapperStillVerifies(t *testing.T) { pub, err := ParsePublicKey(goldenRootPub, "golden") if err != nil { t.Fatalf("ParsePublicKey: %v", err) } p, err := verifyPointer(pub, []byte(goldenRootSignedWrapper), "bab", "kass", "canary/platform-adoption") if err != nil { t.Fatalf("golden root-signed pointer no longer verifies: %v", err) } want := &ChannelPointer{ Schema: 1, App: "kass", Channel: "canary/platform-adoption", Version: "9f3c1ad", PromotedAt: time.Date(2026, 7, 30, 11, 4, 17, 0, time.UTC), History: []string{"1b2e4c7", "0ac91de"}, Audit: []Promotion{{ Version: "9f3c1ad", From: "edge", Actor: "owner@example.test", At: time.Date(2026, 7, 30, 11, 4, 17, 0, time.UTC), }}, } if !reflect.DeepEqual(p, want) { t.Fatalf("golden pointer decoded as\n%+v\nwant\n%+v", p, want) } } // The wrapper a root key writes TODAY has exactly two keys. A new wrapper // field that is not omitted when empty would change every pointer the fleet // writes and break old readers' strictness assumptions; this is the assertion // that catches a forgotten `omitempty`. // // Mutation-mindedness: drop `,omitempty` from any wrapper field added later // and this fails with the extra key named. func TestRootSignedWrapperHasOnlyPayloadAndSig(t *testing.T) { for _, tc := range []struct { name string wrapper []byte }{ {"golden fixture", []byte(goldenRootSignedWrapper)}, {"freshly signed", freshRootWrapper(t)}, } { var keys map[string]json.RawMessage if err := json.Unmarshal(tc.wrapper, &keys); err != nil { t.Fatalf("%s: %v", tc.name, err) } got := make([]string, 0, len(keys)) for k := range keys { got = append(got, k) } sort.Strings(got) if want := []string{"payload", "sig"}; !reflect.DeepEqual(got, want) { t.Errorf("%s: wrapper keys = %v, want %v", tc.name, got, want) } } } // freshRootWrapper promotes with a root key through the real store path and // returns the bytes that landed in the bucket — the shape a fleet reads. func freshRootWrapper(t *testing.T) []byte { t.Helper() s := NewDirStore(t.TempDir()) ctx := context.Background() priv, _ := testKeys(t) shipRelease(t, s, "bab", "kass", "9f3c1ad", "linux-arm64", []byte("bytes")) if err := Promote(ctx, s, priv, "bab", "kass", "canary/platform-adoption", "9f3c1ad"); err != nil { t.Fatalf("Promote: %v", err) } raw, _, err := s.Get(ctx, ChannelKey("bab", "kass", "canary/platform-adoption")) if err != nil { t.Fatalf("read back pointer: %v", err) } return raw } // The signature covers the base64 payload STRING, not the decoded JSON, so no // canonicalisation question can ever arise. Pinned because a refactor to // "verify the decoded bytes" would silently invalidate every live pointer. // // Mutation-mindedness: change signPointer/verifyPointer to sign the decoded // payload and the golden fixture stops verifying; this test says why. func TestSignatureCoversTheBase64PayloadString(t *testing.T) { pub, err := ParsePublicKey(goldenRootPub, "golden") if err != nil { t.Fatal(err) } var sw signedWrapper if err := json.Unmarshal([]byte(goldenRootSignedWrapper), &sw); err != nil { t.Fatal(err) } sig, err := base64.StdEncoding.DecodeString(sw.Sig) if err != nil { t.Fatal(err) } if !ed25519.Verify(pub, []byte(sw.Payload), sig) { t.Fatal("golden signature does not cover the base64 payload string") } decoded, err := base64.StdEncoding.DecodeString(sw.Payload) if err != nil { t.Fatal(err) } if ed25519.Verify(pub, decoded, sig) { t.Fatal("golden signature covers the DECODED payload — the scheme changed") } } // The manifest shape as it exists in every production bucket TODAY: schema 1, // no "capabilities" key, written by binaries that predate the field. It joins // the byte-identical guarantees above for the same reason they exist — these // objects are immutable at their bucket paths, so they can never be rewritten // into a newer shape, and a binary that stopped parsing them would be a fleet // that cannot stage a single running version. // // Mutation-mindedness: make Capabilities a non-pointer, make "v" required at // the manifest level, or put DisallowUnknownFields anywhere on the manifest // read path, and this test goes red. func TestYesterdaysManifestParsesWithNoCapabilities(t *testing.T) { // A var, not a const: strings.Repeat is not a constant expression. yesterday := `{ "schema": 1, "app": "kass", "version": "9f3c1ad", "kind": "binary", "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } } }` var m Manifest if err := json.Unmarshal([]byte(yesterday), &m); err != nil { t.Fatalf("a manifest already in every production bucket stopped parsing: %v", err) } if m.Capabilities != nil { t.Fatalf("Capabilities = %+v, want nil (absent means none)", m.Capabilities) } if err := validateManifest(&m); err != nil { t.Fatalf("validate: %v", err) } // And a capability-free manifest still marshals with NO capabilities key — // which is what keeps Ship's putImmutable idempotent against bytes an // older binary already wrote. raw, err := json.Marshal(&m) if err != nil { t.Fatal(err) } if bytes.Contains(raw, []byte("capabilities")) { t.Fatalf("a capability-free manifest grew a key: %s", raw) } } // The other direction of the same forward-compatibility contract: a manifest // carrying keys THIS binary has never heard of at the TOP level still parses, // because encoding/json ignores unknown fields and nothing on the manifest // read path turns that off. That is what lets an old binary keep running // against a bucket a newer one has shipped into, and it is why the pinned // deploy order (platform binaries first, capability-bearing releases second) // is belt-and-braces rather than load-bearing. func TestAManifestWithFutureTopLevelKeysStillParses(t *testing.T) { tomorrow := `{ "schema": 1, "app": "kass", "version": "9f3c1ad", "kind": "binary", "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } }, "capabilities": { "v": 1, "daemon": true }, "some_future_key": { "nested": [1, 2, 3] } }` var m Manifest if err := json.Unmarshal([]byte(tomorrow), &m); err != nil { t.Fatalf("unknown TOP-LEVEL keys must be ignored, not refused: %v", err) } if err := validateManifest(&m); err != nil { t.Fatalf("validate: %v", err) } if m.Capabilities == nil || !m.Capabilities.Daemon { t.Fatalf("Capabilities = %+v, want the daemon block decoded", m.Capabilities) } } // Yesterday's binary manifest has no processes, hibernate or jit keys. It must // keep parsing with all three at their zero values, and marshalling it back // must not grow them: a binary or static manifest stays byte for byte as it // was (rootfs apps Plan 1, additive wire shape). func TestYesterdaysManifestParsesWithNoProcesses(t *testing.T) { yesterday := `{ "schema": 1, "app": "kass", "version": "9f3c1ad", "kind": "binary", "commit": "9f3c1ad", "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } } }` var m Manifest if err := json.Unmarshal([]byte(yesterday), &m); err != nil { t.Fatalf("a manifest written before rootfs apps no longer parses: %v", err) } if m.Processes != nil || m.Hibernate || m.JIT { t.Fatalf("absent keys decoded as processes=%v hibernate=%v jit=%v", m.Processes, m.Hibernate, m.JIT) } if err := validateManifest(&m); err != nil { t.Fatalf("validate: %v", err) } out, err := json.MarshalIndent(m, "", " ") if err != nil { t.Fatal(err) } for _, key := range []string{`"processes"`, `"hibernate"`, `"jit"`} { if bytes.Contains(out, []byte(key)) { t.Errorf("an empty %s leaked into the marshal:\n%s", key, out) } } } // A manifest from BEFORE the release versioning spec (2026-08-08): no // "label" or "notes" keys at all, same as every manifest already sitting in // a production bucket today. It has to keep parsing, and Label/Notes have to // come back empty rather than erroring — that is the wire back-compat lock // for the new fields, the same guarantee TestYesterdaysManifestParsesWithNoCapabilities // gives Capabilities. // // Mutation-mindedness: make Label or Notes required, or reject a manifest // missing either key, and this test goes red — which would mean every // manifest shipped before today's change stopped parsing. func TestYesterdaysManifestParsesWithNoLabelOrNotes(t *testing.T) { yesterday := `{ "schema": 1, "app": "kass", "version": "9f3c1ad", "kind": "binary", "commit": "9f3c1ad", "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } } }` var m Manifest if err := json.Unmarshal([]byte(yesterday), &m); err != nil { t.Fatalf("a manifest already in every production bucket stopped parsing: %v", err) } if m.Label != "" || m.Notes != "" { t.Fatalf("Label=%q Notes=%q, want both empty — absent means none", m.Label, m.Notes) } if err := validateManifest(&m); err != nil { t.Fatalf("validate: %v", err) } } // goldenLabeledManifest is a manifest carrying the release-versioning spec's // two new fields: Label (a short one-line intent) and Notes (a changelog // fragment), both written at ship time. Captured from json.MarshalIndent // on schema 1 with every field the ship path populates set. // // Mutation-mindedness: reorder the Manifest struct's fields, drop // `,omitempty` from Label or Notes, or rename either json tag, and the // marshal half of this test fails; corrupt the decoder and the unmarshal // half does. // // A var, not a const: strings.Repeat is not a constant expression (same // reason TestYesterdaysManifestParsesWithNoCapabilities's "yesterday" is a // var too). var goldenLabeledManifest = `{ "schema": 1, "app": "kass", "version": "9f3c1ad", "kind": "binary", "commit": "9f3c1ad", "built_by": "ci@carlosframework.com", "built_at": "2026-08-08T09:15:00Z", "label": "fixing modals", "notes": "- focus trap fixed\n- escape closes on nested dialogs", "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } } }` func goldenLabeledManifestValue() *Manifest { return &Manifest{ Schema: 1, App: "kass", Version: "9f3c1ad", Kind: "binary", Commit: "9f3c1ad", BuiltBy: "ci@carlosframework.com", BuiltAt: "2026-08-08T09:15:00Z", Label: "fixing modals", Notes: "- focus trap fixed\n- escape closes on nested dialogs", Artifacts: map[string]Artifact{ "linux-arm64": {File: "linux-arm64", SHA256: strings.Repeat("ab", 32), Size: 12}, }, } } // TestGoldenLabeledManifestRoundTrips is the labeled-manifest half of the // wire-shape lock: the Go type marshals to exactly the golden bytes, and the // golden bytes decode to exactly the Go value — both directions, so a // regression in either the encoder or the decoder is caught here rather than // downstream in Ship or console rendering. func TestGoldenLabeledManifestRoundTrips(t *testing.T) { want := goldenLabeledManifestValue() raw, err := json.MarshalIndent(want, "", " ") if err != nil { t.Fatalf("marshal: %v", err) } if string(raw) != goldenLabeledManifest { t.Fatalf("marshaled manifest =\n%s\nwant golden\n%s", raw, goldenLabeledManifest) } var got Manifest if err := json.Unmarshal([]byte(goldenLabeledManifest), &got); err != nil { t.Fatalf("unmarshal: %v", err) } if !reflect.DeepEqual(&got, want) { t.Fatalf("golden manifest decoded as\n%+v\nwant\n%+v", got, want) } if err := validateManifest(&got); err != nil { t.Fatalf("validate: %v", err) } } // goldenVersionTarget is the version-target.json bucket object (release // versioning spec, 2026-08-08; see target.go's TargetKey) — the per-app // "what we're building toward" that SetTarget writes and GetTarget reads. // Schema 1, every field set. // // Mutation-mindedness: reorder VersionTarget's fields, rename a json tag, or // drop `,omitempty` from SetBy, and the marshal half fails; break the // decoder's handling of set_at as RFC3339 and the unmarshal half does. const goldenVersionTarget = `{ "schema": 1, "target": "0.5.0", "set_by": "paul@example.com", "set_at": "2026-08-08T09:15:00Z" }` func goldenVersionTargetValue() *VersionTarget { return &VersionTarget{ Schema: 1, Target: "0.5.0", SetBy: "paul@example.com", SetAt: time.Date(2026, 8, 8, 9, 15, 0, 0, time.UTC), } } // TestGoldenVersionTargetRoundTrips locks the version-target.json wire shape // down in both directions, matching the pattern every other object in this // file follows. func TestGoldenVersionTargetRoundTrips(t *testing.T) { want := goldenVersionTargetValue() raw, err := json.MarshalIndent(want, "", " ") if err != nil { t.Fatalf("marshal: %v", err) } if string(raw) != goldenVersionTarget { t.Fatalf("marshaled version target =\n%s\nwant golden\n%s", raw, goldenVersionTarget) } var got VersionTarget if err := json.Unmarshal([]byte(goldenVersionTarget), &got); err != nil { t.Fatalf("unmarshal: %v", err) } if !reflect.DeepEqual(&got, want) { t.Fatalf("golden version target decoded as\n%+v\nwant\n%+v", got, want) } } // goldenTagWithChangelog is a Tag as written by EnsureTag at promote-to-stable // time: every field populated, including the compiled changelog spanning // more than one iteration. Tags are unsigned read-side sugar (tag.go's // package doc), so unlike a channel pointer this shape has no signature to // pin — just the JSON. // // Mutation-mindedness: reorder Tag's fields, rename a json tag, or drop // `,omitempty` from Commit/Actor/Changelog, and the marshal half fails. const goldenTagWithChangelog = `{ "schema": 1, "version": "0.5.0", "release": "0.5.0-3", "commit": "9f3c1ad", "tagged_at": "2026-08-08T09:15:00Z", "actor": "paul@example.com", "changelog": "## 0.5.0-1 — fixing modals\n\n- focus trap fixed\n\n## 0.5.0-3 — polish\n\n- rounded corners\n" }` func goldenTagWithChangelogValue() *Tag { return &Tag{ Schema: 1, Version: "0.5.0", Release: "0.5.0-3", Commit: "9f3c1ad", TaggedAt: time.Date(2026, 8, 8, 9, 15, 0, 0, time.UTC), Actor: "paul@example.com", Changelog: "## 0.5.0-1 — fixing modals\n\n- focus trap fixed\n\n## 0.5.0-3 — polish\n\n- rounded corners\n", } } // TestGoldenTagWithChangelogRoundTrips locks the tag wire shape down in both // directions. func TestGoldenTagWithChangelogRoundTrips(t *testing.T) { want := goldenTagWithChangelogValue() raw, err := json.MarshalIndent(want, "", " ") if err != nil { t.Fatalf("marshal: %v", err) } if string(raw) != goldenTagWithChangelog { t.Fatalf("marshaled tag =\n%s\nwant golden\n%s", raw, goldenTagWithChangelog) } var got Tag if err := json.Unmarshal([]byte(goldenTagWithChangelog), &got); err != nil { t.Fatalf("unmarshal: %v", err) } if !reflect.DeepEqual(&got, want) { t.Fatalf("golden tag decoded as\n%+v\nwant\n%+v", got, want) } }