Artifact c4a8283c6e511fe2656b25833115f79a3e0d2a912b93657b92bb995073cb1ad7:
- File
conformance/horizon_structural_zit_test.go
— part of check-in
[74ac2ea4c1]
at
2026-10-22 16:28:44
on branch feat/horizon-cones
— fix(content): take the nodes a structural origin withholds outside the cone
Composed with path members, a structural horizon keeps K's trees, but an origin may withhold their nodes outside the cone, as zit's does since 2b635ea5ce. Those nodes classified transit, so a clone from zit stalled waiting on them. A node the path members exclude is now elided under a structural horizon, as a path cone elides it; zit interop under cone src proves it both ways. (user: [email protected], size: 9842) [annotate] [blame] [check-ins using] [more...]
package conformance import ( "context" "fmt" "maps" "net/http/httptest" "os" "path/filepath" "slices" "strings" "testing" "github.com/danmestas/go-libfossil/internal/cone" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/horizon/seed" "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/internal/tag" "github.com/danmestas/go-libfossil/internal/verify" "github.com/danmestas/go-libfossil/internal/xfer" ) // TestZitHorizonStructuralInterop runs the structural horizon between // go-libfossil and zit both ways (draft-fossil-horizon-00 §5, §6.2.2, // §6.2.3, §6.2.5) over hzSeed: zit clones and pulls at C's structural horizon // from go-libfossil's handler, and go-libfossil clones and pulls at it from // `zit serve`. Each origin sends no manifest below the horizon, labels the // boundary and sends the seed; each client stays structural and derives the // branch, leaf and tag state of K exactly as a dense clone of the same origin // does. func TestZitHorizonStructuralInterop(t *testing.T) { bin := zitHorizonBinary(t) x := newHZStruct(t) dir := t.TempDir() home := filepath.Join(dir, "home") if err := os.Mkdir(home, 0o755); err != nil { t.Fatal(err) } origin := &hzRecorder{r: x.s.f.r} server := httptest.NewServer(origin) defer server.Close() k := []string{x.c3, x.c4, x.feat2} // zit clones at the structural horizon from go-libfossil. zs := filepath.Join(dir, "zstructural") if out, err := zitCommand(bin, home, dir, "clone", server.URL, zs, "--no-open", "--once", "--horizon="+x.c3, "--structural"); err != nil || strings.Contains(out, "content horizon instead") { t.Fatalf("zit's structural clone from go-libfossil: %v\n%s", err, out) } if err := hzStructZitServed(origin.take(), x); err != nil { t.Fatalf("zit's structural clone from go-libfossil: %v", err) } _, want, err := x.s.mint(seedLoginGo) if err != nil { t.Fatal(err) } if got, err := os.ReadFile(filepath.Join(zs, ".zit-cache", "horizon-seed")); err != nil || string(got) != string(want) { t.Fatalf("zit holds seed %q (%v), want go-libfossil's:\n%s", got, err, want) } zd := filepath.Join(dir, "zdense") zitRun(t, bin, home, dir, "clone", server.URL, zd, "--no-open", "--once") origin.take() if err := hzZitSameState(t, bin, home, zs, zd, k); err != nil { t.Fatalf("zit's structural clone of go-libfossil: %v", err) } // A check-in above the horizon and a tag below it that moves the state // in force at C: zit's pull brings the check-in and the re-minted seed. c5rid := hzCommit(t, x.s.f, "c5", []libfossil.FslID{x.s.c4}, nil) c5 := hzUUID(t, x.s.f.r, c5rid) x.s.tag(t, x.s.c1, "hue", tag.TagPropagating, "blue") k = append(k, c5) zitRun(t, bin, home, zs, "pull", server.URL) if err := hzStructZitServed(origin.take(), x); err != nil { t.Fatalf("zit's structural pull from go-libfossil: %v", err) } zitRun(t, bin, home, zd, "pull", server.URL) origin.take() if err := hzZitSameState(t, bin, home, zs, zd, k); err != nil { t.Fatalf("zit's structural pull from go-libfossil: %v", err) } // go-libfossil clones and pulls at the structural horizon from zit. zitURL := zitServe(t, bin, home, zd) peer := &hzRecordingTransport{inner: &sync.HTTPTransport{URL: zitURL}} r, cloned, err := sync.Clone(context.Background(), filepath.Join(dir, "structural.fossil"), peer, sync.CloneOpts{StoreCone: x.cone(t), PullPhantoms: true}) if err != nil { t.Fatalf("go-libfossil's structural clone from zit: %v", err) } defer r.Close() if cloned.HorizonFallback || cloned.StructuralServed == 0 { t.Fatalf("clone from zit fell back %v, structural in %d of %d rounds", cloned.HorizonFallback, cloned.StructuralServed, cloned.Rounds) } if err := hzCrossed(peer.replies, x.below()); err != nil { t.Fatalf("zit's structural clone reply: %v", err) } if held, _ := seed.Held(r.DB()); held == nil { t.Fatal("go-libfossil holds no seed from zit") } if err := x.s.derivesDense(t, r); err != nil { t.Fatalf("go-libfossil's structural clone of zit: %v", err) } if err := hzElided(r, x.below()); err != nil { t.Fatalf("go-libfossil's structural clone of zit: %v", err) } c6 := hzCommit(t, x.s.f, "c6", []libfossil.FslID{x.s.c3}, hzBranch("side")) x.s.tag(t, x.s.feat1, "closed", tag.TagCancel, "") 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 { t.Fatalf("go-libfossil's structural pull from zit: %v", err) } if pulled.HorizonFallback || pulled.StructuralServed == 0 || len(pulled.Errors) != 0 { t.Fatalf("pull from zit: fell back %v, structural %d of %d rounds, errors %q", pulled.HorizonFallback, pulled.StructuralServed, pulled.Rounds, pulled.Errors) } if err := hzCrossed(peer.replies, x.below()); err != nil { t.Fatalf("zit's structural pull reply: %v", err) } x.s.k = append(x.s.k, c5rid, c6) if err := x.s.derivesDense(t, r); err != nil { t.Fatalf("go-libfossil's structural pull from zit: %v", err) } } // seedLoginGo is the U card go-libfossil's origin mints seeds with. const seedLoginGo = "origin" // hzStructZitServed checks zit's structural requests and go-libfossil's // replies: every horizon request carries horizon-structural and a seed offer, // every reply to one says horizon-served structural, and nothing below the // horizon is sent or announced. func hzStructZitServed(exchanges []hzExchange, x *hzStruct) error { scoped := 0 var replies []*xfer.Message for i, e := range exchanges { replies = append(replies, e.reply) if len(hzPragmas(e.request, "horizon")) == 0 { continue } scoped++ if len(hzPragmas(e.request, "horizon-structural")) != 1 || len(hzPragmas(e.request, "horizon-seed")) != 1 { return expectf("request %d: horizon-structural %q, horizon-seed %q", i, hzPragmas(e.request, "horizon-structural"), hzPragmas(e.request, "horizon-seed")) } if got := hzPragmas(e.reply, "horizon-served"); len(got) != 1 || strings.Join(got[0], " ") != "structural" { return expectf("reply %d advertises %q", i, got) } } if scoped == 0 { return expectf("no request of %d carried the horizon pragma", len(exchanges)) } return hzCrossed(replies, x.below()) } // hzZitSameState compares what two zit repositories derive: each check-in // of k described, the branches and the leaves. func hzZitSameState(t *testing.T, bin, home, a, b string, k []string) error { t.Helper() state := func(dir string) string { var out strings.Builder for _, u := range k { out.WriteString(zitRun(t, bin, home, dir, "info", u)) } out.WriteString(zitRun(t, bin, home, dir, "branch")) out.WriteString(zitRun(t, bin, home, dir, "leaves")) return out.String() } if got, want := state(a), state(b); got != want { return expectf("derived state differs from dense:\nstructural:\n%s\ndense:\n%s", got, want) } return nil } // TestZitHorizonStructuralPaths composes the structural horizon with a path // cone over sub-manifest trees, both ways: an origin keeps K's trees but may // withhold the nodes outside the cone (zit since 2b635ea5ce does), so the // client must take those as elided rather than wait on them. go-libfossil // clones from zit serve, and zit from go-libfossil, at C = c3 under cone // src, and each derives K's tag state as the dense origin does. func TestZitHorizonStructuralPaths(t *testing.T) { bin := zitHorizonBinary(t) f := newSCRepo(t, true) if _, err := f.r.DB().Exec("UPDATE user SET cap='gio' WHERE login='nobody'"); err != nil { t.Fatal(err) } files := maps.Clone(scFiles) checkins := []libfossil.FslID{f.c1} for i := 2; i <= 4; i++ { files["src/a.c"] = fmt.Sprintf("a %d\n", i) files["tools/t.c"] = fmt.Sprintf("tools %d\n", i) checkins = append(checkins, f.commit(t, files, checkins[len(checkins)-1])) } c3 := hzUUID(t, f.r, checkins[2]) k := []libfossil.FslID{checkins[2], checkins[3]} dir := t.TempDir() home := filepath.Join(dir, "home") if err := os.Mkdir(home, 0o755); err != nil { t.Fatal(err) } server := httptest.NewServer(xferHandler(t, f.r, func(*xfer.Message) {})) defer server.Close() zs := filepath.Join(dir, "zstructural") if out, err := zitCommand(bin, home, dir, "clone", server.URL, zs, "--no-open", "--once", "--cone=src", "--horizon="+c3, "--structural"); err != nil || strings.Contains(out, "content horizon instead") { t.Fatalf("zit's structural clone under cone src from go-libfossil: %v\n%s", err, out) } zitRun(t, bin, home, zs, "verify") zd := filepath.Join(dir, "zdense") zitRun(t, bin, home, dir, "clone", server.URL, zd, "--no-open", "--once") h, err := cone.NewHorizon(c3, cone.RuleDescendants, cone.DepthStructural) if err != nil { t.Fatal(err) } sc, err := cone.NewWithHorizon([]string{"src"}, nil, h) if err != nil { t.Fatal(err) } r, cloned, err := sync.Clone(context.Background(), filepath.Join(dir, "structural.fossil"), &sync.HTTPTransport{URL: zitServe(t, bin, home, zd)}, sync.CloneOpts{StoreCone: sc, PullPhantoms: true}) if err != nil { t.Fatalf("go-libfossil's structural clone under cone src from zit: %v", err) } defer r.Close() if cloned.HorizonFallback || cloned.StructuralServed == 0 { t.Fatalf("clone fell back %v, structural in %d of %d rounds", cloned.HorizonFallback, cloned.StructuralServed, cloned.Rounds) } report, err := verify.Verify(r) if err != nil || !report.OK() || report.Phantoms != 0 { t.Fatalf("verify: %v ok=%v transit=%d issues=%v", err, report.OK(), report.Phantoms, report.Issues) } for _, rid := range k { u := hzUUID(t, f.r, rid) want, _ := hzTagState(f.r.DB(), u) got, err := hzTagState(r.DB(), u) if err != nil || !slices.Equal(got, want) { t.Fatalf("check-in %.10s carries %v, dense %v (%v)", u, got, want, err) } } }