Artifact e99826797f3d0ae0394723ba3596bae0f69e438ba54db0e3035f1803d990534f:
- File
conformance/horizon_deepen_test.go
— part of check-in
[ad60dffec8]
at
2026-10-22 18:13:28
on branch feat/horizon-cones
— feat(bisect): refuse a good end below the structural horizon
Section 5.5 has a bisection whose good end lies below the horizon deepen to it before it begins. The path between the ends ran through a boundary phantom and the session went on as if the history were there; Next now refuses first, naming the horizon and the deepen that brings the good end, matching content.ErrBelowHorizon. The library session holds no peer, so the deepen is left to the caller. (user: [email protected], size: 18405) [annotate] [blame] [check-ins using] [more...]
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, 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"} 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, 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: [2]string{q[0], 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: [2]string{x.c4, 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 || got[0][0] != x.c3 || got[0][1] != x.feat1 { return expectf("the deepen asked %q, want the base of c3 and feat1", 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 with y, a check-in merging feat1 into // c4, served over HTTP, and a structural clone of it at C = c3 with a // checkout at feat2. The merge base of feat2 and y is feat1, below the // horizon: the store holds y and feat2 but not their base. type hzMergeFixture struct { x *hzStruct y 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)) 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, // the base, and the remedy. func hzMergeCrossing(t *testing.T) error { f := newHZMergeFixture(t) _, err := f.co.Merge(gofossil.CheckoutMergeOpts{Version: f.rid(t, f.y)}) if !errors.Is(err, gofossil.ErrBelowHorizon) { return expectf("merge across the horizon: %v, want ErrBelowHorizon", err) } for _, want := range []string{"below the structural horizon at " + f.x.c3, f.x.feat1, "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, f.x.feat2) { 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. func hzMergeDeepen(t *testing.T) error { f := newHZMergeFixture(t) res, err := f.co.MergeContext(context.Background(), gofossil.CheckoutMergeOpts{Version: f.rid(t, f.y)}, gofossil.RequiredContentOpts{Peer: gofossil.NewHTTPTransport(f.url)}) if err != nil { return expectf("merge with a peer: %v", err) } if res.Ancestor != f.rid(t, f.x.feat1) { return expectf("merged against rid %d, want feat1's", res.Ancestor) } asked := 0 for _, e := range f.origin.take() { asked += len(hzPragmas(e.request, "horizon-mergebase")) } if asked == 0 { return expectf("the merge asked the origin no merge-base query") } 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 } // 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 }