carlos / platform Public

hostclaim: a created incarnation's first admission declares nothing (moves 7.17 note 2)

A predecessor with no writer (writer_box "") acknowledged nothing, so no
writes_lost is recorded; every declaration names a box (claim spec rev
6.25 §6.4a). Inert library code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Squashed from fix/hostclaim-r717 into main.
claude agent, operated by paul@keymail.dev 67547f5a610469d6a869923bc5bf4c2775c6d9e6 parent 1d1a6f1
3 files changed, +44 −3
  • internal/hostclaim/generation.go +4 −1
  • internal/hostclaim/generation_test.go +1 −2
  • internal/hostclaim/rev713_test.go +39 −0

CI passed — run details

diff --git a/internal/hostclaim/generation.go b/internal/hostclaim/generation.go
index 9b4ade0ff..973e71dac 100644
--- a/internal/hostclaim/generation.go
+++ b/internal/hostclaim/generation.go
@@ -108,7 +108,10 @@ func AdmitWriter(in Admission) func(*Claim) error {
if !in.KeepStamp {
h.Stamp = entry.Session
}
- if !in.PredecessorCertified && in.Coverage == "" {
+ // A predecessor with no writer (a created incarnation's first
+ // admission: writer_box "") acknowledged nothing, so there is
+ // nothing to declare; every declaration names a box (§6.4a).
+ if !in.PredecessorCertified && in.Coverage == "" && predecessor.Box != "" {
entry.WritesLost = &WritesLost{Box: predecessor.Box, Generation: predecessor.Generation, Session: predecessor.Session, RemoteMaxTXID: in.RemoteMaxTXID}
}
}
diff --git a/internal/hostclaim/generation_test.go b/internal/hostclaim/generation_test.go
index ae486d4a7..d041868e9 100644
--- a/internal/hostclaim/generation_test.go
+++ b/internal/hostclaim/generation_test.go
@@ -737,8 +737,7 @@ func TestAdmissionRecordJSON(t *testing.T) {
t.Fatal(err)
}
want := []map[string]any{
- {"region": "ie", "predecessor_region": "ie", "session": float64(o.Claim.Holder.Admissions[0].Session), "box": "adi-knitting", "generation": "G1", "mode": "cold", "covered": true,
- "writes_lost": map[string]any{"box": "", "generation": "", "session": float64(0), "remote_max_txid": float64(42)}},
+ {"region": "ie", "predecessor_region": "ie", "session": float64(o.Claim.Holder.Admissions[0].Session), "box": "adi-knitting", "generation": "G1", "mode": "cold", "covered": true},
{"region": "ie", "predecessor_region": "ie", "session": float64(o.Claim.Holder.Session), "box": "adi-knitting", "generation": "G2", "predecessor": "G1", "mode": "cold", "coverage": "sealed", "covered": true},
}
if !reflect.DeepEqual(got, want) {
diff --git a/internal/hostclaim/rev713_test.go b/internal/hostclaim/rev713_test.go
index 49e705fbb..a37ea0130 100644
--- a/internal/hostclaim/rev713_test.go
+++ b/internal/hostclaim/rev713_test.go
@@ -537,3 +537,42 @@ func TestCompleteObligationDoesNotCoverAdmissions(t *testing.T) {
})
}
}
+
+// A created incarnation's first admission declares nothing: its predecessor
+// has no writer, so no write was ever acknowledged (moves 7.17 note 2).
+func TestAdmitWriterFirstAdmissionAfterCreateDeclaresNothing(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ o := servingClaim(t, ctx, s)
+ if o.Claim.Holder.WriterBox != "" || len(o.Claim.Holder.Admissions) != 0 {
+ t.Fatalf("fixture is not a fresh create: %+v", o.Claim.Holder)
+ }
+ o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G1"}))
+ first := o.Claim.Holder.Admissions[0]
+ if first.WritesLost != nil || len(first.AdditionalWritesLost) != 0 {
+ t.Fatalf("first admission declared a loss: %+v", first)
+ }
+ // The next uncertified cold admission does declare its predecessor.
+ o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "G1", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G2"}))
+ second := o.Claim.Holder.Admissions[1]
+ if second.WritesLost == nil || second.WritesLost.Box == "" || second.WritesLost.Generation != "G1" {
+ t.Fatalf("second admission = %+v", second)
+ }
+}
+
+// writes_lost keeps its wire shape when a real predecessor is declared.
+func TestWritesLostJSONShape(t *testing.T) {
+ b, err := json.Marshal(AdmissionRecord{Region: "ie", Session: 4, Box: "boxB", Generation: "G2", Mode: "cold",
+ WritesLost: &WritesLost{Box: "boxA", Generation: "G1", Session: 3, RemoteMaxTXID: 42}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got map[string]any
+ if err := json.Unmarshal(b, &got); err != nil {
+ t.Fatal(err)
+ }
+ want := map[string]any{"box": "boxA", "generation": "G1", "session": float64(3), "remote_max_txid": float64(42)}
+ if !reflect.DeepEqual(got["writes_lost"], want) {
+ t.Fatalf("writes_lost = %v, want %v", got["writes_lost"], want)
+ }
+}