release: golden fixtures for target, tag and labeled manifests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+201
−0
internal/release/golden_test.go+201 −0
diff --git a/internal/release/golden_test.go b/internal/release/golden_test.go| index 4b4a0df76..b9ef53820 100644 |
| --- a/internal/release/golden_test.go |
| +++ b/internal/release/golden_test.go |
| @@ -213,3 +213,204 @@ func TestAManifestWithFutureTopLevelKeysStillParses(t *testing.T) { |
| t.Fatalf("Capabilities = %+v, want the daemon block decoded", m.Capabilities) |
| } |
| } |
| + |
| +// 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) |
| + } |
| +} |