Artifact 3d6c2783008299b5d8ec6587945b260f60ce2543900e1130cf9ebb46f3186fbb:
- File
conformance/horizon_cursor_zit_test.go
— part of check-in
[b61bd3475c]
at
2026-10-22 15:05:34
on branch feat/horizon-cones
— feat(sync): horizon cursor, origin and client
A horizon pull announced the client's whole holding with igot on every request, and the origin answered with its whole kept inventory: one card per artifact, both ways, on every pull. draft-fossil-horizon-00 §6.2.6 replaces both with a position.
The origin values pragma horizon-cursor right after horizon-served in every reply to a horizon pull, as zit places it. Its position is rid order (the clone SEQNO sense), stopped short of the lowest phantom: a filled phantom keeps its old rid, so a cursor covering it would hide the arrival. Receiving a cursor, any reply that pulls announces nothing at or below it. A filled phantom that a check-in from an earlier receipt already named as parent is recorded as the reclassification position; a cursor below it is answered as if absent. A cursor beyond the origin's own position is ignored.
The client keeps, per origin and cone, the last reply's cursor and the rid count the origin holds through, sends the cursor on every request, announces nothing on a pull-only request and only what it appended on one that sends. Sessions name their origin with SyncOpts.Origin; without it nothing is kept and every request announces in full. (user: [email protected], size: 5349) [annotate] [blame] [check-ins using] [more...]
package conformance import ( "context" "net/http/httptest" "os" "path/filepath" "testing" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/internal/xfer" ) // TestZitHorizonCursorInterop runs the horizon cursor between go-libfossil // and zit both ways (draft-fossil-horizon-00 §6.2.6): zit pulls at a horizon // from go-libfossil's handler, records the cursor its reply hands out and // sends it back, announcing nothing, and is announced only what lies above // it; go-libfossil does the same against `zit serve`. func TestZitHorizonCursorInterop(t *testing.T) { bin := zitBinary(t) dir := t.TempDir() home := filepath.Join(dir, "home") if err := os.Mkdir(home, 0o755); err != nil { t.Fatal(err) } g := newHZSyncGraph(t) c := hzUUID(t, g.f.r, g.c3) origin := &hzRecorder{r: g.f.r} server := httptest.NewServer(origin) defer server.Close() // zit's first pull after its horizon clone is handed a cursor. zhorizon := filepath.Join(dir, "zhorizon") zitRun(t, bin, home, dir, "clone", "--no-open", "--horizon="+c, server.URL, zhorizon) zitRun(t, bin, home, zhorizon, "pull") n, err := hzLastCursor(hzReplies(origin.take())) if err != nil { t.Fatalf("go-libfossil's reply to zit's pull: %v", err) } // Its next pull sends the cursor, announces nothing, and is announced // only what the origin appended since. hzCommit(t, g.f, "c5", []libfossil.FslID{g.m}, nil) zitRun(t, bin, home, zhorizon, "pull") exchanges := origin.take() if err := hzZitCursorPull(exchanges, n); err != nil { t.Fatalf("zit's pull under go-libfossil's cursor: %v", err) } for _, e := range exchanges { above, err := hzAbove(g.f.r, hzIGots(e.reply), n) if err != nil { t.Fatal(err) } if len(above) != len(hzIGots(e.reply)) { t.Fatalf("go-libfossil announced %d artifacts at or below cursor %d to zit", len(hzIGots(e.reply))-len(above), n) } } if len(hzAnnounced(hzReplies(exchanges))) == 0 { t.Fatal("go-libfossil announced none of what it appended above zit's cursor") } // go-libfossil clones at the horizon from zit, and its first pull is // handed zit's cursor. zdense := filepath.Join(dir, "zdense") zitRun(t, bin, home, dir, "clone", "--no-open", server.URL, zdense) zitURL := zitServe(t, bin, home, zdense) peer := &hzRecordingTransport{inner: &sync.HTTPTransport{URL: zitURL}} r, _, err := sync.Clone(context.Background(), filepath.Join(dir, "horizon.fossil"), peer, sync.CloneOpts{StoreCone: g.c, PullPhantoms: true}) if err != nil { t.Fatalf("go-libfossil's horizon clone from zit: %v", err) } defer r.Close() opts := sync.SyncOpts{Pull: true, Origin: zitURL} peer.replies = nil if _, err := sync.Sync(context.Background(), r, peer, opts); err != nil { t.Fatalf("go-libfossil's pull from zit: %v", err) } zn, err := hzLastCursor(peer.replies) if err != nil { t.Fatalf("zit's reply to go-libfossil's pull: %v", err) } // Its next pull sends zit's cursor and announces nothing; zit announces // only what it appended since, which go-libfossil then holds. above := hzCommit(t, g.f, "c6", []libfossil.FslID{g.m}, nil) zitRun(t, bin, home, zdense, "pull", server.URL) sent := &hzSentTransport{inner: peer} peer.replies = nil pulled, err := sync.Sync(context.Background(), r, sent, opts) if err != nil { t.Fatalf("go-libfossil's pull from zit under its cursor: %v", err) } if err := hzRequestCursors(sent.requests, zn); err != nil { t.Fatalf("go-libfossil's pull from zit: %v", err) } if got := hzAnnounced(sent.requests); len(got) != 0 { t.Fatalf("go-libfossil's pull-only request under zit's cursor announced %d artifacts", len(got)) } if len(pulled.Errors) != 0 { t.Fatalf("pull from zit: %q", pulled.Errors) } want := hzFiles(t, g.f.r, above) want[hzUUID(t, g.f.r, above)] = true got := hzAnnounced(peer.replies) if len(got) == 0 { t.Fatal("zit announced nothing it appended above its cursor") } for uuid := range got { if !want[uuid] { t.Fatalf("zit announced %s, below its cursor", uuid) } } if err := hzHeld(r, want, true); err != nil { t.Fatal(err) } } // hzReplies is every reply of exchanges. func hzReplies(exchanges []hzExchange) []*xfer.Message { out := make([]*xfer.Message, 0, len(exchanges)) for _, e := range exchanges { out = append(out, e.reply) } return out } // hzZitCursorPull fails unless every request zit sent carries cursor n and // no igot. func hzZitCursorPull(exchanges []hzExchange, n int64) error { if len(exchanges) == 0 { return expectf("zit sent no request") } requests := make([]*xfer.Message, 0, len(exchanges)) for _, e := range exchanges { requests = append(requests, e.request) } if err := hzRequestCursors(requests, n); err != nil { return err } if got := hzAnnounced(requests); len(got) != 0 { return expectf("zit's pull-only requests announced %d artifacts", len(got)) } return nil } // hzSentTransport records the requests sent through it. type hzSentTransport struct { inner sync.Transport requests []*xfer.Message } func (p *hzSentTransport) Exchange(ctx context.Context, req *xfer.Message) (*xfer.Message, error) { wire, err := scSyncWire(req) if err != nil { return nil, err } p.requests = append(p.requests, wire) return p.inner.Exchange(ctx, req) }