Artifact 3b775084dd3d84b71ca66298076e070f5689065974051510ab1fdc5801c4e936:
- File
conformance/horizon_peer_zit_test.go
— part of check-in
[8802c5704f]
at
2026-10-29 02:06:06
on branch feat/horizon-peer-serving
— fix(sync): a horizon store refuses a pull it does not hold and mints no seed
A server over a horizon clone answered a horizon pull or sync at an earlier C from its own partial graph. It now refuses any horizon pull whose held set is not a subset of its own with one error card and no content, rolled back whole, and a go-libfossil client fails the round on that reply (draft-fossil-horizon-00 §6.4). partial-mirror serves it, as it does an uncovered clone. The clone coverage check now judges the horizon the request asked for, not the depth serving cut it to.
A store at a structural horizon no longer mints a seed from its own seed: it answers a structural request at the content level (§6.2.3) and names its horizon-history-url. The requester falls back and reports HorizonRemedy, naming C and the dense origin that holds the manifests below it; the CLI prints it.
The concept doc states when a horizon store may serve a horizon peer. Conformance covers §6.4 on the pull path, a structural store answering as content, and the fallback a structural clone takes from such a peer; zit pulls from a go-libfossil horizon store in both outcomes.
Closes #490 (user: [email protected], size: 3943) [annotate] [blame] [check-ins using]
package conformance import ( "context" "net/http/httptest" "os" "path/filepath" "testing" "github.com/danmestas/go-libfossil/internal/cone" "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/internal/xfer" ) // TestZitHorizonPeer has zit pull from a go-libfossil horizon store // (draft-fossil-horizon-00 §6.4): a pull at the store's own C is served at // the horizon, and a pull at an earlier C, whose held set the store does not // hold, is refused with the one error card, on which zit fails. func TestZitHorizonPeer(t *testing.T) { bin := zitHorizonBinary(t) dir := t.TempDir() home := filepath.Join(dir, "home") if err := os.Mkdir(home, 0o755); err != nil { t.Fatal(err) } g := newHZSyncGraph(t) _, store, _, err := hzServeClone(t, g, g.c, sync.HandleOpts{}) if err != nil { t.Fatal(err) } hzPeerOpen(t, store) recorder := &hzRecorder{r: store} server := httptest.NewServer(recorder) defer server.Close() origin := httptest.NewServer(sync.XferHandler(g.f.r, sync.HandleSync)) defer origin.Close() c := hzUUID(t, g.f.r, g.c3) same := filepath.Join(dir, "zsame") zitRun(t, bin, home, dir, "clone", "--no-open", "--horizon="+c, origin.URL, same) zitRun(t, bin, home, same, "pull", server.URL) if err := hzZitServed(recorder.take(), c, g.below); err != nil { t.Fatalf("zit's pull at the store's own C: %v", err) } earlier := filepath.Join(dir, "zearlier") zitRun(t, bin, home, dir, "clone", "--no-open", "--horizon="+hzUUID(t, g.f.r, g.c2), origin.URL, earlier) out, err := zitCommand(bin, home, earlier, "pull", server.URL) if err == nil { t.Fatalf("zit's pull at an earlier C succeeded, want the refusal as fatal\n%s", out) } exchanges := recorder.take() if len(exchanges) != 1 || len(hzPragmas(exchanges[0].request, "horizon")) != 1 { t.Fatalf("zit's refused pull made %d exchanges, want one horizon request", len(exchanges)) } if reply := exchanges[0].reply; scSyncRefused(reply, hzPeerRefusal) != nil || len(reply.Cards) != 1 { t.Fatalf("zit's refused pull was answered with %d cards: %v", len(reply.Cards), scSyncRefused(reply, hzPeerRefusal)) } } // TestZitHorizonPeerServes has go-libfossil pull at an earlier C from zit's // horizon store. zit is the server: when it refuses with one error card and // no content (§6.4), go-libfossil's pull must fail and store nothing; a zit // that serves the pull instead does not implement the refusal, and the case // is skipped. func TestZitHorizonPeerServes(t *testing.T) { bin := zitHorizonBinary(t) dir := t.TempDir() home := filepath.Join(dir, "home") if err := os.Mkdir(home, 0o755); err != nil { t.Fatal(err) } g := newHZSyncGraph(t) origin := httptest.NewServer(sync.XferHandler(g.f.r, sync.HandleSync)) defer origin.Close() zstore := filepath.Join(dir, "zstore") zitRun(t, bin, home, dir, "clone", "--no-open", "--horizon="+hzUUID(t, g.f.r, g.c3), origin.URL, zstore) peer := &hzRecordingTransport{inner: &sync.HTTPTransport{URL: zitServe(t, bin, home, zstore)}} earlier := hzStoreCone(t, hzUUID(t, g.f.r, g.c2), cone.RuleDescendants, cone.DepthContent) _, r, _, err := hzServeClone(t, g, earlier, sync.HandleOpts{}) if err != nil { t.Fatal(err) } before := hzBlobCount(t, r) _, err = sync.Sync(context.Background(), r, peer, sync.SyncOpts{Pull: true}) var refusal string for _, reply := range peer.replies { for _, card := range reply.Cards { if c, ok := card.(*xfer.ErrorCard); ok { refusal = c.Message } } } if refusal == "" { t.Skipf("zit (%s) serves a horizon pull at an earlier C than its own (§6.4 refusal not implemented)", zitVersion(bin)) } t.Logf("zit refused with %q; go-libfossil's pull: %v", refusal, err) if err == nil { t.Fatalf("go-libfossil's pull took zit's refusal %q as non-fatal", refusal) } if after := hzBlobCount(t, r); after != before { t.Fatalf("the refused pull went from %d blob rows to %d", before, after) } }