Artifact 9936e84fbbeb28657774ac47186710ebb8c2458c54932f47255ecd88ba23b417:
- File
conformance/horizon_peer_zit_test.go
— part of check-in
[a557971c28]
at
2026-10-29 03:45:23
on branch feat/horizon-peer-serving
— fix(sync): a refused horizon sync still takes the peer's push
Re-review of #525. Resetting the reply after the card loop dropped the gimmes that answer the client's igot announcements. The real client sends only igots in its first round, so its push never landed.
- Server: a §6.4 refusal drops only what the pull half put in the reply. That is content, announcements and the pragmas serving the horizon. The push half's cards stay, its gimmes and any push error, and the horizon-not-held error goes last, since a client stops reading at an error card. - Client: on a horizon-not-held refusal the pull stops and later requests carry no pull card. Rounds go on answering gimmes until the push converges, bounded by MaxRounds, and Sync then returns the refusal. A structural store does not read the refusal as a reason to fall back. - The refused-push case now drives the real client: an earlier-C peer with a check-in of its own syncs, and the store holds that check-in while the peer is sent no content. A push error survives the refusal. - The requirement table and concept doc say why taking the push is consistent with §6.4's "no content". (user: [email protected], size: 4049) [annotate] [blame] [check-ins using] [more...]
package conformance import ( "context" "net/http/httptest" "os" "path/filepath" "strings" "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 err := hzPeerRefused(exchanges[0].reply); err != nil { t.Fatalf("zit's refused pull: %v", err) } } // 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) carrying the horizon-not-held: prefix, go-libfossil's // pull must fail and store nothing. A zit that serves the pull instead, or // refuses without the prefix, does not implement the refusal clients match, // 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 } } } switch { case refusal == "": t.Skipf("zit (%s) serves a horizon pull at an earlier C than its own (§6.4 refusal not implemented)", zitVersion(bin)) case !strings.HasPrefix(refusal, hzPeerRefusalPrefix): t.Skipf("zit (%s) refuses with %q, which lacks the %s prefix a client matches", zitVersion(bin), refusal, hzPeerRefusalPrefix) } 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) } }