Artifact df79ae4ad627a7ea6b71900d3441fd56d63e6b695a2dbacac2875b1a8264d579:
- File
internal/sync/horizon_pragma_test.go
— part of check-in
[352a0d9e0b]
at
2026-10-22 12:02:50
on branch feat/horizon-cones
— feat(sync): serve the content horizon and send its pragma
A horizon store asked every peer for the whole history and threw away what fell below C on arrival, so a horizon clone cost a dense clone's bandwidth. draft-fossil-horizon-00 §6 lets the request carry the horizon and the origin restrict what it sends.
The client sends `pragma horizon HASH RULE` after its path-cone pragmas on every clone and pull, never horizon-structural, and counts the replies that say `pragma horizon-served content` (HorizonServed on the clone and sync results, internal and public). It still classifies every reply locally, so a horizon-ignorant peer stays correct (§6.3).
The handler reads the pragma into the request's cone and serves it through the existing keep machinery rather than a parallel path: the pull inventory's keep set drops the blobs below the horizon (via ConeReferences.HorizonBelow) and every cluster; the clone walk walks the tree of a held-set check-in only, counts a parent as sent only inside the held set, and sends every sub-manifest node at its row while sending no cluster. A crossing delta is inflated through the same sourceKept test the path cone uses, charged to the round's batch. req-clusters under a horizon answers nothing, and every reply says horizon-served content.
A horizon store refuses, through refuseClone, a dense clone and a horizon whose held set is not inside its own, both computed over the store with horizon.Place. That is the spec's subset test; zit refuses any other C or rule. A path-only store now also refuses a horizon-only request, which keeps every path.
Interop with zit at horizon-rfc bbd1435674 holds both ways: zit clones and pulls at a horizon from the handler, the client from `zit serve`, and each side's horizon store refuses the other's dense clone. (user: [email protected], size: 2695) [annotate] [blame] [check-ins using] [more...]
package sync import ( "bytes" "strings" "testing" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/xfer" ) const testHorizonHash = "0123456789abcdef0123456789abcdef01234567" func testHorizonCone(t *testing.T, depth cone.HorizonDepth, include ...string) *cone.Cone { t.Helper() h, err := cone.NewHorizon(testHorizonHash, cone.RuleLeaves, depth) if err != nil { t.Fatal(err) } c, err := cone.NewWithHorizon(include, nil, h) if err != nil { t.Fatal(err) } return c } func pragmaLines(t *testing.T, cards []xfer.Card) []string { t.Helper() var lines []string for _, card := range cards { var buf bytes.Buffer if err := xfer.EncodeCard(&buf, card); err != nil { t.Fatal(err) } lines = append(lines, strings.TrimSuffix(buf.String(), "\n")) } return lines } // draft-fossil-horizon-00 §6.1: one horizon pragma after the path-cone // pragmas, spelled as zit spells it, and never horizon-structural: this // client asks for the content horizon whatever depth its store records. func TestHorizonPragmaWire(t *testing.T) { for _, tc := range []struct { depth cone.HorizonDepth include []string want []string }{ {cone.DepthContent, nil, []string{"pragma horizon " + testHorizonHash + " leaves"}}, {cone.DepthStructural, nil, []string{"pragma horizon " + testHorizonHash + " leaves"}}, {cone.DepthContent, []string{"src"}, []string{"pragma cone src", "pragma horizon " + testHorizonHash + " leaves"}}, } { c := testHorizonCone(t, tc.depth, tc.include...) cards := conePragmas(c) if got := pragmaLines(t, cards); strings.Join(got, "\n") != strings.Join(tc.want, "\n") { t.Fatalf("pragmas = %q, want %q", got, tc.want) } round, err := requestCone(cards) if err != nil { t.Fatal(err) } h, ok := round.Horizon() if !ok || h.Hash() != testHorizonHash || h.Rule() != cone.RuleLeaves || h.Depth() != cone.DepthContent { t.Fatalf("request horizon = %v, %v", h, ok) } if strings.Join(round.Include(), ",") != strings.Join(tc.include, ",") { t.Fatalf("request paths = %q, want %q", round.Include(), tc.include) } } } // A malformed horizon pragma is ignored, as zit ignores one: the request is // then served as if it carried none (§6.3 from the server's side). func TestHorizonPragmaMalformedIgnored(t *testing.T) { for _, values := range [][]string{ {testHorizonHash}, {testHorizonHash, "leaves", "extra"}, {"0123", "leaves"}, {testHorizonHash, "ancestors"}, } { c, err := requestCone([]xfer.Card{&xfer.PragmaCard{Name: "horizon", Values: values}}) if err != nil || c != nil { t.Fatalf("horizon %q: cone=%v err=%v, want ignored", values, c, err) } } }