carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/base64"
8 "encoding/json"
9 "reflect"
10 "sort"
11 "strings"
12 "testing"
13 "time"
14)
15
16// The wire shape of a channel pointer, pinned. Every deployment bucket in
17// production holds objects that look EXACTLY like this right now, written by
18// binaries that predate whatever is being changed today; they have to keep
19// parsing and keep verifying, byte for byte, forever. So this fixture is not
20// regenerated when the shape changes — a diff here means live pointers stopped
21// verifying, which is a fleet that cannot resolve a channel.
22//
23// Captured from the signing path on f1cc914 with a deterministic seed
24// (bytes 1..32) and a frozen promoted_at, over a payload carrying every
25// optional field a real pointer has ever had: history and an audit trail.
26const goldenRootSignedWrapper = `{
27 "payload": "ewogICJzY2hlbWEiOiAxLAogICJhcHAiOiAia2FzcyIsCiAgImNoYW5uZWwiOiAiY2FuYXJ5L3BsYXRmb3JtLWFkb3B0aW9uIiwKICAidmVyc2lvbiI6ICI5ZjNjMWFkIiwKICAicHJvbW90ZWRfYXQiOiAiMjAyNi0wNy0zMFQxMTowNDoxN1oiLAogICJoaXN0b3J5IjogWwogICAgIjFiMmU0YzciLAogICAgIjBhYzkxZGUiCiAgXSwKICAiYXVkaXQiOiBbCiAgICB7CiAgICAgICJ2ZXJzaW9uIjogIjlmM2MxYWQiLAogICAgICAiZnJvbSI6ICJlZGdlIiwKICAgICAgImFjdG9yIjogIm93bmVyQGV4YW1wbGUudGVzdCIsCiAgICAgICJhdCI6ICIyMDI2LTA3LTMwVDExOjA0OjE3WiIKICAgIH0KICBdCn0=",
28 "sig": "NZ8OU/xsAUtv5uSDzlTRJnktVXG2KD8T0ko5JogagJHdVLxC19tucSiul9nVKDNsldGdL4t09AC5nznBicuuDA=="
29}`
30
31// goldenRootPub is the public half of the fixture's signer.
32const goldenRootPub = "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ="
33
34// A pointer written by an older binary — no grant field at all — verifies
35// against the root public key and decodes to exactly what it said.
36//
37// Mutation-mindedness: if grant handling were ever made mandatory (require a
38// grant, or reject a wrapper without one), this test goes red immediately,
39// because the entire live fleet is on wrappers shaped like this one.
40func TestGoldenRootSignedWrapperStillVerifies(t *testing.T) {
41 pub, err := ParsePublicKey(goldenRootPub, "golden")
42 if err != nil {
43 t.Fatalf("ParsePublicKey: %v", err)
44 }
45 p, err := verifyPointer(pub, []byte(goldenRootSignedWrapper), "bab", "kass", "canary/platform-adoption")
46 if err != nil {
47 t.Fatalf("golden root-signed pointer no longer verifies: %v", err)
48 }
49 want := &ChannelPointer{
50 Schema: 1,
51 App: "kass",
52 Channel: "canary/platform-adoption",
53 Version: "9f3c1ad",
54 PromotedAt: time.Date(2026, 7, 30, 11, 4, 17, 0, time.UTC),
55 History: []string{"1b2e4c7", "0ac91de"},
56 Audit: []Promotion{{
57 Version: "9f3c1ad",
58 From: "edge",
59 Actor: "owner@example.test",
60 At: time.Date(2026, 7, 30, 11, 4, 17, 0, time.UTC),
61 }},
62 }
63 if !reflect.DeepEqual(p, want) {
64 t.Fatalf("golden pointer decoded as\n%+v\nwant\n%+v", p, want)
65 }
66}
67
68// The wrapper a root key writes TODAY has exactly two keys. A new wrapper
69// field that is not omitted when empty would change every pointer the fleet
70// writes and break old readers' strictness assumptions; this is the assertion
71// that catches a forgotten `omitempty`.
72//
73// Mutation-mindedness: drop `,omitempty` from any wrapper field added later
74// and this fails with the extra key named.
75func TestRootSignedWrapperHasOnlyPayloadAndSig(t *testing.T) {
76 for _, tc := range []struct {
77 name string
78 wrapper []byte
79 }{
80 {"golden fixture", []byte(goldenRootSignedWrapper)},
81 {"freshly signed", freshRootWrapper(t)},
82 } {
83 var keys map[string]json.RawMessage
84 if err := json.Unmarshal(tc.wrapper, &keys); err != nil {
85 t.Fatalf("%s: %v", tc.name, err)
86 }
87 got := make([]string, 0, len(keys))
88 for k := range keys {
89 got = append(got, k)
90 }
91 sort.Strings(got)
92 if want := []string{"payload", "sig"}; !reflect.DeepEqual(got, want) {
93 t.Errorf("%s: wrapper keys = %v, want %v", tc.name, got, want)
94 }
95 }
96}
97
98// freshRootWrapper promotes with a root key through the real store path and
99// returns the bytes that landed in the bucket — the shape a fleet reads.
100func freshRootWrapper(t *testing.T) []byte {
101 t.Helper()
102 s := NewDirStore(t.TempDir())
103 ctx := context.Background()
104 priv, _ := testKeys(t)
105 shipRelease(t, s, "bab", "kass", "9f3c1ad", "linux-arm64", []byte("bytes"))
106 if err := Promote(ctx, s, priv, "bab", "kass", "canary/platform-adoption", "9f3c1ad"); err != nil {
107 t.Fatalf("Promote: %v", err)
108 }
109 raw, _, err := s.Get(ctx, ChannelKey("bab", "kass", "canary/platform-adoption"))
110 if err != nil {
111 t.Fatalf("read back pointer: %v", err)
112 }
113 return raw
114}
115
116// The signature covers the base64 payload STRING, not the decoded JSON, so no
117// canonicalisation question can ever arise. Pinned because a refactor to
118// "verify the decoded bytes" would silently invalidate every live pointer.
119//
120// Mutation-mindedness: change signPointer/verifyPointer to sign the decoded
121// payload and the golden fixture stops verifying; this test says why.
122func TestSignatureCoversTheBase64PayloadString(t *testing.T) {
123 pub, err := ParsePublicKey(goldenRootPub, "golden")
124 if err != nil {
125 t.Fatal(err)
126 }
127 var sw signedWrapper
128 if err := json.Unmarshal([]byte(goldenRootSignedWrapper), &sw); err != nil {
129 t.Fatal(err)
130 }
131 sig, err := base64.StdEncoding.DecodeString(sw.Sig)
132 if err != nil {
133 t.Fatal(err)
134 }
135 if !ed25519.Verify(pub, []byte(sw.Payload), sig) {
136 t.Fatal("golden signature does not cover the base64 payload string")
137 }
138 decoded, err := base64.StdEncoding.DecodeString(sw.Payload)
139 if err != nil {
140 t.Fatal(err)
141 }
142 if ed25519.Verify(pub, decoded, sig) {
143 t.Fatal("golden signature covers the DECODED payload — the scheme changed")
144 }
145}
146
147// The manifest shape as it exists in every production bucket TODAY: schema 1,
148// no "capabilities" key, written by binaries that predate the field. It joins
149// the byte-identical guarantees above for the same reason they exist — these
150// objects are immutable at their bucket paths, so they can never be rewritten
151// into a newer shape, and a binary that stopped parsing them would be a fleet
152// that cannot stage a single running version.
153//
154// Mutation-mindedness: make Capabilities a non-pointer, make "v" required at
155// the manifest level, or put DisallowUnknownFields anywhere on the manifest
156// read path, and this test goes red.
157func TestYesterdaysManifestParsesWithNoCapabilities(t *testing.T) {
158 // A var, not a const: strings.Repeat is not a constant expression.
159 yesterday := `{
160 "schema": 1,
161 "app": "kass",
162 "version": "9f3c1ad",
163 "kind": "binary",
164 "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } }
165}`
166 var m Manifest
167 if err := json.Unmarshal([]byte(yesterday), &m); err != nil {
168 t.Fatalf("a manifest already in every production bucket stopped parsing: %v", err)
169 }
170 if m.Capabilities != nil {
171 t.Fatalf("Capabilities = %+v, want nil (absent means none)", m.Capabilities)
172 }
173 if err := validateManifest(&m); err != nil {
174 t.Fatalf("validate: %v", err)
175 }
176 // And a capability-free manifest still marshals with NO capabilities key —
177 // which is what keeps Ship's putImmutable idempotent against bytes an
178 // older binary already wrote.
179 raw, err := json.Marshal(&m)
180 if err != nil {
181 t.Fatal(err)
182 }
183 if bytes.Contains(raw, []byte("capabilities")) {
184 t.Fatalf("a capability-free manifest grew a key: %s", raw)
185 }
186}
187
188// The other direction of the same forward-compatibility contract: a manifest
189// carrying keys THIS binary has never heard of at the TOP level still parses,
190// because encoding/json ignores unknown fields and nothing on the manifest
191// read path turns that off. That is what lets an old binary keep running
192// against a bucket a newer one has shipped into, and it is why the pinned
193// deploy order (platform binaries first, capability-bearing releases second)
194// is belt-and-braces rather than load-bearing.
195func TestAManifestWithFutureTopLevelKeysStillParses(t *testing.T) {
196 tomorrow := `{
197 "schema": 1,
198 "app": "kass",
199 "version": "9f3c1ad",
200 "kind": "binary",
201 "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } },
202 "capabilities": { "v": 1, "daemon": true },
203 "some_future_key": { "nested": [1, 2, 3] }
204}`
205 var m Manifest
206 if err := json.Unmarshal([]byte(tomorrow), &m); err != nil {
207 t.Fatalf("unknown TOP-LEVEL keys must be ignored, not refused: %v", err)
208 }
209 if err := validateManifest(&m); err != nil {
210 t.Fatalf("validate: %v", err)
211 }
212 if m.Capabilities == nil || !m.Capabilities.Daemon {
213 t.Fatalf("Capabilities = %+v, want the daemon block decoded", m.Capabilities)
214 }
215}
216
217// A manifest from BEFORE the release versioning spec (2026-08-08): no
218// "label" or "notes" keys at all, same as every manifest already sitting in
219// a production bucket today. It has to keep parsing, and Label/Notes have to
220// come back empty rather than erroring — that is the wire back-compat lock
221// for the new fields, the same guarantee TestYesterdaysManifestParsesWithNoCapabilities
222// gives Capabilities.
223//
224// Mutation-mindedness: make Label or Notes required, or reject a manifest
225// missing either key, and this test goes red — which would mean every
226// manifest shipped before today's change stopped parsing.
227func TestYesterdaysManifestParsesWithNoLabelOrNotes(t *testing.T) {
228 yesterday := `{
229 "schema": 1,
230 "app": "kass",
231 "version": "9f3c1ad",
232 "kind": "binary",
233 "commit": "9f3c1ad",
234 "artifacts": { "linux-arm64": { "file": "linux-arm64", "sha256": "` + strings.Repeat("ab", 32) + `", "size": 12 } }
235}`
236 var m Manifest
237 if err := json.Unmarshal([]byte(yesterday), &m); err != nil {
238 t.Fatalf("a manifest already in every production bucket stopped parsing: %v", err)
239 }
240 if m.Label != "" || m.Notes != "" {
241 t.Fatalf("Label=%q Notes=%q, want both empty — absent means none", m.Label, m.Notes)
242 }
243 if err := validateManifest(&m); err != nil {
244 t.Fatalf("validate: %v", err)
245 }
246}
247
248// goldenLabeledManifest is a manifest carrying the release-versioning spec's
249// two new fields: Label (a short one-line intent) and Notes (a changelog
250// fragment), both written at ship time. Captured from json.MarshalIndent
251// on schema 1 with every field the ship path populates set.
252//
253// Mutation-mindedness: reorder the Manifest struct's fields, drop
254// `,omitempty` from Label or Notes, or rename either json tag, and the
255// marshal half of this test fails; corrupt the decoder and the unmarshal
256// half does.
257//
258// A var, not a const: strings.Repeat is not a constant expression (same
259// reason TestYesterdaysManifestParsesWithNoCapabilities's "yesterday" is a
260// var too).
261var goldenLabeledManifest = `{
262 "schema": 1,
263 "app": "kass",
264 "version": "9f3c1ad",
265 "kind": "binary",
266 "commit": "9f3c1ad",
267 "built_by": "ci@carlosframework.com",
268 "built_at": "2026-08-08T09:15:00Z",
269 "label": "fixing modals",
270 "notes": "- focus trap fixed\n- escape closes on nested dialogs",
271 "artifacts": {
272 "linux-arm64": {
273 "file": "linux-arm64",
274 "sha256": "` + strings.Repeat("ab", 32) + `",
275 "size": 12
276 }
277 }
278}`
279
280func goldenLabeledManifestValue() *Manifest {
281 return &Manifest{
282 Schema: 1,
283 App: "kass",
284 Version: "9f3c1ad",
285 Kind: "binary",
286 Commit: "9f3c1ad",
287 BuiltBy: "ci@carlosframework.com",
288 BuiltAt: "2026-08-08T09:15:00Z",
289 Label: "fixing modals",
290 Notes: "- focus trap fixed\n- escape closes on nested dialogs",
291 Artifacts: map[string]Artifact{
292 "linux-arm64": {File: "linux-arm64", SHA256: strings.Repeat("ab", 32), Size: 12},
293 },
294 }
295}
296
297// TestGoldenLabeledManifestRoundTrips is the labeled-manifest half of the
298// wire-shape lock: the Go type marshals to exactly the golden bytes, and the
299// golden bytes decode to exactly the Go value — both directions, so a
300// regression in either the encoder or the decoder is caught here rather than
301// downstream in Ship or console rendering.
302func TestGoldenLabeledManifestRoundTrips(t *testing.T) {
303 want := goldenLabeledManifestValue()
304 raw, err := json.MarshalIndent(want, "", " ")
305 if err != nil {
306 t.Fatalf("marshal: %v", err)
307 }
308 if string(raw) != goldenLabeledManifest {
309 t.Fatalf("marshaled manifest =\n%s\nwant golden\n%s", raw, goldenLabeledManifest)
310 }
311 var got Manifest
312 if err := json.Unmarshal([]byte(goldenLabeledManifest), &got); err != nil {
313 t.Fatalf("unmarshal: %v", err)
314 }
315 if !reflect.DeepEqual(&got, want) {
316 t.Fatalf("golden manifest decoded as\n%+v\nwant\n%+v", got, want)
317 }
318 if err := validateManifest(&got); err != nil {
319 t.Fatalf("validate: %v", err)
320 }
321}
322
323// goldenVersionTarget is the version-target.json bucket object (release
324// versioning spec, 2026-08-08; see target.go's TargetKey) — the per-app
325// "what we're building toward" that SetTarget writes and GetTarget reads.
326// Schema 1, every field set.
327//
328// Mutation-mindedness: reorder VersionTarget's fields, rename a json tag, or
329// drop `,omitempty` from SetBy, and the marshal half fails; break the
330// decoder's handling of set_at as RFC3339 and the unmarshal half does.
331const goldenVersionTarget = `{
332 "schema": 1,
333 "target": "0.5.0",
334 "set_by": "paul@example.com",
335 "set_at": "2026-08-08T09:15:00Z"
336}`
337
338func goldenVersionTargetValue() *VersionTarget {
339 return &VersionTarget{
340 Schema: 1,
341 Target: "0.5.0",
342 SetBy: "paul@example.com",
343 SetAt: time.Date(2026, 8, 8, 9, 15, 0, 0, time.UTC),
344 }
345}
346
347// TestGoldenVersionTargetRoundTrips locks the version-target.json wire shape
348// down in both directions, matching the pattern every other object in this
349// file follows.
350func TestGoldenVersionTargetRoundTrips(t *testing.T) {
351 want := goldenVersionTargetValue()
352 raw, err := json.MarshalIndent(want, "", " ")
353 if err != nil {
354 t.Fatalf("marshal: %v", err)
355 }
356 if string(raw) != goldenVersionTarget {
357 t.Fatalf("marshaled version target =\n%s\nwant golden\n%s", raw, goldenVersionTarget)
358 }
359 var got VersionTarget
360 if err := json.Unmarshal([]byte(goldenVersionTarget), &got); err != nil {
361 t.Fatalf("unmarshal: %v", err)
362 }
363 if !reflect.DeepEqual(&got, want) {
364 t.Fatalf("golden version target decoded as\n%+v\nwant\n%+v", got, want)
365 }
366}
367
368// goldenTagWithChangelog is a Tag as written by EnsureTag at promote-to-stable
369// time: every field populated, including the compiled changelog spanning
370// more than one iteration. Tags are unsigned read-side sugar (tag.go's
371// package doc), so unlike a channel pointer this shape has no signature to
372// pin — just the JSON.
373//
374// Mutation-mindedness: reorder Tag's fields, rename a json tag, or drop
375// `,omitempty` from Commit/Actor/Changelog, and the marshal half fails.
376const goldenTagWithChangelog = `{
377 "schema": 1,
378 "version": "0.5.0",
379 "release": "0.5.0-3",
380 "commit": "9f3c1ad",
381 "tagged_at": "2026-08-08T09:15:00Z",
382 "actor": "paul@example.com",
383 "changelog": "## 0.5.0-1 — fixing modals\n\n- focus trap fixed\n\n## 0.5.0-3 — polish\n\n- rounded corners\n"
384}`
385
386func goldenTagWithChangelogValue() *Tag {
387 return &Tag{
388 Schema: 1,
389 Version: "0.5.0",
390 Release: "0.5.0-3",
391 Commit: "9f3c1ad",
392 TaggedAt: time.Date(2026, 8, 8, 9, 15, 0, 0, time.UTC),
393 Actor: "paul@example.com",
394 Changelog: "## 0.5.0-1 — fixing modals\n\n- focus trap fixed\n\n## 0.5.0-3 — polish\n\n- rounded corners\n",
395 }
396}
397
398// TestGoldenTagWithChangelogRoundTrips locks the tag wire shape down in both
399// directions.
400func TestGoldenTagWithChangelogRoundTrips(t *testing.T) {
401 want := goldenTagWithChangelogValue()
402 raw, err := json.MarshalIndent(want, "", " ")
403 if err != nil {
404 t.Fatalf("marshal: %v", err)
405 }
406 if string(raw) != goldenTagWithChangelog {
407 t.Fatalf("marshaled tag =\n%s\nwant golden\n%s", raw, goldenTagWithChangelog)
408 }
409 var got Tag
410 if err := json.Unmarshal([]byte(goldenTagWithChangelog), &got); err != nil {
411 t.Fatalf("unmarshal: %v", err)
412 }
413 if !reflect.DeepEqual(&got, want) {
414 t.Fatalf("golden tag decoded as\n%+v\nwant\n%+v", got, want)
415 }
416}
417