Artifact 24345db55e0d276de47002107951367546619a6c9e4a6729354ebbdb3a49e37b:
- File
conformance/horizon_deepen_test.go
— part of check-in
[78483726d9]
at
2026-10-23 01:41:07
on branch feat/horizon-cones
— feat(sync): ask the list-form merge base whenever a merge crosses
The re-pinned draft's 6.2.4 query is a primary and one or two secondaries, `pragma horizon-mergebase <primary> <secondary>...`, and the answer repeats the query in order and appends Fossil's pivot over the set. A two-hash query was answered as the base of a pair, so a merge into a checkout with a merge already pending could be told a base Fossil's merge would not choose. The origin now takes the list (sync.MergeBaseQuery), answers nothing when it lacks any named check-in or is asked more secondaries than a six-token card can repeat, and the client takes only an answer that repeats its query.
5.5 now has the clone ask whenever either side's ancestry crosses the horizon, not only when its own walk does: over part of the graph the common ancestor found need not be the nearest. MergeContext, given a peer, checks the ancestry of the merged-in version, the checkout and its pending plain merges (content.AncestryCrosses), asks the pivot of the merged-in version over the checkout and those merges, and merges over the answer, falling back to Merge when the origin does not answer, as zit's merge does. With two merges already pending the query cannot carry them, and asking without them would name another base, so the merge is refused before anything changes.
The deepen point's query names the target over C, as zit asks it. (user: [email protected], size: 24927) [annotate] [blame] [check-ins using]
package conformance import ( "context" "errors" "net/http/httptest" "path/filepath" "slices" "strings" "testing" "time" gofossil "github.com/danmestas/go-libfossil" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/bisect" "github.com/danmestas/go-libfossil/internal/branch" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/horizon" "github.com/danmestas/go-libfossil/internal/horizon/seed" "github.com/danmestas/go-libfossil/internal/merge" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/internal/tag" "github.com/danmestas/go-libfossil/internal/xfer" ) // The merge-base query and deepen (draft-fossil-horizon-00 Appendix A step // 6: §5.5, §5.7, §5.8, §5.9, §6.2.4) over hzStruct: go-libfossil's handler // is the origin, its structural clone the client. func horizonDeepenCases() []Case { return []Case{horizonMergeBaseQuery, horizonMergeBaseBound, horizonDeepen, horizonDeepenPoint, horizonDeepenFailed, horizonMergeCrossing, horizonMergeDeepen, horizonMergeMustAsk, horizonMergeListForm, horizonPushCheck, horizonSeedVerify, horizonBisectCrossing} } var ( horizonMergeBaseQuery = Case{ID: "horizon.6.2.4.merge-base", Req: "HZ-6.2-7"} horizonMergeBaseBound = Case{ID: "horizon.8.merge-base-bound", Req: "HZ-8-1"} horizonDeepen = Case{ID: "horizon.5.8.deepen", Req: "HZ-5.8-1"} horizonDeepenPoint = Case{ID: "horizon.5.8.deepen-point", Req: "HZ-5.8-1"} horizonDeepenFailed = Case{ID: "horizon.5.8.deepen-failed", Req: "HZ-5.8-1"} horizonMergeCrossing = Case{ID: "horizon.5.5.merge-crossing", Req: "HZ-5.5-1"} horizonMergeDeepen = Case{ID: "horizon.5.5.merge-deepen", Req: "HZ-5.5-2"} horizonMergeMustAsk = Case{ID: "horizon.5.5.merge-must-ask", Req: "HZ-5.5-3"} horizonMergeListForm = Case{ID: "horizon.6.2.4.merge-pending", Req: "HZ-6.2-16"} horizonPushCheck = Case{ID: "horizon.5.7.push-check", Req: "HZ-5.7-2"} horizonSeedVerify = Case{ID: "horizon.5.9.verify-seed", Req: "HZ-5.9-1"} horizonBisectCrossing = Case{ID: "horizon.5.5.bisect-crossing", Req: "HZ-5.5-1"} ) func TestHorizonDeepen(t *testing.T) { check(t, horizonMergeBaseQuery, func() error { return hzMergeBaseQuery(t) }) check(t, horizonMergeBaseBound, func() error { return hzMergeBaseBound(t) }) check(t, horizonDeepen, func() error { return hzDeepen(t) }) check(t, horizonDeepenPoint, func() error { return hzDeepenPoint(t) }) check(t, horizonDeepenFailed, func() error { return hzDeepenFailed(t) }) check(t, horizonMergeCrossing, func() error { return hzMergeCrossing(t) }) check(t, horizonMergeDeepen, func() error { return hzMergeDeepen(t) }) check(t, horizonMergeMustAsk, func() error { return hzMergeMustAsk(t) }) check(t, horizonMergeListForm, func() error { return hzMergeListForm(t) }) check(t, horizonPushCheck, func() error { return hzPushCheck(t) }) check(t, horizonSeedVerify, func() error { return hzSeedVerify(t) }) check(t, horizonBisectCrossing, func() error { return hzBisectCrossing(t) }) } // hzMergeBaseQuery: a structural clone asks its origin for the merge base of // two check-ins, one below its horizon, and the origin answers from its own // graph: the base, or - for check-ins sharing no ancestor. func hzMergeBaseQuery(t *testing.T) error { x := newHZStruct(t) r, _, err := x.clone(t, x.peer()) if err != nil { return err } for _, q := range [][3]string{{x.c4, x.feat1, x.c1}, {x.feat2, x.c4, x.c3}} { res, err := sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Pull: true, MergeBaseOf: sync.MergeBaseQuery{Primary: q[0], Secondaries: []string{q[1]}}}) if err != nil || !res.MergeBaseAnswered || res.MergeBase != q[2] { return expectf("base of %.10s and %.10s: %v answered=%v %q, want %.10s", q[0], q[1], err, res.MergeBaseAnswered, res.MergeBase, q[2]) } } root := hzCommit(t, x.s.f, "unrelated", []libfossil.FslID{0}, nil) res, err := sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Pull: true, MergeBaseOf: sync.MergeBaseQuery{Primary: x.c4, Secondaries: []string{hzUUID(t, x.s.f.r, root)}}}) if err != nil || !res.MergeBaseAnswered || res.MergeBase != "" { return expectf("unrelated: %v answered=%v %q, want -", err, res.MergeBaseAnswered, res.MergeBase) } return nil } // hzMergeBaseBound: the walk behind an answer stops at its visited-node // bound, as the origin's other graph walks do (§8). func hzMergeBaseBound(t *testing.T) error { x := newHZStruct(t) if _, err := merge.FindCommonAncestorWithin(context.Background(), x.s.f.r, x.s.c4, x.s.feat1, 2); !errors.Is(err, merge.ErrBound) { return expectf("a walk past its bound: %v, want merge.ErrBound", err) } base, err := merge.FindCommonAncestorWithin(context.Background(), x.s.f.r, x.s.c4, x.s.feat1, 64) if err != nil || base != x.s.c1 { return expectf("within the bound: %d %v, want c1", base, err) } return nil } // hzDeepen: deepening a structural clone from C = c3 to its parent c2 moves // the stored horizon, discards the seed for c3 before the synchronization // (the request offers none), takes c2's manifest and the seed for c2, and // replaces nothing held: K's check-ins derive densely, only c1 and feat1 // stay below. func hzDeepen(t *testing.T) error { x := newHZStruct(t) r, _, err := x.clone(t, x.peer()) if err != nil { return err } before, err := hzHeldNames(r) if err != nil { return err } peer := x.peer() res, err := sync.Deepen(context.Background(), r, peer, x.c2, sync.SyncOpts{}) if err != nil || res.HorizonFallback || res.StructuralServed == 0 { return expectf("deepen to c2: %v %+v", err, res) } if err := hzStoredHorizon(r, x.c2, cone.DepthStructural); err != nil { return err } first := peer.sent()[0] if got := hzPragmas(first, "horizon-seed"); len(got) != 1 || got[0][0] != "-" { return expectf("the deepen's first request offers seed %q, want - (the seed for c3 discarded first)", got) } if got := hzPragmas(first, "horizon"); len(got) != 1 || got[0][0] != x.c2 { return expectf("the deepen's first request names horizon %q, want c2", got) } k, err := horizon.HeldSet(context.Background(), x.s.f.r.DB(), x.s.c2, cone.RuleDescendants) if err != nil { return err } _, want, err := seed.Mint(x.s.f.r.DB(), x.s.c2, k, seedLoginGo) if err != nil { return err } if got := mustHeld(t, r); string(got) != string(want) { return expectf("held seed after the deepen:\n%s\nwant the seed for c2:\n%s", got, want) } after, err := hzHeldNames(r) if err != nil { return err } for name := range before { if !after[name] { return expectf("the deepen dropped held artifact %.10s", name) } } if !after[x.c2] { return expectf("c2's manifest is not held after the deepen") } x.s.k = k if err := x.s.derivesDense(t, r); err != nil { return err } return hzElided(r, map[string]bool{x.c1: true, x.feat1: true}) } // hzDeepenPoint: a deepen toward a check-in that is not an ancestor of C // moves C to their merge base, so K only grows: feat1 lies off C's // ancestry, the clone holds neither its manifest nor its ancestry, so it // asks the origin, and C' is c1. func hzDeepenPoint(t *testing.T) error { x := newHZStruct(t) r, _, err := x.clone(t, x.peer()) if err != nil { return err } peer := x.peer() if _, err := sync.Deepen(context.Background(), r, peer, x.feat1, sync.SyncOpts{}); err != nil { return err } if err := hzStoredHorizon(r, x.c1, cone.DepthStructural); err != nil { return err } if got := hzPragmas(peer.sent()[0], "horizon-mergebase"); len(got) != 1 || !slices.Equal(got[0], []string{x.feat1, x.c3}) { return expectf("the deepen asked %q, want the base of feat1 over c3", got) } return hzElided(r, nil) } // hzDeepenFailed: a deepen whose synchronization fails leaves the store as it // was, the horizon and the seed for C both, as zit's does. func hzDeepenFailed(t *testing.T) error { x := newHZStruct(t) r, _, err := x.clone(t, x.peer()) if err != nil { return err } held := mustHeld(t, r) if _, err := sync.Deepen(context.Background(), r, hzFailingPeer{}, x.c2, sync.SyncOpts{}); err == nil { return expectf("a deepen through a failing peer succeeded") } if err := hzStoredHorizon(r, x.c3, cone.DepthStructural); err != nil { return err } if got := mustHeld(t, r); string(got) != string(held) { return expectf("the failed deepen left seed %q, want the seed for c3 back", got) } return x.s.derivesDense(t, r) } type hzFailingPeer struct{} func (hzFailingPeer) Exchange(context.Context, *xfer.Message) (*xfer.Message, error) { return nil, errors.New("peer unreachable") } // hzStoredHorizon is nil when r's store cone carries a horizon at c, of the // descendants rule, at depth d. func hzStoredHorizon(r *repo.Repo, c string, d cone.HorizonDepth) error { sc, err := content.StoreCone(r.DB()) if err != nil { return err } if sc == nil { return expectf("the store is dense") } h, ok := sc.Horizon() if !ok || h.Hash() != c || h.Rule() != cone.RuleDescendants || h.Depth() != d { return expectf("stored horizon %v %s %v %v, want %.10s descendants %v", ok, h.Hash(), h.Rule(), h.Depth(), c, d) } return nil } // hzHeldNames is every artifact r holds, by name. func hzHeldNames(r *repo.Repo) (map[string]bool, error) { rows, err := r.DB().Query("SELECT uuid FROM blob WHERE size>=0") if err != nil { return nil, err } defer rows.Close() out := map[string]bool{} for rows.Next() { var u string if err := rows.Scan(&u); err != nil { return nil, err } out[u] = true } return out, rows.Err() } // hzMergeFixture is hzStruct's origin served over HTTP, with feat3, a child // of feat1 committed after everything else; w and z, which merge feat3 into // c4 and into feat2; and y, which merges feat1 into c4. Its client is a // structural clone at C = c3 with a checkout at feat2. // // fossil's merge base (pivot.c), the most recent common ancestor, of w and z // is feat3, below the horizon: the store holds w and z but not their base. // That of y and feat2 is c3, which the store holds, but feat1, a common // ancestor too, is below it: the store's walk crosses the horizon on the way. // d1, d2 and d3 are children of c4, committed last, whose base the store's // walk finds at c4 without crossing; their ancestry crosses all the same. type hzMergeFixture struct { x *hzStruct feat3, w, z string y string d1, d2, d3 string origin *hzRecorder url string r *gofossil.Repo co *gofossil.Checkout } func newHZMergeFixture(t *testing.T) *hzMergeFixture { t.Helper() x := newHZStruct(t) f := &hzMergeFixture{x: x, origin: &hzRecorder{r: x.s.f.r}} f.y = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "y", []libfossil.FslID{x.s.c4, x.s.feat1}, nil)) feat3 := hzCommit(t, x.s.f, "feat3", []libfossil.FslID{x.s.feat1}, nil) f.feat3 = hzUUID(t, x.s.f.r, feat3) f.w = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "w", []libfossil.FslID{x.s.c4, feat3}, nil)) f.z = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "z", []libfossil.FslID{x.s.feat2, feat3}, nil)) f.d1 = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "d1", []libfossil.FslID{x.s.c4}, nil)) f.d2 = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "d2", []libfossil.FslID{x.s.c4}, nil)) f.d3 = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "d3", []libfossil.FslID{x.s.c4}, nil)) server := httptest.NewServer(f.origin) t.Cleanup(server.Close) f.url = server.URL hc, err := gofossil.NewHorizonCone(nil, nil, gofossil.Horizon{Checkin: x.c3, Structural: true}) if err != nil { t.Fatal(err) } r, res, err := gofossil.Clone(context.Background(), filepath.Join(t.TempDir(), "structural.fossil"), gofossil.NewHTTPTransport(f.url), gofossil.CloneOpts{StoreCone: hc, PullPhantoms: true}) if err != nil || res.HorizonFallback { t.Fatalf("structural clone: %v %+v", err, res) } t.Cleanup(func() { r.Close() }) f.r = r co, err := r.CreateCheckout(t.TempDir(), gofossil.CheckoutCreateOpts{}) if err != nil { t.Fatal(err) } t.Cleanup(func() { co.Close() }) if err := co.Extract(f.rid(t, x.feat2), gofossil.ExtractOpts{}); err != nil { t.Fatal(err) } f.co = co f.origin.take() return f } func (f *hzMergeFixture) rid(t *testing.T, uuid string) int64 { t.Helper() rid, err := f.r.ResolveVersion(uuid) if err != nil { t.Fatal(err) } return rid } // hzMergeCrossing: a merge whose base lies below the structural horizon is // detected before anything changes, and the diagnostic names the horizon and // the remedy; so is a merge whose base the store holds when its walk to it // crossed the horizon, where an older-looking ancestor below it could be the // base. Neither names a base: the store cannot know it. func hzMergeCrossing(t *testing.T) error { f := newHZMergeFixture(t) for _, m := range [][2]string{{f.z, f.w}, {f.x.feat2, f.y}} { if err := f.co.Extract(f.rid(t, m[0]), gofossil.ExtractOpts{}); err != nil { return err } _, err := f.co.Merge(gofossil.CheckoutMergeOpts{Version: f.rid(t, m[1])}) if !errors.Is(err, gofossil.ErrBelowHorizon) { return expectf("merge of %.10s across the horizon: %v, want ErrBelowHorizon", m[1], err) } for _, want := range []string{"below the structural horizon at " + f.x.c3, "deepen"} { if !strings.Contains(err.Error(), want) { return expectf("diagnostic %q lacks %q", err, want) } } if v, _, err := f.co.Version(); err != nil || v != f.rid(t, m[0]) { return expectf("the refused merge moved the checkout to %d (%v)", v, err) } changed, err := f.co.HasChanges() if err != nil || changed { return expectf("the refused merge left changes %v (%v)", changed, err) } } return nil } // hzMergeDeepen: given a peer, the merge asks the origin for the base, // deepens to a common ancestor of C and the base, and merges over the real // base it then holds. Where the store holds the base the origin names, it // merges over it without deepening. func hzMergeDeepen(t *testing.T) error { f := newHZMergeFixture(t) peer := gofossil.RequiredContentOpts{Peer: gofossil.NewHTTPTransport(f.url)} res, err := f.co.MergeContext(context.Background(), gofossil.CheckoutMergeOpts{Version: f.rid(t, f.y)}, peer) if err != nil { return expectf("merge of y with a peer: %v", err) } if res.Ancestor != f.rid(t, f.x.c3) { return expectf("merged y against rid %d, want c3's", res.Ancestor) } if err := hzAskedMergeBase(f, f.y, f.x.feat2); err != nil { return err } if err := hzStoredHorizon(f.r.Inner(), f.x.c3, cone.DepthStructural); err != nil { return expectf("merging over a held base deepened: %v", err) } f = newHZMergeFixture(t) peer = gofossil.RequiredContentOpts{Peer: gofossil.NewHTTPTransport(f.url)} if err := f.co.Extract(f.rid(t, f.z), gofossil.ExtractOpts{}); err != nil { return err } res, err = f.co.MergeContext(context.Background(), gofossil.CheckoutMergeOpts{Version: f.rid(t, f.w)}, peer) if err != nil { return expectf("merge of w with a peer: %v", err) } if res.Ancestor != f.rid(t, f.feat3) { return expectf("merged w against rid %d, want feat3's", res.Ancestor) } if err := hzAskedMergeBase(f, f.w, f.z); err != nil { return err } sc, err := f.r.StoreCone() if err != nil { return err } if h, ok := sc.Horizon(); !ok || h.Checkin != f.x.c1 || !h.Structural { return expectf("store horizon after the merge %+v %v, want c1 structural", h, ok) } return nil } // hzAskedMergeBase is nil when the origin was asked the base of q first. A // deepen may ask another, for its deepen point. // hzAskedMergeBase checks that the first merge-base query the origin saw // since the last take is primary over secondaries, in that order (§6.2.4). func hzAskedMergeBase(f *hzMergeFixture, primary string, secondaries ...string) error { var asked [][]string for _, e := range f.origin.take() { asked = append(asked, hzPragmas(e.request, "horizon-mergebase")...) } want := append([]string{primary}, secondaries...) if len(asked) == 0 || !slices.Equal(asked[0], want) { return expectf("the merge asked the origin %q, first %q", asked, want) } return nil } // hzMergeMustAsk: §5.5 has the clone ask the origin whenever either side's // ancestry crosses the horizon, even when its own walk finds a common // ancestor. Merging d2 into a checkout of d1, the walk finds c4 without // reaching below C, but c4's ancestry crosses at c3: MergeContext asks the // origin for the pivot of d2 over d1 and merges over its answer, c4, which // the store holds, so nothing deepens. Without a peer there is no one to ask // and Merge merges over its own walk, as zit's merge does when the saved // remote does not answer. func hzMergeMustAsk(t *testing.T) error { f := newHZMergeFixture(t) peer := gofossil.RequiredContentOpts{Peer: gofossil.NewHTTPTransport(f.url)} if err := f.co.Extract(f.rid(t, f.d1), gofossil.ExtractOpts{}); err != nil { return err } res, err := f.co.MergeContext(context.Background(), gofossil.CheckoutMergeOpts{Version: f.rid(t, f.d2)}, peer) if err != nil { return expectf("merge of d2 with a peer: %v", err) } if res.Ancestor != f.rid(t, f.x.c4) { return expectf("merged d2 against rid %d, want c4's", res.Ancestor) } if err := hzAskedMergeBase(f, f.d2, f.d1); err != nil { return err } if err := hzStoredHorizon(f.r.Inner(), f.x.c3, cone.DepthStructural); err != nil { return expectf("merging over a held base deepened: %v", err) } f = newHZMergeFixture(t) if err := f.co.Extract(f.rid(t, f.d1), gofossil.ExtractOpts{}); err != nil { return err } res, err = f.co.Merge(gofossil.CheckoutMergeOpts{Version: f.rid(t, f.d2)}) if err != nil || res.Ancestor != f.rid(t, f.x.c4) { return expectf("merge of d2 without a peer: %v ancestor %d, want c4's", err, res.Ancestor) } return nil } // hzMergeListForm: §6.2.4's secondaries are the checkout and each plain // merge pending in it. With d2 already merged into a checkout of d1, a merge // of d3 asks the pivot of d3 over d1 and d2. A query carries at most two // secondaries, so with d2 and d3 both pending a merge that must ask cannot, // and MergeContext refuses before anything changes, naming why. func hzMergeListForm(t *testing.T) error { f := newHZMergeFixture(t) peer := gofossil.RequiredContentOpts{Peer: gofossil.NewHTTPTransport(f.url)} if err := f.co.Extract(f.rid(t, f.d1), gofossil.ExtractOpts{}); err != nil { return err } if _, err := f.co.Merge(gofossil.CheckoutMergeOpts{Version: f.rid(t, f.d2)}); err != nil { return err } f.origin.take() res, err := f.co.MergeContext(context.Background(), gofossil.CheckoutMergeOpts{Version: f.rid(t, f.d3)}, peer) if err != nil { return expectf("merge of d3 with d2 pending: %v", err) } if res.Ancestor != f.rid(t, f.x.c4) { return expectf("merged d3 against rid %d, want c4's", res.Ancestor) } if err := hzAskedMergeBase(f, f.d3, f.d1, f.d2); err != nil { return err } _, err = f.co.MergeContext(context.Background(), gofossil.CheckoutMergeOpts{Version: f.rid(t, f.y)}, peer) if err == nil || !strings.Contains(err.Error(), "§6.2.4") { return expectf("merge of y with d2 and d3 pending: %v, want a refusal citing §6.2.4", err) } for _, e := range f.origin.take() { if got := hzPragmas(e.request, "horizon-mergebase"); len(got) != 0 { return expectf("the refused merge asked %q", got) } } return nil } // hzPushCheck: the origin moves c4 to branch moved after a structural clone // took its seed. The clone, re-seeded by nothing yet, creates a branch from // c4 cancelling sym-trunk; the origin refuses the push, naming the stale seed // and moved, and stores nothing. After a pull re-seeds the clone, the clone // writes the control artifact cancelling sym-moved on its check-in and // pushes again, and the origin admits both and derives the check-in on the // new branch alone. func hzPushCheck(t *testing.T) error { x := newHZStruct(t) r, _, err := x.clone(t, x.peer()) if err != nil { return err } x.s.tag(t, x.s.c4, "branch", tag.TagPropagating, "moved") x.s.tag(t, x.s.c4, "sym-moved", tag.TagPropagating, "") x.s.tag(t, x.s.c4, "sym-trunk", tag.TagCancel, "") c4, err := hzRID(r, x.c4) if err != nil { return err } when := x.s.f.when.Add(24 * time.Hour) nb, _, err := branch.Create(r, branch.CreateOpts{Name: "nb", Parent: c4, User: "u", Time: when}) if err != nil { return err } nbUUID := hzUUID(t, r, nb) res, err := sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Push: true}) if !hzRefusedStale(err, res, "'moved'") || !hzRefusedStale(err, res, "'trunk'") { return expectf("push of a stale cancellation: %v %+v", err, res) } if names, err := hzHeldNames(x.s.f.r); err != nil || names[nbUUID] { return expectf("the origin holds the refused check-in (%v)", err) } if res, err := sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Pull: true}); err != nil || res.HorizonFallback { return expectf("re-sync: %v %+v", err, res) } if _, err := tag.AddTag(r, tag.TagOpts{TargetRID: nb, TagName: "sym-moved", TagType: tag.TagCancel, User: "u", Time: when.Add(time.Hour)}); err != nil { return err } res, err = sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Push: true}) if err != nil || len(res.Errors) != 0 { return expectf("push with the correction: %v %q", err, res.Errors) } got, err := hzTagState(x.s.f.r.DB(), nbUUID) if err != nil || !slices.Contains(got, "branch/2=nb") || slices.ContainsFunc(got, func(s string) bool { return strings.HasPrefix(s, "sym-moved") || strings.HasPrefix(s, "sym-trunk") }) { return expectf("the origin derives the corrected check-in as %v (%v)", got, err) } return nil } // hzSeedVerify: verification by deepening (§5.9) holds the real tag artifacts // and primary-parent chains below C, derives every entry's tag state from // them and compares it with what the seed asserted. An honest seed verifies; // one whose hue was altered after delivery is reported at its entry. The // store is left a content-horizon store holding no seed. func hzSeedVerify(t *testing.T) error { x := newHZStruct(t) r, _, err := x.clone(t, x.peer()) if err != nil { return err } report, err := sync.VerifySeed(context.Background(), r, x.peer(), sync.SyncOpts{}) if err != nil || report.Checked == 0 || len(report.Mismatches) != 0 { return expectf("an honest seed: %v %+v", err, report) } if err := hzStoredHorizon(r, x.c3, cone.DepthContent); err != nil { return err } if held, err := seed.Held(r.DB()); err != nil || held != nil { return expectf("a verified store still holds a seed (%v)", err) } x = newHZStruct(t) r, _, err = x.clone(t, x.peer()) if err != nil { return err } d, err := deck.Parse(mustHeld(t, r)) if err != nil { return err } for i := range d.T { if d.T[i].Name == "hue" { d.T[i].Value = "green" } } forged, err := d.Marshal() if err != nil { return err } if err := r.WithTx(func(tx *db.Tx) error { return seed.Hold(tx, hash.SHA3(forged), forged, nil) }); err != nil { return err } report, err = sync.VerifySeed(context.Background(), r, x.peer(), sync.SyncOpts{}) if err != nil { return err } if len(report.Mismatches) == 0 { return expectf("an altered seed verified: %+v", report) } for _, m := range report.Mismatches { if m.Tag != "hue" || m.Seeded != "*green" || m.Derived != "*red" { return expectf("mismatch %+v, want hue seeded *green, derived *red", m) } } return nil } // hzBisectCrossing: a bisection whose good end lies below the structural // horizon is refused before it begins, naming the horizon and the deepen; // one within K runs. func hzBisectCrossing(t *testing.T) error { x := newHZStruct(t) r, _, err := x.clone(t, x.peer()) if err != nil { return err } if _, err := r.DB().Exec("CREATE TABLE IF NOT EXISTS vvar(name TEXT PRIMARY KEY, value CLOB) WITHOUT ROWID"); err != nil { return err } rid := func(uuid string) libfossil.FslID { id, err := hzRID(r, uuid) if err != nil { t.Fatal(err) } return id } s := bisect.NewSession(r.DB().SqlDB()) if err := s.MarkGood(rid(x.c2)); err != nil { return err } if err := s.MarkBad(rid(x.c4)); err != nil { return err } _, err = s.Next() if !errors.Is(err, content.ErrBelowHorizon) || !strings.Contains(err.Error(), "below the structural horizon at "+x.c3) || !strings.Contains(err.Error(), "repo cone deepen "+x.c2) { return expectf("bisect from below the horizon: %v, want a diagnostic naming the horizon and the deepen", err) } if err := s.MarkGood(rid(x.c3)); err != nil { return err } if _, err := s.Next(); err != nil && !errors.Is(err, bisect.ErrBisectComplete) { return expectf("bisect within K: %v", err) } return nil }