| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "strings" |
| 8 | "syscall" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/hostwrite" |
| 12 | ) |
| 13 | |
| 14 | type heldClaimWork struct { |
| 15 | ids []hostwrite.Identity |
| 16 | shut bool |
| 17 | } |
| 18 | |
| 19 | func (w *heldClaimWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error { |
| 20 | w.ids = append(w.ids, id) |
| 21 | if w.shut { |
| 22 | return hostwrite.ErrUnavailable |
| 23 | } |
| 24 | return fn() |
| 25 | } |
| 26 | |
| 27 | // TestMaterializeHeldUnderPublishesThroughTheClaimSelection (moves Plan 3 |
| 28 | // Task 5, H13): with CARLOS_HOST_CLAIM on, the held release is staged as |
| 29 | // adoption stages and live/<host> is pointed at it inside the claim |
| 30 | // selection, under the identity naming exactly the pinned version, with the |
| 31 | // caller's lock inside it. |
| 32 | func TestMaterializeHeldUnderPublishesThroughTheClaimSelection(t *testing.T) { |
| 33 | ctx, s, opt := heldFixture(t) |
| 34 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 35 | work := &heldClaimWork{} |
| 36 | locked := 0 |
| 37 | m, err := MaterializeHeldUnder(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), work, func(publish func() error) error { |
| 38 | locked++ |
| 39 | return publish() |
| 40 | }) |
| 41 | if err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | if m == nil || m.Version != "v1" { |
| 45 | t.Fatalf("manifest %+v", m) |
| 46 | } |
| 47 | if got, err := LiveVersion(opt, heldTestHost); err != nil || got != "v1" { |
| 48 | t.Fatalf("LiveVersion = %q, %v", got, err) |
| 49 | } |
| 50 | if len(work.ids) != 1 || work.ids[0] != (hostwrite.Identity{Release: "v1"}) || locked != 1 { |
| 51 | t.Fatalf("publication ran under %v with the lock taken %d times", work.ids, locked) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | // A refused selection never points live/<host>; a missing selection is |
| 56 | // refused before anything is staged. |
| 57 | func TestMaterializeHeldUnderRefusals(t *testing.T) { |
| 58 | ctx, s, opt := heldFixture(t) |
| 59 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 60 | if _, err := MaterializeHeldUnder(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), nil, nil); !errors.Is(err, errHeldNoGuard) { |
| 61 | t.Fatalf("no selection: %v", err) |
| 62 | } |
| 63 | if _, err := MaterializeHeldUnder(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), &heldClaimWork{shut: true}, nil); !errors.Is(err, hostwrite.ErrUnavailable) { |
| 64 | t.Fatalf("a refused selection: %v", err) |
| 65 | } |
| 66 | if got, _ := LiveVersion(opt, heldTestHost); got != "" { |
| 67 | t.Fatalf("live/<host> names %q", got) |
| 68 | } |
| 69 | noOp := heldRoute() |
| 70 | noOp.HoldOp = "" |
| 71 | if _, err := MaterializeHeldUnder(ctx, s, heldPub, nr(t), opt, noOp, "v1", offGate(t), &heldClaimWork{}, nil); err == nil { |
| 72 | t.Fatal("a row no move holds was staged") |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // Settings spec rev 11 §4.3a on the claim path: MaterializeHeldUnder stages |
| 77 | // only a build a promotion record or a ratchet-admitted pointer authorizes, |
| 78 | // bound to that digest. A build nothing authorizes, or a replayed record over |
| 79 | // other bytes under the same name, is refused before an artifact byte is read |
| 80 | // or written: nothing staged, no selection entered, live/<host> untouched. |
| 81 | func TestMaterializeHeldUnderStagesOnlyAnAuthorizedBuild(t *testing.T) { |
| 82 | t.Run("nothing authorizes it", func(t *testing.T) { |
| 83 | ctx, s, opt := heldFixture(t) |
| 84 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 85 | shipRelease(t, s, "bab", "console", "v3", adoptPlatform, []byte("shipped, never promoted")) |
| 86 | rec := &heldRecordingStore{Store: s} |
| 87 | work := &heldClaimWork{} |
| 88 | if _, err := MaterializeHeldUnder(ctx, rec, heldPub, nr(t), opt, heldRoute(), "v3", offGate(t), work, nil); !errors.Is(err, ErrHeldNotAuthorized) { |
| 89 | t.Fatalf("MaterializeHeldUnder of a never-promoted build = %v; want ErrHeldNotAuthorized", err) |
| 90 | } |
| 91 | heldClaimNothingStaged(t, rec, work, opt, "v3", false) |
| 92 | }) |
| 93 | t.Run("a replayed record over other bytes", func(t *testing.T) { |
| 94 | ctx, s, opt := heldFixture(t) |
| 95 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 96 | saved, _, err := s.Get(ctx, PromotionRecordKey("bab", "console", "v1")) |
| 97 | if err != nil { |
| 98 | t.Fatal(err) |
| 99 | } |
| 100 | if err := DeleteVersion(ctx, s, "bab", "console", "v1"); err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("nobody promoted these")) |
| 104 | if _, err := s.Put(ctx, PromotionRecordKey("bab", "console", "v1"), saved, ""); err != nil { |
| 105 | t.Fatal(err) |
| 106 | } |
| 107 | rec := &heldRecordingStore{Store: s} |
| 108 | work := &heldClaimWork{} |
| 109 | if _, err := MaterializeHeldUnder(ctx, rec, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), work, nil); !errors.Is(err, ErrManifestDigest) { |
| 110 | t.Fatalf("MaterializeHeldUnder under a replayed record = %v; want ErrManifestDigest", err) |
| 111 | } |
| 112 | heldClaimNothingStaged(t, rec, work, opt, "v1", true) |
| 113 | }) |
| 114 | } |
| 115 | |
| 116 | // heldClaimNothingStaged fails the test if a claim-path held stage of version |
| 117 | // read its artifact, staged it, entered the selection or pointed live/<host>. |
| 118 | // manifestRead says whether the stage was allowed to read the manifest (to |
| 119 | // check a digest) before refusing. |
| 120 | func heldClaimNothingStaged(t *testing.T, rec *heldRecordingStore, work *heldClaimWork, opt, version string, manifestRead bool) { |
| 121 | t.Helper() |
| 122 | rel := ReleasesPrefix("bab", "console") + version + "/" |
| 123 | manifest := ManifestKey("bab", "console", version) |
| 124 | rec.mu.Lock() |
| 125 | for _, c := range rec.calls { |
| 126 | key, ok := strings.CutPrefix(c, "get ") |
| 127 | if !ok || !strings.HasPrefix(key, rel) { |
| 128 | continue |
| 129 | } |
| 130 | if manifestRead && key == manifest { |
| 131 | continue |
| 132 | } |
| 133 | rec.mu.Unlock() |
| 134 | t.Fatalf("the refused held stage read %s", key) |
| 135 | } |
| 136 | rec.mu.Unlock() |
| 137 | if _, err := os.Stat(VersionPath(opt, "bab", version, "console")); err == nil { |
| 138 | t.Fatalf("%s was staged", version) |
| 139 | } |
| 140 | if len(work.ids) != 0 { |
| 141 | t.Fatalf("the refused held stage entered the selection under %v", work.ids) |
| 142 | } |
| 143 | if got, _ := LiveVersion(opt, heldTestHost); got != "" { |
| 144 | t.Fatalf("live/<host> names %q", got) |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | // TestStageHeldVerifiedStagesWithoutPublishing (moves after claim Plan 12, |
| 149 | // Task 7; Paul's T7 ruling of 2026-10-12): under the claim, T7 stages the |
| 150 | // pinned release into the shared version store and verifies it, §4.1's |
| 151 | // authorization and D3's admit before the stage and D3's bind on the staged |
| 152 | // bytes after it, and never touches live/<host>: the publication is the |
| 153 | // landing's, at configured, under its held-release selection. |
| 154 | func TestStageHeldVerifiedStagesWithoutPublishing(t *testing.T) { |
| 155 | t.Run("staged, live untouched", func(t *testing.T) { |
| 156 | ctx, s, opt := heldFixture(t) |
| 157 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 158 | m, err := StageHeldVerified(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t)) |
| 159 | if err != nil { |
| 160 | t.Fatal(err) |
| 161 | } |
| 162 | if m == nil || m.Version != "v1" { |
| 163 | t.Fatalf("manifest %+v", m) |
| 164 | } |
| 165 | if _, err := os.Stat(VersionPath(opt, "bab", "v1", "console")); err != nil { |
| 166 | t.Fatalf("v1 was not staged: %v", err) |
| 167 | } |
| 168 | if _, err := os.Lstat(LiveLink(opt, heldTestHost)); !os.IsNotExist(err) { |
| 169 | t.Fatalf("live/<host> was written: %v", err) |
| 170 | } |
| 171 | // Again: the stage is idempotent, and still publishes nothing. |
| 172 | if _, err := StageHeldVerified(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t)); err != nil { |
| 173 | t.Fatal(err) |
| 174 | } |
| 175 | if _, err := os.Lstat(LiveLink(opt, heldTestHost)); !os.IsNotExist(err) { |
| 176 | t.Fatalf("live/<host> was written: %v", err) |
| 177 | } |
| 178 | }) |
| 179 | t.Run("proven, live left on the earlier build", func(t *testing.T) { |
| 180 | e := newHeldProofEnv(t) |
| 181 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 182 | e.require(t, "console") |
| 183 | e.prove(t, e.pinnedSHA) |
| 184 | if _, err := StageHeldVerified(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate); err != nil { |
| 185 | t.Fatal(err) |
| 186 | } |
| 187 | if _, err := os.Stat(VersionPath(e.opt, "bab", "v1", "console")); err != nil || e.live() != "v0" { |
| 188 | t.Fatalf("staged %v, live %q; want v1 staged and v0 live", err, e.live()) |
| 189 | } |
| 190 | }) |
| 191 | t.Run("not proven", func(t *testing.T) { |
| 192 | e := newHeldProofEnv(t) |
| 193 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 194 | e.require(t, "console") |
| 195 | if _, err := StageHeldVerified(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate); !errors.Is(err, ErrHeldNotProven) { |
| 196 | t.Fatalf("an unproven release: %v", err) |
| 197 | } |
| 198 | if _, err := os.Stat(VersionPath(e.opt, "bab", "v1", "console")); err == nil || e.live() != "v0" { |
| 199 | t.Fatalf("an unproven release was staged (%v) or live moved (%q)", err, e.live()) |
| 200 | } |
| 201 | }) |
| 202 | t.Run("the staged bytes are not the proven ones", func(t *testing.T) { |
| 203 | e := newHeldProofEnv(t) |
| 204 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 205 | e.require(t, "console") |
| 206 | e.prove(t, e.pinnedSHA) |
| 207 | stale := []byte("not the shipped bytes") |
| 208 | down := e.staleCopyDown(t, stale) |
| 209 | if _, err := StageHeldVerified(e.ctx, down, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate); !errors.Is(err, ErrHeldNotProven) || e.live() != "v0" { |
| 210 | t.Fatalf("other staged bytes: %v, live %q", err, e.live()) |
| 211 | } |
| 212 | r, _ := ReadRefusal(HeldRefusalBase(e.bake), heldTestHost) |
| 213 | if r == nil || r.Kind != RefusalNotProven || r.MoveOp != heldRoute().HoldOp || !strings.Contains(r.Reason, etagOf(stale)) { |
| 214 | t.Fatalf("refusal = %+v, want the move's not-proven record naming the staged hash", r) |
| 215 | } |
| 216 | }) |
| 217 | t.Run("not authorized", func(t *testing.T) { |
| 218 | ctx, s, opt := heldFixture(t) |
| 219 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 220 | shipRelease(t, s, "bab", "console", "v3", adoptPlatform, []byte("shipped, never promoted")) |
| 221 | if _, err := StageHeldVerified(ctx, s, heldPub, nr(t), opt, heldRoute(), "v3", offGate(t)); !errors.Is(err, ErrHeldNotAuthorized) { |
| 222 | t.Fatalf("a never-promoted build: %v", err) |
| 223 | } |
| 224 | if _, err := os.Stat(VersionPath(opt, "bab", "v3", "console")); err == nil { |
| 225 | t.Fatal("an unauthorized build was staged") |
| 226 | } |
| 227 | }) |
| 228 | t.Run("blocked", func(t *testing.T) { |
| 229 | ctx, s, opt := heldFixture(t) |
| 230 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 231 | orig := materializeHeldStage |
| 232 | t.Cleanup(func() { materializeHeldStage = orig }) |
| 233 | materializeHeldStage = func(context.Context, Store, string, string, string, string, string, string) (*Manifest, error) { |
| 234 | return nil, &os.PathError{Op: "mkdir", Path: opt, Err: syscall.EROFS} |
| 235 | } |
| 236 | if _, err := StageHeldVerified(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t)); !errors.Is(err, ErrStageBlocked) { |
| 237 | t.Fatalf("a read-only version store: %v", err) |
| 238 | } |
| 239 | }) |
| 240 | t.Run("refusals", func(t *testing.T) { |
| 241 | ctx, s, opt := heldFixture(t) |
| 242 | if _, err := StageHeldVerified(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", nil); !errors.Is(err, errHeldNoProofGate) { |
| 243 | t.Fatalf("no gate: %v", err) |
| 244 | } |
| 245 | noOp := heldRoute() |
| 246 | noOp.HoldOp = "" |
| 247 | if _, err := StageHeldVerified(ctx, s, heldPub, nr(t), opt, noOp, "v1", offGate(t)); err == nil { |
| 248 | t.Fatal("a row no move holds was staged") |
| 249 | } |
| 250 | if _, err := os.Stat(VersionPath(opt, "bab", "v1", "console")); err == nil { |
| 251 | t.Fatal("a refused stage staged v1") |
| 252 | } |
| 253 | }) |
| 254 | } |
| 255 | |
| 256 | // TestVerifyHeldLiveReadsUnderTheClaim (moves Task 7): the configured step's |
| 257 | // reading of a release it published under the held-release selection is |
| 258 | // VerifyHeldLive's, which writes nothing, so CARLOS_HOST_CLAIM does not |
| 259 | // refuse it. A link on another version is still ErrHeldNotLive. |
| 260 | func TestVerifyHeldLiveReadsUnderTheClaim(t *testing.T) { |
| 261 | ctx, s, opt := heldFixture(t) |
| 262 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 263 | lock := func(read func() error) error { return read() } |
| 264 | if err := VerifyHeldLive(ctx, s, opt, heldRoute(), "v1", offGate(t), lock); !errors.Is(err, ErrHeldNotLive) { |
| 265 | t.Fatalf("nothing published: %v, want ErrHeldNotLive", err) |
| 266 | } |
| 267 | if _, err := MaterializeHeldUnder(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1", offGate(t), &heldClaimWork{}, nil); err != nil { |
| 268 | t.Fatal(err) |
| 269 | } |
| 270 | if err := VerifyHeldLive(ctx, s, opt, heldRoute(), "v1", offGate(t), lock); err != nil { |
| 271 | t.Fatalf("published under the selection: %v", err) |
| 272 | } |
| 273 | } |
| 274 | |