carlos / platform Public

hostclaim: admission bound to op, phase and kind; writer_box; writes_lost; park-generation (spec rev 6.9)

Co-Authored-By: Codex (gpt-6-astra) <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev fdff530f7997579cf86764619e2ef7f4c1f43bb9 parent a67d9f4
4 files changed, +285 −29
  • internal/hostclaim/claim.go +9 −3
  • internal/hostclaim/claim_test.go +62 −0
  • internal/hostclaim/generation.go +45 −14
  • internal/hostclaim/generation_test.go +169 −12

CI passed — run details

diff --git a/internal/hostclaim/claim.go b/internal/hostclaim/claim.go
index 9d867bca1..2df8c344f 100644
--- a/internal/hostclaim/claim.go
+++ b/internal/hostclaim/claim.go
@@ -55,6 +55,7 @@ type Holder struct {
Generation string `json:"generation,omitempty"`
Stamp uint64 `json:"stamp,omitempty"`
Session uint64 `json:"session,omitempty"`
+ WriterBox string `json:"writer_box,omitempty"`
}
type Op struct {
@@ -86,8 +87,9 @@ type InventoryEntry struct {
type Obligation struct {
ID string `json:"id"`
Op string `json:"op"`
- Kind string `json:"kind"` // "park-replica" | "park-local"
+ Kind string `json:"kind"` // "park-replica" | "park-local" | "park-generation"
Region string `json:"region,omitempty"`
+ Generation string `json:"generation,omitempty"`
Box string `json:"box,omitempty"`
Retirement string `json:"retirement"`
Owner string `json:"owner"` // "region:<r>" | "box:<b>"
@@ -212,12 +214,16 @@ func (c *Claim) Validate() error {
}
seen[o.ID] = true
switch o.Kind {
+ case "park-generation":
+ if o.Region == "" || o.Generation == "" || o.Box != "" || o.Owner != "region:"+o.Region {
+ return fmt.Errorf("%w: park-generation %q location/generation/owner", ErrMalformed, o.ID)
+ }
case "park-replica":
- if o.Region == "" || o.Box != "" || o.Owner != "region:"+o.Region {
+ if o.Region == "" || o.Box != "" || o.Generation != "" || o.Owner != "region:"+o.Region {
return fmt.Errorf("%w: park-replica %q location/owner", ErrMalformed, o.ID)
}
case "park-local":
- if o.Box == "" || o.Region != "" || o.Owner != "box:"+o.Box {
+ if o.Box == "" || o.Region != "" || o.Generation != "" || o.Owner != "box:"+o.Box {
return fmt.Errorf("%w: park-local %q location/owner", ErrMalformed, o.ID)
}
default:
diff --git a/internal/hostclaim/claim_test.go b/internal/hostclaim/claim_test.go
index f72cd3faa..c5e7168f5 100644
--- a/internal/hostclaim/claim_test.go
+++ b/internal/hostclaim/claim_test.go
@@ -1,10 +1,15 @@
package hostclaim
import (
+ "context"
"encoding/json"
+ "errors"
+ "reflect"
"strings"
"testing"
"time"
+
+ "github.com/carlosframework/platform/internal/release"
)
const testHost = "shop.bab.oncarlos.com"
@@ -86,3 +91,60 @@ func TestValidate(t *testing.T) {
}
}
}
+
+func TestParkGenerationValidation(t *testing.T) {
+ for _, name := range []string{"valid", "no region", "no generation", "box", "wrong owner", "no owner", "park-replica", "park-local"} {
+ t.Run(name, func(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ before := servingClaim(t, ctx, s)
+ ob := Obligation{ID: "o1", Op: "m1", Kind: "park-generation", Region: "ie", Generation: "dead_gen", Retirement: "r1", Owner: "region:ie"}
+ switch name {
+ case "no region":
+ ob.Region, ob.Owner = "", "region:"
+ case "no generation":
+ ob.Generation = ""
+ case "box":
+ ob.Box = "boxS"
+ case "wrong owner":
+ ob.Owner = "region:uk"
+ case "no owner":
+ ob.Owner = ""
+ case "park-replica":
+ ob.Kind = name
+ case "park-local":
+ ob.Kind, ob.Region, ob.Box, ob.Owner = name, "", "boxS", "box:boxS"
+ }
+ next, err := Update(ctx, s, before, func(c *Claim) error { c.Obligations = []Obligation{ob}; return nil })
+ if name != "valid" {
+ if !errors.Is(err, ErrMalformed) {
+ t.Fatalf("invalid obligation = %v, want ErrMalformed", err)
+ }
+ assertAdmissionStored(t, ctx, s, before)
+ return
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ stored := assertAdmissionStored(t, ctx, s, next)
+ if !reflect.DeepEqual(stored.Claim.Obligations, []Obligation{ob}) {
+ t.Fatalf("stored obligations = %+v", stored.Claim.Obligations)
+ }
+ raw, err := json.Marshal(ob)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(raw), `"generation":"dead_gen"`) {
+ t.Fatalf("generation wire form = %s", raw)
+ }
+ ob.Generation = ""
+ raw, err = json.Marshal(ob)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(string(raw), `"generation"`) {
+ t.Fatalf("empty generation not omitted: %s", raw)
+ }
+ })
+ }
+}
diff --git a/internal/hostclaim/generation.go b/internal/hostclaim/generation.go
index 28c5fe4a3..e61f6ad8e 100644
--- a/internal/hostclaim/generation.go
+++ b/internal/hostclaim/generation.go
@@ -5,6 +5,17 @@ import (
"fmt"
)
+type Admission struct {
+ ObservedOpID, ObservedPhase string // Empty values are compared literally.
+ ObservedSession uint64
+ ObservedGeneration string
+ Box string
+ Mode string
+ NewGeneration string
+ PredecessorCertified bool
+ RemoteMaxTXID uint64
+}
+
type writerPredecessor struct {
Session uint64 `json:"session"`
Box string `json:"box"`
@@ -19,31 +30,48 @@ type writerAdmission struct {
Stamp uint64 `json:"stamp"`
Mode string `json:"mode"`
Predecessor writerPredecessor `json:"predecessor"`
+ WritesLost *writerWritesLost `json:"writes_lost,omitempty"`
+}
+
+type writerWritesLost struct {
+ Box string `json:"box"`
+ Generation string `json:"generation"`
+ Session uint64 `json:"session"`
+ RemoteMaxTXID uint64 `json:"remote_max_txid"`
}
// AdmitWriter is §6.4a's writer admission CAS within the current operation.
// Warm admission keeps the installation; cold admission installs a fresh
-// generation and stamp. The caller checks the evidence required for warm mode.
-func AdmitWriter(observedSession uint64, observedGeneration, box, mode, newGeneration string) func(*Claim) error {
+// generation and stamp. The caller checks the evidence required for warm mode
+// and supplies PredecessorCertified. Op.Journal is map[string]string; its
+// "writers" value holds a JSON array string.
+func AdmitWriter(in Admission) func(*Claim) error {
return func(c *Claim) error {
- if c.Holder == nil || c.Op.Exec == "" || c.Op.Exec != box {
+ if c.Holder == nil || c.Op.Exec == "" || c.Op.Exec != in.Box {
return fmt.Errorf("%w: only the executor may admit a writer", ErrWrongState)
}
+ if c.Op.ID != in.ObservedOpID || c.Op.Phase != in.ObservedPhase {
+ return fmt.Errorf("%w: operation or phase moved since it was observed", ErrWrongState)
+ }
+ switch c.Op.Kind {
+ case KindDeleting, KindParking, KindPurging, KindPlacing:
+ return fmt.Errorf("%w: op kind %q cannot admit a writer", ErrWrongState, c.Op.Kind)
+ }
h := c.Holder
- if h.Session != observedSession || h.Generation != observedGeneration {
+ if h.Session != in.ObservedSession || h.Generation != in.ObservedGeneration {
return fmt.Errorf("%w: writer session or generation moved since it was observed", ErrWrongState)
}
- switch mode {
+ switch in.Mode {
case "warm":
- if newGeneration != "" {
- return fmt.Errorf("%w: a warm admission keeps the generation", ErrWrongState)
+ if !in.PredecessorCertified || in.NewGeneration != "" {
+ return fmt.Errorf("%w: a warm admission requires a certificate and keeps the generation", ErrWrongState)
}
case "cold":
- if newGeneration == "" || newGeneration == h.Generation {
+ if in.NewGeneration == "" || in.NewGeneration == h.Generation {
return fmt.Errorf("%w: a cold recovery needs a new generation", ErrWrongState)
}
default:
- return fmt.Errorf("%w: unknown writer admission mode %q", ErrWrongState, mode)
+ return fmt.Errorf("%w: unknown writer admission mode %q", ErrWrongState, in.Mode)
}
var writers []json.RawMessage
@@ -52,18 +80,20 @@ func AdmitWriter(observedSession uint64, observedGeneration, box, mode, newGener
return fmt.Errorf("%w: writer journal: %v", ErrWrongState, err)
}
}
- predecessor := writerPredecessor{Session: h.Session, Generation: h.Generation, Stamp: h.Stamp}
+ predecessor := writerPredecessor{Session: h.Session, Box: h.WriterBox, Generation: h.Generation, Stamp: h.Stamp}
if len(writers) > 0 {
var last writerPredecessor
if err := json.Unmarshal(writers[len(writers)-1], &last); err != nil {
return fmt.Errorf("%w: previous writer: %v", ErrWrongState, err)
}
- predecessor.Box = last.Box
}
- entry := writerAdmission{Session: c.Gen + 1, Box: box, Generation: h.Generation, Stamp: h.Stamp, Mode: mode, Predecessor: predecessor}
- if mode == "cold" {
- entry.Generation = newGeneration
+ entry := writerAdmission{Session: c.Gen + 1, Box: in.Box, Generation: h.Generation, Stamp: h.Stamp, Mode: in.Mode, Predecessor: predecessor}
+ if in.Mode == "cold" {
+ entry.Generation = in.NewGeneration
entry.Stamp = entry.Session
+ if !in.PredecessorCertified {
+ entry.WritesLost = &writerWritesLost{Box: h.WriterBox, Generation: h.Generation, Session: h.Session, RemoteMaxTXID: in.RemoteMaxTXID}
+ }
}
raw, err := json.Marshal(entry)
if err != nil {
@@ -78,6 +108,7 @@ func AdmitWriter(observedSession uint64, observedGeneration, box, mode, newGener
}
c.Op.Journal["writers"] = string(journal)
h.Session, h.Generation, h.Stamp = entry.Session, entry.Generation, entry.Stamp
+ h.WriterBox = in.Box
return nil
}
}
diff --git a/internal/hostclaim/generation_test.go b/internal/hostclaim/generation_test.go
index 8c46b0fd6..a3d6e1c38 100644
--- a/internal/hostclaim/generation_test.go
+++ b/internal/hostclaim/generation_test.go
@@ -7,6 +7,7 @@ import (
"fmt"
"reflect"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/release"
)
@@ -16,7 +17,7 @@ func TestAdmitWriterWarmColdJournal(t *testing.T) {
s := release.NewDirStore(t.TempDir())
o := servingClaim(t, ctx, s)
stamp := o.Claim.Holder.Stamp
- if o.Claim.Holder.Session != 0 {
+ if o.Claim.Holder.Session != 0 || o.Claim.Holder.WriterBox != "" {
t.Fatalf("legacy session = %d, want zero", o.Claim.Holder.Session)
}
raw, err := json.Marshal(o.Claim.Holder)
@@ -27,15 +28,17 @@ func TestAdmitWriterWarmColdJournal(t *testing.T) {
if err := json.Unmarshal(raw, &fields); err != nil {
t.Fatal(err)
}
- if _, ok := fields["session"]; ok {
- t.Fatal("zero session must be omitted")
+ for _, name := range []string{"session", "writer_box"} {
+ if _, ok := fields[name]; ok {
+ t.Fatalf("zero %s must be omitted", name)
+ }
}
- o, err = Update(ctx, s, o, AdmitWriter(0, "", "adi-knitting", "cold", "gen_a"))
+ o, err = Update(ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a", PredecessorCertified: true}))
if err != nil {
t.Fatal(err)
}
cold := o.Claim.Gen
- if o.Claim.Holder.Generation != "gen_a" || o.Claim.Holder.Stamp != cold || o.Claim.Holder.Session != cold || cold <= stamp {
+ if o.Claim.Holder.WriterBox != "adi-knitting" || o.Claim.Holder.Generation != "gen_a" || o.Claim.Holder.Stamp != cold || o.Claim.Holder.Session != cold || cold <= stamp {
t.Fatalf("cold holder = %+v, gen = %d", o.Claim.Holder, cold)
}
first := fmt.Sprintf(`{"session":%d,"box":"adi-knitting","generation":"gen_a","stamp":%d,"mode":"cold","predecessor":{"session":0,"box":"","generation":"","stamp":%d}}`, cold, cold, stamp)
@@ -47,17 +50,17 @@ func TestAdmitWriterWarmColdJournal(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- o, err = Update(ctx, s, o, AdmitWriter(cold, "gen_a", "boxT", "warm", ""))
+ o, err = Update(ctx, s, o, AdmitWriter(Admission{ObservedSession: cold, ObservedGeneration: "gen_a", Box: "boxT", Mode: "warm", PredecessorCertified: true}))
if err != nil {
t.Fatal(err)
}
warm := o.Claim.Gen
- if o.Claim.Holder.Session != warm || warm <= cold || o.Claim.Holder.Generation != "gen_a" || o.Claim.Holder.Stamp != cold {
+ if o.Claim.Holder.WriterBox != "boxT" || o.Claim.Holder.Session != warm || warm <= cold || o.Claim.Holder.Generation != "gen_a" || o.Claim.Holder.Stamp != cold {
t.Fatalf("warm holder = %+v, gen = %d", o.Claim.Holder, warm)
}
second := fmt.Sprintf(`{"session":%d,"box":"boxT","generation":"gen_a","stamp":%d,"mode":"warm","predecessor":{"session":%d,"box":"adi-knitting","generation":"gen_a","stamp":%d}}`, warm, cold, cold, cold)
assertWriterJournal(t, o.Claim, "["+first+","+second+"]")
- o, err = Update(ctx, s, o, AdmitWriter(warm, "gen_a", "boxT", "cold", "gen_b"))
+ o, err = Update(ctx, s, o, AdmitWriter(Admission{ObservedSession: warm, ObservedGeneration: "gen_a", Box: "boxT", Mode: "cold", NewGeneration: "gen_b", PredecessorCertified: true}))
if err != nil {
t.Fatal(err)
}
@@ -90,7 +93,7 @@ func assertWriterJournal(t *testing.T, c *Claim, want string) {
}
func TestAdmitWriterPreservesOperation(t *testing.T) {
- for _, kind := range []string{KindServing, KindCreating, KindDeleting, KindParking, KindPurging, KindPlacing, KindMoving, KindLanding, KindReverting, KindRestoring, KindPushing} {
+ for _, kind := range []string{KindServing, KindCreating, KindMoving, KindLanding, KindReverting, KindRestoring, KindPushing} {
for _, mode := range []string{"warm", "cold"} {
t.Run(kind+"/"+mode, func(t *testing.T) {
ctx := context.Background()
@@ -110,7 +113,7 @@ func TestAdmitWriterPreservesOperation(t *testing.T) {
if mode == "cold" {
generation = "gen_a"
}
- next, err := Update(ctx, s, o, AdmitWriter(0, "", "boxT", mode, generation))
+ next, err := Update(ctx, s, o, AdmitWriter(Admission{ObservedOpID: "m1", ObservedPhase: "probe", Box: "boxT", Mode: mode, NewGeneration: generation, PredecessorCertified: true}))
if err != nil {
t.Fatal(err)
}
@@ -119,6 +122,7 @@ func TestAdmitWriterPreservesOperation(t *testing.T) {
want.Holder = &holder
want.Gen++
want.Holder.Session = want.Gen
+ want.Holder.WriterBox = "boxT"
if mode == "cold" {
want.Holder.Generation = generation
want.Holder.Stamp = want.Gen
@@ -133,7 +137,7 @@ func TestAdmitWriterPreservesOperation(t *testing.T) {
}
func TestAdmitWriterRefusals(t *testing.T) {
- for _, name := range []string{"no holder", "no executor", "non-executor", "stale session", "stale generation", "warm with generation", "cold without generation", "cold same generation", "unknown mode", "empty mode", "malformed journal"} {
+ for _, name := range []string{"no holder", "no executor", "non-executor", "stale session", "stale generation", "warm with generation", "cold without generation", "cold same generation", "unknown mode", "empty mode", "malformed journal", "warm without certificate", "stale op", "stale phase", "deleting", "parking", "purging", "placing"} {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
s := release.NewDirStore(t.TempDir())
@@ -147,6 +151,8 @@ func TestAdmitWriterRefusals(t *testing.T) {
c.Op = Op{}
case "no executor":
c.Op.Exec = ""
+ case "deleting", "parking", "purging", "placing":
+ c.Op.Kind, c.Op.ID = name, "op1"
case "malformed journal":
c.Op.Journal = map[string]string{"writers": "not-json"}
}
@@ -176,7 +182,16 @@ func TestAdmitWriterRefusals(t *testing.T) {
case "empty mode":
mode = ""
}
- if _, err := Update(ctx, s, o, AdmitWriter(session, generation, box, mode, newGeneration)); !errors.Is(err, ErrWrongState) {
+ in := Admission{ObservedOpID: o.Claim.Op.ID, ObservedPhase: o.Claim.Op.Phase, ObservedSession: session, ObservedGeneration: generation, Box: box, Mode: mode, NewGeneration: newGeneration, PredecessorCertified: true}
+ switch name {
+ case "warm without certificate":
+ in.Mode, in.NewGeneration, in.PredecessorCertified = "warm", "", false
+ case "stale op":
+ in.ObservedOpID = "old"
+ case "stale phase":
+ in.ObservedPhase = "old"
+ }
+ if _, err := Update(ctx, s, o, AdmitWriter(in)); !errors.Is(err, ErrWrongState) {
t.Fatalf("admit writer = %v, want ErrWrongState", err)
}
stored, err := Read(ctx, s, testHost)
@@ -189,3 +204,145 @@ func TestAdmitWriterRefusals(t *testing.T) {
})
}
}
+
+func TestAdmitWriterCompetingAdmissions(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ before := servingClaim(t, ctx, s)
+ in := Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a"}
+ first := AdmitWriter(in)
+ in.NewGeneration = "gen_b"
+ second := AdmitWriter(in)
+ admitted, err := Update(ctx, s, before, first)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Update(ctx, s, before, second); !errors.Is(err, ErrConflict) {
+ t.Fatalf("competing admission = %v, want ErrConflict", err)
+ }
+ current := assertAdmissionStored(t, ctx, s, admitted)
+ if _, err := Update(ctx, s, current, second); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("stale evidence after reread = %v, want ErrWrongState", err)
+ }
+ assertAdmissionStored(t, ctx, s, admitted)
+}
+
+func TestAdmitWriterLostResponse(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ before := servingClaim(t, ctx, s)
+ admit := AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a"})
+ admitted, err := Update(ctx, s, before, admit)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Update(ctx, s, before, admit); !errors.Is(err, ErrConflict) {
+ t.Fatalf("retry after lost response = %v, want ErrConflict", err)
+ }
+ assertAdmissionStored(t, ctx, s, admitted)
+}
+
+func TestAdmitWriterBoundToOperationAndPhase(t *testing.T) {
+ for _, change := range []string{"operation", "phase", "delete"} {
+ t.Run(change, func(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ o := servingClaim(t, ctx, s)
+ stale := AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a"})
+ var err error
+ if change == "delete" {
+ o, err = Update(ctx, s, o, BeginDelete("d1", "console", "etag", time.Now(), nil, nil))
+ } else {
+ o, err = Update(ctx, s, o, func(c *Claim) error {
+ if change == "operation" {
+ c.Op.Kind, c.Op.ID = KindMoving, "m1"
+ } else {
+ c.Op.Phase = "probe"
+ }
+ return nil
+ })
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Update(ctx, s, o, stale); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("admission after %s changed = %v, want ErrWrongState", change, err)
+ }
+ assertAdmissionStored(t, ctx, s, o)
+ if change == "delete" {
+ fresh := AdmitWriter(Admission{ObservedOpID: "d1", Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a"})
+ if _, err := Update(ctx, s, o, fresh); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("deleting admission = %v", err)
+ }
+ assertAdmissionStored(t, ctx, s, o)
+ id := 0
+ o, err = Update(ctx, s, o, ToParking("d1", nil, nil, func() string { id++; return fmt.Sprintf("o%d", id) }))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Update(ctx, s, o, fresh); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("parking admission = %v", err)
+ }
+ assertAdmissionStored(t, ctx, s, o)
+ }
+ })
+ }
+}
+
+func TestAdmitWriterSurvivesOperationReplacement(t *testing.T) {
+ for _, transition := range []string{"delete", "move"} {
+ t.Run(transition, func(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ o := servingClaim(t, ctx, s)
+ o, err := Update(ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a"}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ writer := *o.Claim.Holder
+ if writer.WriterBox != "adi-knitting" || writer.Session != o.Claim.Gen {
+ t.Fatalf("admitted holder = %+v", writer)
+ }
+ if transition == "delete" {
+ o, err = Update(ctx, s, o, BeginDelete("d1", "console", "etag", time.Now(), nil, nil))
+ } else {
+ o, err = Update(ctx, s, o, func(c *Claim) error {
+ c.Op = Op{Kind: KindMoving, ID: "m1", Phase: "probe", Exec: "boxT"}
+ return nil
+ })
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ o = assertAdmissionStored(t, ctx, s, o)
+ if !reflect.DeepEqual(*o.Claim.Holder, writer) {
+ t.Fatalf("%s changed writer: %+v", transition, o.Claim.Holder)
+ }
+ if transition == "delete" {
+ return
+ }
+ o, err = Update(ctx, s, o, AdmitWriter(Admission{ObservedOpID: "m1", ObservedPhase: "probe", ObservedSession: writer.Session, ObservedGeneration: writer.Generation, Box: "boxT", Mode: "cold", NewGeneration: "gen_b", RemoteMaxTXID: 42}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := fmt.Sprintf(`[{"session":%d,"box":"boxT","generation":"gen_b","stamp":%d,"mode":"cold","predecessor":{"session":%d,"box":"adi-knitting","generation":"gen_a","stamp":%d},"writes_lost":{"box":"adi-knitting","generation":"gen_a","session":%d,"remote_max_txid":42}}]`, o.Claim.Gen, o.Claim.Gen, writer.Session, writer.Stamp, writer.Session)
+ assertWriterJournal(t, o.Claim, want)
+ if o.Claim.Holder.WriterBox != "boxT" {
+ t.Fatalf("new writer = %q", o.Claim.Holder.WriterBox)
+ }
+ assertAdmissionStored(t, ctx, s, o)
+ })
+ }
+}
+
+func assertAdmissionStored(t *testing.T, ctx context.Context, s release.Store, want Observed) Observed {
+ t.Helper()
+ got, err := Read(ctx, s, testHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("stored claim or ETag changed: got %+v, want %+v", got, want)
+ }
+ return got
+}