host: the real writer's publication under every claim state folds through the real edge fold as the mixed-history statement says
lifetimes on and off crossed with region HA off and a positive, absent, failed and undecided claim read: the pair appears only with lifetimes, the stamp only with a positive claim, and every retained publication folds against every fresh one, in both file orders, through edge.FoldedHome. The unstamped claims-on publication against a retained claims-off pair is spelled out on its own.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+189
−0
internal/host/edgesfile_test.go+189 −0
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go| index bb2154778..f094a810f 100644 |
| --- a/internal/host/edgesfile_test.go |
| +++ b/internal/host/edgesfile_test.go |
| @@ -20,6 +20,7 @@ import ( |
| "time" |
| "github.com/carlosframework/platform/internal/appconfig" |
| + "github.com/carlosframework/platform/internal/edge" |
| "github.com/carlosframework/platform/internal/edgesfile" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/moves" |
| @@ -1751,6 +1752,194 @@ func TestLifetimesOffWriterPublishesNoPair(t *testing.T) { |
| } |
| } |
| +// claimRead is how one pass's claim read for the host went. |
| +type claimRead int |
| + |
| +const ( |
| + readHAOff claimRead = iota // region HA off: no claim reader at all |
| + readPositive // the claim names this box: stamped |
| + readAbsent // no claim object |
| + readFailed // the read errored, and never succeeded before |
| + readUndecided // the pass's deadline came before the read |
| +) |
| + |
| +func (r claimRead) String() string { |
| + return [...]string{"ha-off", "positive", "absent", "failed", "undecided"}[r] |
| +} |
| + |
| +// claimsFor builds the region HA claim reader for one claim state, with |
| +// box as this box and stamp as its claim's holder stamp. |
| +func claimsFor(r claimRead, box, host string, stamp uint64) (*edgesClaims, *claimsBook) { |
| + book := &claimsBook{claims: map[string]*hostclaim.Claim{}} |
| + switch r { |
| + case readHAOff: |
| + return nil, book |
| + case readPositive, readUndecided: |
| + book.claims[host] = serving(box, stamp) |
| + case readFailed: |
| + book.errs = map[string]bool{host: true} |
| + } |
| + c := newEdgesClaims(box, book.read) |
| + c.logf = func(string, ...any) {} // a failed read logs; not this test's business |
| + if r == readUndecided { |
| + c.passDeadline = 0 // expired before the first read: no decision |
| + } |
| + return c, book |
| +} |
| + |
| +// publication is one writer configuration and the stamp and pair it must |
| +// publish for the host. |
| +type publication struct { |
| + lifetimes bool |
| + read claimRead |
| + row registry.Pair |
| + // claimStamp is the holder stamp of the claim this box would read |
| + // (positive and undecided reads); stamp is what it must publish. |
| + claimStamp uint64 |
| + stamp int64 |
| + pair registry.Pair |
| +} |
| + |
| +func (p publication) String() string { |
| + return fmt.Sprintf("lifetimes=%v read=%s row=(%d, %d) claim stamp %d", p.lifetimes, p.read, p.row.Incarnation, p.row.ServingSeq, p.claimStamp) |
| +} |
| + |
| +func pairLess(a, b registry.Pair) bool { |
| + if a.Incarnation != b.Incarnation { |
| + return a.Incarnation < b.Incarnation |
| + } |
| + return a.ServingSeq < b.ServingSeq |
| +} |
| + |
| +// mixedHistory is the brief's mixed-history statement: whether the |
| +// earlier file's publication keeps the dial, and the clause that says so. |
| +func mixedHistory(first, second publication) (bool, string) { |
| + switch { |
| + case first.stamp > 0 && second.stamp > 0: |
| + return first.stamp >= second.stamp, "two stamped publications fold as today" |
| + case first.stamp > 0 || second.stamp > 0: |
| + return first.stamp > 0, "a stamped publication beats any unstamped entry whatever its pair" |
| + case first.pair == second.pair: |
| + return true, "equal pairs (two unstamped claims-on (0, 0) among them) fold as today" |
| + case first.pair == (registry.Pair{}) || second.pair == (registry.Pair{}): |
| + return second.pair == (registry.Pair{}), "an unstamped (0, 0) loses to a retained publication carrying a pair" |
| + default: |
| + return pairLess(second.pair, first.pair), "the higher pair wins (the design's moves)" |
| + } |
| +} |
| + |
| +func TestPublicationUnderEveryClaimState(t *testing.T) { |
| + const host = "shop.example.com" |
| + reads := []claimRead{readHAOff, readPositive, readAbsent, readFailed, readUndecided} |
| + regs := map[registry.Pair]*registry.Registry{} |
| + regFor := func(p registry.Pair) *registry.Registry { |
| + if regs[p] == nil { |
| + regs[p] = edgesTestRegistry(t) |
| + lifetimeRow(t, regs[p], host, p.Incarnation, p.ServingSeq) |
| + } |
| + return regs[p] |
| + } |
| + // publish runs the real writer as box under p and checks what it |
| + // published against p's stamp and pair. |
| + publish := func(p publication, box string) []byte { |
| + t.Helper() |
| + claims, book := claimsFor(p.read, box, host, p.claimStamp) |
| + body, hosts := publishLifetimes(t, regFor(p.row), box, claims, p.lifetimes) |
| + h, ok := hosts[host] |
| + if !ok { |
| + t.Fatalf("%s as %s: host not published", p, box) |
| + } |
| + if h.Stamp != p.stamp || h.Incarnation != p.pair.Incarnation || h.ServingSeq != p.pair.ServingSeq { |
| + t.Errorf("%s as %s: published stamp %d pair (%d, %d), want stamp %d pair (%d, %d)", |
| + p, box, h.Stamp, h.Incarnation, h.ServingSeq, p.stamp, p.pair.Incarnation, p.pair.ServingSeq) |
| + } |
| + if p.read == readUndecided && len(book.reads) != 0 { |
| + t.Errorf("%s: an undecided pass read the claim", p) |
| + } |
| + return body |
| + } |
| + states := func(row registry.Pair, stamp int64) []publication { |
| + var out []publication |
| + for _, lifetimes := range []bool{false, true} { |
| + for _, r := range reads { |
| + p := publication{lifetimes: lifetimes, read: r, row: row, claimStamp: uint64(stamp)} |
| + if r == readPositive { |
| + p.stamp = stamp // the stamp only with a positive claim |
| + } |
| + if lifetimes { |
| + p.pair = row // the pair only with lifetimes |
| + } |
| + out = append(out, p) |
| + } |
| + } |
| + return out |
| + } |
| + retained := states(registry.Pair{Incarnation: 1, ServingSeq: 3}, 41) |
| + var fresh []publication |
| + for _, seq := range []uint64{2, 3, 4} { |
| + fresh = append(fresh, states(registry.Pair{Incarnation: 1, ServingSeq: seq}, 57)...) |
| + } |
| + bodies := map[string][]byte{} // publication and box -> body |
| + bodyOf := func(p publication, box string) []byte { |
| + k := p.String() + " " + box |
| + if bodies[k] == nil { |
| + bodies[k] = publish(p, box) |
| + } |
| + return bodies[k] |
| + } |
| + clauses := map[string]int{} |
| + for _, r := range retained { |
| + for _, f := range fresh { |
| + for _, order := range [][2]publication{{r, f}, {f, r}} { |
| + files := map[string][]byte{ |
| + edgesFileKey("annie"): bodyOf(order[0], "annie"), |
| + edgesFileKey("betty"): bodyOf(order[1], "betty"), |
| + } |
| + box, ok, err := edge.FoldedHome(files, "carol", host) |
| + if err != nil || !ok { |
| + t.Fatalf("fold %s | %s: %q %v %v", order[0], order[1], box, ok, err) |
| + } |
| + firstWins, clause := mixedHistory(order[0], order[1]) |
| + clauses[clause]++ |
| + want := "betty" |
| + if firstWins { |
| + want = "annie" |
| + } |
| + if box != want { |
| + t.Errorf("annie %s | betty %s: fold chose %s, want %s (%s)", order[0], order[1], box, want, clause) |
| + } |
| + } |
| + } |
| + } |
| + // Every clause of the statement was exercised, in both orders. |
| + for _, clause := range []string{ |
| + "two stamped publications fold as today", |
| + "a stamped publication beats any unstamped entry whatever its pair", |
| + "equal pairs (two unstamped claims-on (0, 0) among them) fold as today", |
| + "an unstamped (0, 0) loses to a retained publication carrying a pair", |
| + "the higher pair wins (the design's moves)", |
| + } { |
| + if clauses[clause] == 0 { |
| + t.Errorf("no fold exercised %q", clause) |
| + } |
| + } |
| + // The statement's named case, spelled out: an unstamped claims-on |
| + // publication against a retained claims-off one carrying (1, 3) loses |
| + // to it in both orders, whatever its claim read was. |
| + for _, rr := range []claimRead{readHAOff, readAbsent, readFailed, readUndecided} { |
| + for _, fr := range []claimRead{readHAOff, readAbsent, readFailed, readUndecided} { |
| + ret := publication{lifetimes: true, read: rr, row: registry.Pair{Incarnation: 1, ServingSeq: 3}, claimStamp: 41, pair: registry.Pair{Incarnation: 1, ServingSeq: 3}} |
| + on := publication{lifetimes: false, read: fr, row: registry.Pair{Incarnation: 1, ServingSeq: 4}, claimStamp: 57} |
| + for _, boxes := range [][2]string{{"annie", "betty"}, {"betty", "annie"}} { |
| + files := map[string][]byte{edgesFileKey(boxes[0]): bodyOf(ret, boxes[0]), edgesFileKey(boxes[1]): bodyOf(on, boxes[1])} |
| + if box, _, _ := edge.FoldedHome(files, "carol", host); box != boxes[0] { |
| + t.Errorf("retained %s on %s against unstamped claims-on %s: fold chose %s", ret, boxes[0], on, box) |
| + } |
| + } |
| + } |
| + } |
| +} |
| + |
| // The box-side lifetimes switch is the one the box judge is built from |
| // (controller ruling 1): runAgent derives it from boxJudge, once, and hands |
| // it to the edges file loop, which hands it to the writer. |