Artifact 5da8076e4b8941b761a400cd9c400dd190d2e04dc1fe89033afe713a0d1cb565:
- File
conformance/horizon_nodes_zit_test.go
— part of check-in
[0de31d6f8e]
at
2026-10-22 23:43:27
on branch feat/horizon-cones
— feat(sync): withhold out-of-cone nodes of the trees a horizon keeps
Under either horizon with path members an origin now sends only the sub-manifest nodes on paths into the cone of the trees its horizon keeps (every check-in's at the content depth, K's at the structural one), announces none of the rest and answers no gimme for them; a request without path members still receives every node (draft-fossil-horizon-00 §6.2 at zit 3a2df62515).
The walk is zit's walkSlots (src/sparse.zig): only slots that may hold an in-cone path are descended, a last slot takes its parent's bound, and a node is walked again only under a wider bound. It runs over the classification's facts, never the store, and the origin's memo keeps it by cone so a later request walks only the roots appended since, falling back to a fresh walk when a root arrives below the ones walked. A clone round sends nodes by the per-check-in tree walk it already uses without a horizon, walking a check-in outside the held set for its nodes alone.
Each kept node records the lowest root that keeps it, so a cursor is void (§6.2.6) exactly when some node at or below it is kept only by a root above it: stateless, and the same for a fresh classification and the memo, where zit voids every cursor below the store count at the moment its sidecar sees the change.
The memo returned a cone's earlier classification whenever no fact list grew, keeping that store's names, so an artifact appended or filled without becoming a record read as unnamed; it now carries the current facts. Found by the memo equivalence test, which now covers path cones under both horizons, the slot walk, cursor voiding and a tree form that widens a node's bound. (user: [email protected], size: 5562) [annotate] [blame] [check-ins using] [more...]
package conformance import ( "context" "fmt" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/danmestas/go-libfossil/internal/cone" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/internal/xfer" ) // TestZitHorizonNodesInterop runs a path cone's node elision under either // horizon (draft-fossil-horizon-00 at zit 3a2df62515, §4.1, §5.1, §6.2) // between go-libfossil and zit both ways: zit clones and pulls under cone // src at a content and at a structural horizon from go-libfossil's handler, // and go-libfossil clones and pulls the same from `zit serve`. Each origin // sends exactly the nodes on paths into the cone of the trees its horizon // keeps and announces none of the rest, and each client verifies clean. func TestZitHorizonNodesInterop(t *testing.T) { bin := zitHorizonBinary(t) for _, depth := range []cone.HorizonDepth{cone.DepthContent, cone.DepthStructural} { t.Run(fmt.Sprint(depth), func(t *testing.T) { hzZitNodes(t, bin, depth) }) } } func hzZitNodes(t *testing.T, bin string, depth cone.HorizonDepth) { o := newHZNodesOrigin(t) sc := o.cone(t, depth) c := hzUUID(t, o.f.r, o.c2) all := hzNodes(t, o.f.r) trees := []libfossil.FslID{o.c1, o.c2, o.c3} if depth == cone.DepthStructural { trees = trees[1:] } want := hzSlotNodes(t, o.f.r, sc, trees...) dir := t.TempDir() home := filepath.Join(dir, "home") if err := os.Mkdir(home, 0o755); err != nil { t.Fatal(err) } origin := &hzRecorder{r: o.f.r} server := httptest.NewServer(origin) defer server.Close() flags := []string{"--no-open", "--once", "--cone=src", "--horizon=" + c} if depth == cone.DepthStructural { flags = append(flags, "--structural") } // zit clones under the cone at the horizon from go-libfossil. zh := filepath.Join(dir, "zhorizon") if out, err := zitCommand(bin, home, dir, append([]string{"clone", server.URL, zh}, flags...)...); err != nil || strings.Contains(out, "content horizon instead") { t.Fatalf("zit's clone from go-libfossil: %v\n%s", err, out) } if err := hzZitNodesServed(origin.take(), all, want); err != nil { t.Fatalf("go-libfossil serving zit's clone: %v", err) } zitRun(t, bin, home, zh, "verify") // zit pulls a check-in whose tree changes inside and outside the cone. zd := filepath.Join(dir, "zdense") zitRun(t, bin, home, dir, "clone", server.URL, zd, "--no-open", "--once") origin.take() files := scSyncTreeFiles() files["src/a"], files["zzz/b"], files["yyy/e/f"] = "inside 4\n", "outside 4\n", "yf 4\n" c4 := o.f.commit(t, files, o.c3) all = hzNodes(t, o.f.r) want = hzUnion(want, hzSlotNodes(t, o.f.r, sc, c4)) zitRun(t, bin, home, zh, "pull", server.URL) if err := hzZitNodesServed(origin.take(), all, want); err != nil { t.Fatalf("go-libfossil serving zit's pull: %v", err) } zitRun(t, bin, home, zh, "verify") // go-libfossil clones and pulls under the cone at the horizon from zit. zitRun(t, bin, home, zd, "pull", server.URL) origin.take() peer := &hzRecordingTransport{inner: &sync.HTTPTransport{URL: zitServe(t, bin, home, zd)}} r, cloned, err := sync.Clone(context.Background(), filepath.Join(dir, "horizon.fossil"), peer, sync.CloneOpts{StoreCone: sc}) if err != nil { t.Fatalf("go-libfossil's clone from zit: %v", err) } defer r.Close() if cloned.HorizonServed == 0 || depth == cone.DepthStructural && cloned.StructuralServed == 0 { t.Fatalf("zit served %d of %d rounds at the horizon, %d structural", cloned.HorizonServed, cloned.Rounds, cloned.StructuralServed) } if err := hzZitNodesSent(peer.replies, all, want); err != nil { t.Fatalf("zit serving go-libfossil's clone: %v", err) } files["src/shared"], files["zzz/deep/c"] = "shared 5\n", "deep 5\n" c5 := o.f.commit(t, files, c4) all = hzNodes(t, o.f.r) want = hzUnion(want, hzSlotNodes(t, o.f.r, sc, c5)) zitRun(t, bin, home, zd, "pull", server.URL) peer.replies = nil pulled, err := sync.Sync(context.Background(), r, peer, sync.SyncOpts{Pull: true}) if err != nil || len(pulled.Errors) != 0 { t.Fatalf("go-libfossil's pull from zit: %v %q", err, pulled.Errors) } if err := hzCrossed(peer.replies, hzMinusSet(all, want)); err != nil { t.Fatalf("zit serving go-libfossil's pull: %v", err) } if err := hzHeld(r, hzSlotNodes(t, o.f.r, sc, c5), true); err != nil { t.Fatal(err) } if err := hzNodesClean(r, hzMinusSet(all, want)); err != nil { t.Fatal(err) } } // hzZitNodesServed checks go-libfossil's replies to zit: every request // carries the path member and the horizon, and the replies send only nodes // of want and announce none outside it. func hzZitNodesServed(exchanges []hzExchange, all, want map[string]bool) error { var replies []*xfer.Message for i, e := range exchanges { if len(hzPragmas(e.request, "horizon")) != 1 || len(hzPragmas(e.request, "cone")) != 1 { return expectf("request %d carries horizon %q and cone %q", i, hzPragmas(e.request, "horizon"), hzPragmas(e.request, "cone")) } replies = append(replies, e.reply) } if len(replies) == 0 { return expectf("no exchange") } return hzZitNodesSent(replies, all, want) } // hzZitNodesSent checks replies send or announce no node outside want. func hzZitNodesSent(replies []*xfer.Message, all, want map[string]bool) error { if err := hzCrossed(replies, hzMinusSet(all, want)); err != nil { return err } if sent := hzIntersect(scSetOf(hzSent(replies)), all); len(sent) == 0 { return expectf("no node was sent") } return nil }