Artifact 594dcaceab1d58716c02a4c421289a7292caaa67e9fcbf5dc80eb9fa40d05036:
- File
internal/sync/horizon_seedrefuse_test.go
— part of check-in
[584c55d1fd]
at
2026-10-24 06:59:39
on branch main
— fix(sync): judge pending rows when a received delta fills their basis (#471)
Two review nits.
Liveness. A pushed artifact pending on a basis P stayed withheld for good when a pull supplied P as a delta: - Only MarkPending re-rooted, and only for a pushed delta. - Filling a phantom with a delta runs no cascade, so nothing judged the waiting rows if the delta was readable, or moved them to P's own root if it was not. A later fill of that root never reached them.
The fix is one hook at the choke point every received delta passes, storeDeltaContent: seedcheck.DeltaStored. - If the stored delta is readable, the rows pending on it are judged. - If not, they move to its chain root, so whatever finally resolves the chain judges them. - It replaces MarkPending's push-only re-root.
Error surfacing. JudgeFilled still skips a row whose own bytes cannot be read (a delta that does not apply or fails its checksum, or bytes that are not the artifact named) and judges the rest of the batch. Any other failure is now returned, a broken chain to the filled root included: check-in and tag return it, and the cascade logs it, as it already did for JudgeFilled errors. The separate availability skip went with it.
Tests: - A pushed plain file and control artifact pending on a basis that a pull supplies as a delta, on a held basis or on one that arrives in the next round. Both become readable and stop being withheld, are announced on a push-on, and the control artifact is linked. - A row whose content does not decompress is reported. - A checksum-failing row is skipped like an invalid one. - Each hook branch and each error class fails a test when changed. (user: [email protected], size: 37033) [annotate] [blame] [check-ins using] [more...]
package sync import ( "context" "path/filepath" "slices" "strings" "testing" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/delta" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/seedcheck" "github.com/danmestas/go-libfossil/internal/shun" "github.com/danmestas/go-libfossil/internal/tag" "github.com/danmestas/go-libfossil/internal/verify" "github.com/danmestas/go-libfossil/internal/xfer" ) // wantSeedRefused is the whole reply to a push carrying a seed (horizon // §5.2.3 as amended by the zit 0.7.1 erratum): the error card alone. const wantSeedRefused = "seed refused: a seed is private to the clone that holds it; it is never pushed" // labelled is a control artifact tagging p under user, as a seed minted // with that label is. func (o *pcOrigin) labelled(user, value string) (*xfer.FileCard, []byte) { o.t.Helper() d := &deck.Deck{Type: deck.Control, D: o.when.Add(4 * time.Hour), U: deck.User(user), T: []deck.TagCard{{Type: deck.TagSingleton, Name: "seedtest", UUID: o.p, Value: value}}} data, err := d.Marshal() if err != nil { o.t.Fatal(err) } return &xfer.FileCard{UUID: hash.SHA1(data), Content: data}, data } // deltaCard is target sent as a delta against base, named baseUUID. func deltaCard(t *testing.T, base []byte, baseUUID string, target []byte) *xfer.FileCard { t.Helper() d, err := delta.Create(base, target) if err != nil { t.Fatal(err) } return &xfer.FileCard{UUID: hash.SHA1(target), DeltaSrc: baseUUID, Content: d} } // assertSeedRefused checks that a reply is the refusal alone and that none // of the named artifacts was stored. func assertSeedRefused(t *testing.T, o *pcOrigin, resp *xfer.Message, uuids ...string) { t.Helper() if len(resp.Cards) != 1 { t.Fatalf("reply has %d cards %v, want the refusal alone", len(resp.Cards), resp.Cards) } if e, ok := resp.Cards[0].(*xfer.ErrorCard); !ok || e.Message != wantSeedRefused { t.Fatalf("reply = %#v, want error %q", resp.Cards[0], wantSeedRefused) } for _, u := range uuids { if o.held(u) { t.Fatalf("the refused request stored %s", u) } } } func TestHorizonPushSeedRefusedFullText(t *testing.T) { o := newPCOrigin(t) s, _ := o.labelled(seedLogin, "a") resp := o.push(o.blobCard(), s) assertSeedRefused(t, o, resp, s.UUID, o.fileUID) } func TestHorizonPushSeedRefusedDeltaHeldBase(t *testing.T) { o := newPCOrigin(t) rid, ok := blob.Exists(o.r.DB(), o.p) if !ok { t.Fatal("origin does not hold p") } base, err := content.Expand(o.r.DB(), rid) if err != nil { t.Fatal(err) } _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, o.p, seed) resp := o.push(o.blobCard(), s) assertSeedRefused(t, o, resp, s.UUID, o.fileUID) } func TestHorizonPushSeedRefusedLateBase(t *testing.T) { o := newPCOrigin(t) baseCard, base := o.labelled("u", "b") _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, baseCard.UUID, seed) resp := o.push(s, o.blobCard(), baseCard) assertSeedRefused(t, o, resp, s.UUID, o.fileUID, baseCard.UUID) } // A delta whose base never arrives cannot be identified: it is stored as // any dangling delta is, under the transit-phantom rules. func TestHorizonPushSeedUnexpandableAdmitted(t *testing.T) { o := newPCOrigin(t) _, base := o.labelled("u", "b") _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, hash.SHA1(base), seed) if errs := errorsOf(o.push(o.blobCard(), s)); len(errs) != 0 { t.Fatalf("errors = %q", errs) } if !o.held(o.fileUID) { t.Fatal("the ordinary artifact was not stored") } } // A seed admitted as a dangling delta is judged when a later request makes // it readable (spec F): that request is innocent and stored whole with no // error, while the seed is recorded as refused, never crosslinked, announced // or sent. The label and the shape are both caught. func TestHorizonPushSeedBaseInLaterRequest(t *testing.T) { for _, shape := range []string{"label", "shape"} { o := newPCOrigin(t) baseCard, base := o.labelled("u", "b") var seed []byte if shape == "label" { _, seed = o.labelled(seedLogin, "a") } else { _, seed = o.bound("zit-seed") } s := deltaCard(t, base, baseCard.UUID, seed) if errs := errorsOf(o.push(s)); len(errs) != 0 { t.Fatalf("%s: the dangling delta: errors %q", shape, errs) } if errs := errorsOf(o.push(o.blobCard(), baseCard)); len(errs) != 0 { t.Fatalf("%s: the base's push: errors %q", shape, errs) } for _, u := range []string{baseCard.UUID, o.fileUID} { if phantom, err := isPhantomRow(o.r.DB(), u); err != nil || phantom || !o.held(u) { t.Fatalf("%s: %.10s not stored (%v)", shape, u, err) } } assertRefused(t, o.r, s.UUID, true) var linked int if err := o.r.DB().QueryRow(`SELECT (SELECT count(*) FROM tagxref JOIN blob ON blob.rid=tagxref.srcid WHERE blob.uuid=?1) + (SELECT count(*) FROM event JOIN blob ON blob.rid=event.objid WHERE blob.uuid=?1)`, s.UUID).Scan(&linked); err != nil { t.Fatal(err) } if linked != 0 { t.Fatalf("%s: the seed was crosslinked: %d rows", shape, linked) } if _, err := verify.Rebuild(o.r); err != nil { t.Fatal(err) } if err := o.r.DB().QueryRow(`SELECT count(*) FROM tagxref JOIN blob ON blob.rid=tagxref.srcid WHERE blob.uuid=?`, s.UUID).Scan(&linked); err != nil || linked != 0 { t.Fatalf("%s: rebuild crosslinked the seed: %d rows (%v)", shape, linked, err) } pull := o.push(&xfer.PullCard{ServerCode: "s", ProjectCode: testPC}, &xfer.GimmeCard{UUID: s.UUID}) for _, c := range pull.Cards { if f, ok := c.(*xfer.FileCard); ok && f.UUID == s.UUID { t.Fatalf("%s: the seed was served", shape) } if g, ok := c.(*xfer.IGotCard); ok && g.UUID == s.UUID { t.Fatalf("%s: the seed was announced", shape) } } } } // Every dependent the base grounds is judged: an ordinary one before the // seed is linked, and does not end the check. A forged dependent, whose // bytes are a seed but not the artifact its name names, is not refused, so // a forgery cannot bar the real artifact. func TestHorizonPushSeedLaterJudgesEveryDependent(t *testing.T) { o := newPCOrigin(t) baseCard, base := o.labelled("u", "b") ordinaryCard, ordinary := o.labelled("u", "ordinary") _, seed := o.labelled(seedLogin, "a") _, forgedSeed := o.bound("zit-seed") plain := deltaCard(t, base, baseCard.UUID, ordinary) s := deltaCard(t, base, baseCard.UUID, seed) forged := deltaCard(t, base, baseCard.UUID, forgedSeed) forged.UUID = hash.SHA1([]byte("a name the forged bytes do not hash to\n")) // A plain file after the forgery: no link step judges it, so it is // cleared only if the forgery does not end the fill's judging. fileBytes := append(slices.Clone(base), []byte("a plain file on the base\n")...) file := deltaCard(t, base, baseCard.UUID, fileBytes) if errs := errorsOf(o.push(plain, forged, file, s)); len(errs) != 0 { t.Fatalf("the dangling deltas: errors %q", errs) } if errs := errorsOf(o.push(baseCard)); len(errs) != 0 { t.Fatalf("the base's push: errors %q", errs) } for _, c := range []struct { uuid string want bool }{{ordinaryCard.UUID, false}, {s.UUID, true}, {forged.UUID, false}} { assertRefused(t, o.r, c.uuid, c.want) } frid, _ := blob.Exists(o.r.DB(), file.UUID) if withheld, err := seedcheck.IsWithheld(o.r.DB(), frid); err != nil || withheld { t.Fatalf("the file after the forgery is withheld (%v)", err) } var linked int if err := o.r.DB().QueryRow(`SELECT count(*) FROM event JOIN blob ON blob.rid=event.objid WHERE blob.uuid=?`, ordinaryCard.UUID).Scan(&linked); err != nil || linked == 0 { t.Fatalf("the ordinary dependent was not linked: %d rows (%v)", linked, err) } } // A delta that cannot be read does not end the check: a seed after it in // the same request is still refused. func TestHorizonPushSeedAfterUnexpandable(t *testing.T) { o := newPCOrigin(t) _, never := o.labelled("u", "never") _, other := o.labelled("u", "other") stuck := deltaCard(t, never, hash.SHA1(never), other) baseCard, base := o.labelled("u", "b") _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, baseCard.UUID, seed) resp := o.push(stuck, s, o.blobCard(), baseCard) assertSeedRefused(t, o, resp, stuck.UUID, s.UUID, o.fileUID, baseCard.UUID) } // Only this origin's own label is refused: another implementation's seed // label, and any user's, is an ordinary control artifact. func TestHorizonPushForeignSeedLabelOrdinary(t *testing.T) { for _, user := range []string{"zit-seed", "zit-serve", "u", "Go-Libfossil"} { o := newPCOrigin(t) c, _ := o.labelled(user, "a") if errs := errorsOf(o.push(c)); len(errs) != 0 || !o.held(c.UUID) { t.Fatalf("U %s: errors %q, held %v", user, errs, o.held(c.UUID)) } } } // A check-in carrying the label is not a seed: only control artifacts are. func TestHorizonPushSeedLabelCheckinOrdinary(t *testing.T) { o := newPCOrigin(t) d := &deck.Deck{Type: deck.Checkin, C: "x", D: o.when.Add(2 * time.Hour), U: deck.User(seedLogin), P: []string{o.p}, F: []deck.FileCard{{Name: "c", UUID: o.fileUID}}} data, err := d.Marshal() if err != nil { t.Fatal(err) } c := &xfer.FileCard{UUID: hash.SHA1(data), Content: data} if errs := errorsOf(o.push(o.blobCard(), c)); len(errs) != 0 || !o.held(c.UUID) { t.Fatalf("errors %q, held %v", errs, o.held(c.UUID)) } } // §5.7 is judged on expanded content too: a stale cancellation sent as a // delta whose base arrives later in the same push is refused. func TestHorizonPushStaleCancelLateDelta(t *testing.T) { o := newPCOrigin(t) card, uuid := o.branchFrom("feat") d := deltaCard(t, o.file, o.fileUID, card.Content) errs := errorsOf(o.push(d, o.blobCard())) if len(errs) != 1 || !strings.HasPrefix(errs[0], "stale seed: ") { t.Fatalf("errors = %q, want one stale-seed refusal", errs) } if o.held(uuid) || o.held(o.fileUID) { t.Fatal("the refused push stored artifacts") } } // A pusher surfaces the refusal as its sync's error and stops. func TestHorizonPushSeedRefusedClient(t *testing.T) { when := time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC) origin := setupPushableTestRepo(t) client := setupSyncTestRepo(t) rid, _ := mbCommit(t, client, "root", 0, when) if _, err := tag.AddTag(client, tag.TagOpts{TargetRID: rid, TagName: "seedtest", TagType: tag.TagSingleton, Value: "a", User: seedLogin, Time: when.Add(time.Hour)}); err != nil { t.Fatal(err) } res, err := Sync(context.Background(), client, handlerTransport(t, origin), SyncOpts{Push: true, ServerCode: "s", ProjectCode: testPC}) if err != nil { t.Fatalf("Sync: %v", err) } // Round one announces, round two sends what the origin asks for and is // refused; nothing is sent again. if res.Rounds != 2 || res.FilesSent != 3 { t.Fatalf("rounds %d, files sent %d: want the push to stop at the refusal", res.Rounds, res.FilesSent) } if len(res.Errors) != 1 || res.Errors[0] != "error: "+wantSeedRefused { t.Fatalf("errors = %q", res.Errors) } var n int if err := origin.DB().QueryRow("SELECT count(*) FROM blob WHERE size>=0").Scan(&n); err != nil { t.Fatal(err) } if n != 0 { t.Fatalf("origin stored %d artifacts of a refused push", n) } } // bound is a control artifact under user carrying a horizon-seed T card, the // shape of a bound seed whatever its label (horizon §5.2.3, spec E.2). func (o *pcOrigin) bound(user string) (*xfer.FileCard, []byte) { o.t.Helper() d := &deck.Deck{Type: deck.Control, D: o.when.Add(4 * time.Hour), U: deck.User(user), T: []deck.TagCard{{Type: deck.TagSingleton, Name: "horizon-seed", UUID: o.p, Value: o.p}}} data, err := d.Marshal() if err != nil { o.t.Fatal(err) } return &xfer.FileCard{UUID: hash.SHA1(data), Content: data}, data } func TestHorizonPushSeedShapeRefusedForeignLabel(t *testing.T) { o := newPCOrigin(t) s, _ := o.bound("zit-seed") resp := o.push(o.blobCard(), s) assertSeedRefused(t, o, resp, s.UUID, o.fileUID) } func TestHorizonPushSeedShapeRefusedLateBase(t *testing.T) { o := newPCOrigin(t) baseCard, base := o.labelled("u", "b") _, seed := o.bound("zit-seed") s := deltaCard(t, base, baseCard.UUID, seed) resp := o.push(s, o.blobCard(), baseCard) assertSeedRefused(t, o, resp, s.UUID, o.fileUID, baseCard.UUID) } // isPhantomRow reports whether uuid names a row whose content the store // does not hold. func isPhantomRow(q db.Querier, uuid string) (bool, error) { var phantom bool err := q.QueryRow("SELECT EXISTS(SELECT 1 FROM blob WHERE uuid=? AND size<0)", uuid).Scan(&phantom) return phantom, err } // assertRefused checks whether r records uuid as a refused seed: refused by // its hash, kept stored, withheld from every reply and never linked. A // refusal never lands in the shun table, which peers pull. func assertRefused(t *testing.T, r *repo.Repo, uuid string, want bool) { t.Helper() refused, err := seedcheck.IsRefusedUUID(r.DB(), uuid) if err != nil || refused != want { t.Fatalf("%.10s refused = %v (%v), want %v", uuid, refused, err, want) } if shunned, err := shun.IsShunned(r.DB(), uuid); err != nil || shunned { t.Fatalf("%.10s is shunned (%v)", uuid, err) } if !want { return } rid, held := blob.Exists(r.DB(), uuid) if !held { t.Fatalf("the refused %.10s is not kept", uuid) } if withheld, err := seedcheck.IsWithheld(r.DB(), rid); err != nil || !withheld { t.Fatalf("the refused %.10s is not withheld (%v)", uuid, err) } if n := seedLinkRows(t, r, uuid); n != 0 { t.Fatalf("the refused %.10s was crosslinked: %d rows", uuid, n) } } // seedLinkRows counts the derived rows sourced from uuid: tag rows and its // event. func seedLinkRows(t *testing.T, r *repo.Repo, uuid string) int { t.Helper() var n int if err := r.DB().QueryRow(`SELECT (SELECT count(*) FROM tagxref JOIN blob ON blob.rid=tagxref.srcid WHERE blob.uuid=?1) + (SELECT count(*) FROM event JOIN blob ON blob.rid=event.objid WHERE blob.uuid=?1)`, uuid).Scan(&n); err != nil { t.Fatal(err) } return n } // danglingSeed pushes a seed to o as a delta on base, which o lacks, and // returns the seed's card. The base is named as a new repository names // what it stores itself, by SHA3. func danglingSeed(t *testing.T, o *pcOrigin, base []byte) *xfer.FileCard { t.Helper() _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, hash.SHA3(base), seed) if errs := errorsOf(o.push(s)); len(errs) != 0 { t.Fatalf("the dangling delta: errors %q", errs) } return s } // The origin pulling the base from an upstream fills it through the client // path: the seed is judged there too, refused and never linked. func TestHorizonPullFillsSeedBase(t *testing.T) { o := newPCOrigin(t) upstream := setupPushableTestRepo(t) _, base := o.labelled("u", "b") s := danglingSeed(t, o, base) storeAndLink(t, upstream, base) res, err := Sync(context.Background(), o.r, handlerTransport(t, upstream), SyncOpts{Pull: true, ServerCode: "s", ProjectCode: testPC}) if err != nil || len(res.Errors) != 0 { t.Fatalf("pull: %v %q", err, res.Errors) } if phantom, err := isPhantomRow(o.r.DB(), hash.SHA3(base)); err != nil || phantom { t.Fatalf("the base was not pulled (%v)", err) } assertRefused(t, o.r, s.UUID, true) if n := seedLinkRows(t, o.r, s.UUID); n != 0 { t.Fatalf("the seed was crosslinked: %d rows", n) } } // A local check-in that writes the base's bytes fills it too, and judges // the seed in its own transaction: with no sweep after it, the seed is // already refused, and neither a clone nor a pulling peer receives it. func TestHorizonCheckinFillsSeedBase(t *testing.T) { o := newPCOrigin(t) base := []byte("file content that is also the seed's base\n") s := danglingSeed(t, o, base) if _, _, err := manifest.Checkin(o.r, manifest.CheckinOpts{Files: []manifest.File{{Name: "f", Content: base}}, Comment: "f", User: "u", Parent: 0, Time: o.when.Add(5 * time.Hour)}); err != nil { t.Fatal(err) } assertRefused(t, o.r, s.UUID, true) assertNotServed(t, o, s.UUID) } // assertNotServed checks that neither a clone of o nor a peer pulling from it // receives uuid. func assertNotServed(t *testing.T, o *pcOrigin, uuid string) { t.Helper() clone, _, err := Clone(context.Background(), filepath.Join(t.TempDir(), "c.fossil"), handlerTransport(t, o.r), CloneOpts{}) if err != nil { t.Fatal(err) } defer clone.Close() if _, ok := blob.Exists(clone.DB(), uuid); ok { t.Fatalf("a clone received %.10s", uuid) } peer := setupSyncTestRepo(t) if _, err := Sync(context.Background(), peer, handlerTransport(t, o.r), SyncOpts{Pull: true, ServerCode: "s", ProjectCode: testPC}); err != nil { t.Fatal(err) } if _, ok := blob.Exists(peer.DB(), uuid); ok { t.Fatalf("a pulling peer received %.10s", uuid) } } // A seed still pending, its base not yet arrived, is withheld too: it has // not been judged. (A clone of a store holding a dangling delta stalls // whatever the delta is, so only a pull and a gimme are checked.) func TestHorizonPendingSeedNotServed(t *testing.T) { o := newPCOrigin(t) _, base := o.labelled("u", "never sent") s := danglingSeed(t, o, base) resp, err := HandleSync(context.Background(), o.r, &xfer.Message{Cards: []xfer.Card{ &xfer.PullCard{ServerCode: "s", ProjectCode: testPC}, &xfer.GimmeCard{UUID: s.UUID}}}) if err != nil { t.Fatal(err) } for _, c := range resp.Cards { switch c := c.(type) { case *xfer.FileCard: if c.UUID == s.UUID { t.Fatal("a gimme was answered with the pending seed") } case *xfer.IGotCard: if c.UUID == s.UUID { t.Fatal("an inventory announced the pending seed") } } } } // A pending row is cleared once judged, whatever the artifact: a plain file // pushed as a delta ahead of its basis is served once the basis arrives. func TestHorizonPendingFileClearedWhenJudged(t *testing.T) { o := newPCOrigin(t) base := []byte("the basis of a plain file\n") file := []byte("the basis of a plain file\nand a line more\n") d := deltaCard(t, base, hash.SHA3(base), file) if errs := errorsOf(o.push(d)); len(errs) != 0 { t.Fatalf("the dangling file: errors %q", errs) } rid, _ := blob.Exists(o.r.DB(), d.UUID) if withheld, err := seedcheck.IsWithheld(o.r.DB(), rid); err != nil || !withheld { t.Fatalf("the dangling file is not pending (%v)", err) } if errs := errorsOf(o.push(&xfer.FileCard{UUID: hash.SHA3(base), Content: base})); len(errs) != 0 { t.Fatalf("the base's push: errors %q", errs) } if withheld, err := seedcheck.IsWithheld(o.r.DB(), rid); err != nil || withheld { t.Fatalf("the judged file is still withheld (%v)", err) } peer := setupSyncTestRepo(t) if _, err := Sync(context.Background(), peer, handlerTransport(t, o.r), SyncOpts{Pull: true, ServerCode: "s", ProjectCode: testPC}); err != nil { t.Fatal(err) } if _, ok := blob.Exists(peer.DB(), d.UUID); !ok { t.Fatal("a pulling peer did not receive the file") } // And the origin announces it again when it pushes on. pushed := false for _, req := range pushOnRecording(t, o) { for _, c := range req.Cards { if g, ok := c.(*xfer.IGotCard); ok && g.UUID == d.UUID { pushed = true } } } if !pushed { t.Fatal("the origin did not announce the judged file when pushing on") } } // Purging a pending delta forgets its record, so an innocent artifact that // is later stored under the reused rid is served. func TestHorizonPurgedPendingForgotten(t *testing.T) { o := newPCOrigin(t) _, base := o.labelled("u", "never sent") s := danglingSeed(t, o, base) pendingRID, _ := blob.Exists(o.r.DB(), s.UUID) var maxRID libfossil.FslID if err := o.r.DB().QueryRow("SELECT max(rid) FROM blob").Scan(&maxRID); err != nil { t.Fatal(err) } if pendingRID != maxRID { t.Fatalf("fixture: the pending delta is rid %d, not the last row %d", pendingRID, maxRID) } if err := shun.Add(o.r.DB(), s.UUID, "purge"); err != nil { t.Fatal(err) } if _, err := shun.Purge(o.r.DB()); err != nil { t.Fatal(err) } innocent := []byte("an innocent blob stored after the purge\n") rid, _, err := blob.Store(o.r.DB(), innocent) if err != nil { t.Fatal(err) } if rid != pendingRID { t.Fatalf("fixture: the innocent blob is rid %d, not the reused %d", rid, pendingRID) } if withheld, err := seedcheck.IsWithheld(o.r.DB(), rid); err != nil || withheld { t.Fatalf("the innocent blob inherited the pending record (%v)", err) } } // A private sync's inventory withholds a seed pushed privately. func TestHorizonSeedWithheldFromPrivateInventory(t *testing.T) { o := newPCOrigin(t) if _, err := o.r.DB().Exec("UPDATE user SET cap=cap||'x' WHERE login='nobody'"); err != nil { t.Fatal(err) } _, base := o.labelled("u", "never sent") _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, hash.SHA3(base), seed) private := &xfer.PragmaCard{Name: "send-private"} push := func(cards ...xfer.Card) *xfer.Message { req := &xfer.Message{Cards: append([]xfer.Card{private, &xfer.PushCard{ServerCode: "s", ProjectCode: testPC}}, cards...)} resp, err := HandleSync(context.Background(), o.r, req) if err != nil { t.Fatal(err) } return resp } if errs := errorsOf(push(&xfer.PrivateCard{}, s)); len(errs) != 0 { t.Fatalf("the private dangling seed: errors %q", errs) } resp, err := HandleSync(context.Background(), o.r, &xfer.Message{Cards: []xfer.Card{private, &xfer.PullCard{ServerCode: "s", ProjectCode: testPC}}}) if err != nil { t.Fatal(err) } for _, c := range resp.Cards { if g, ok := c.(*xfer.IGotCard); ok && g.UUID == s.UUID { t.Fatal("a private inventory announced the seed") } } } // A delta built on a pending seed is an ordinary artifact: once the base // arrives the seed is refused but kept, so the delta on it is readable, // linked and served. Its chain runs through the seed, whether it arrived // after the seed (root the base) or before it (root the seed, moved to the // base when the seed arrives). func TestHorizonDeltaOnSeedServed(t *testing.T) { for _, order := range []string{"seed first", "dependent first"} { o := newPCOrigin(t) _, base := o.labelled("u", "b") _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, hash.SHA3(base), seed) depCard, dep := o.labelled("u", "built on the seed") y := deltaCard(t, seed, s.UUID, dep) // A plain file on the seed too: no link step judges it, so only the // fill that grounds it clears it. file := append(slices.Clone(seed), []byte("a plain file built on the seed\n")...) f := deltaCard(t, seed, s.UUID, file) cards := []*xfer.FileCard{s, y, f} if order == "dependent first" { cards = []*xfer.FileCard{y, f, s} } for _, c := range cards { if errs := errorsOf(o.push(c)); len(errs) != 0 { t.Fatalf("%s: pushing %.10s: errors %q", order, c.UUID, errs) } } if errs := errorsOf(o.push(&xfer.FileCard{UUID: hash.SHA3(base), Content: base})); len(errs) != 0 { t.Fatalf("%s: the base's push: errors %q", order, errs) } assertRefused(t, o.r, s.UUID, true) rid, _ := blob.Exists(o.r.DB(), depCard.UUID) if !content.IsAvailable(o.r.DB(), rid) { t.Fatalf("%s: the delta on the seed is not readable", order) } if withheld, err := seedcheck.IsWithheld(o.r.DB(), rid); err != nil || withheld { t.Fatalf("%s: the delta on the seed is withheld (%v)", order, err) } if n := seedLinkRows(t, o.r, depCard.UUID); n == 0 { t.Fatalf("%s: the delta on the seed was not linked", order) } frid, _ := blob.Exists(o.r.DB(), f.UUID) if withheld, err := seedcheck.IsWithheld(o.r.DB(), frid); err != nil || withheld { t.Fatalf("%s: the file on the seed is withheld (%v)", order, err) } resp, err := HandleSync(context.Background(), o.r, &xfer.Message{Cards: []xfer.Card{ &xfer.PullCard{ServerCode: "s", ProjectCode: testPC}, &xfer.GimmeCard{UUID: depCard.UUID}}}) if err != nil { t.Fatal(err) } served := false for _, c := range resp.Cards { if f, ok := c.(*xfer.FileCard); ok && f.UUID == depCard.UUID { served = true } } if !served { t.Fatalf("%s: the delta on the seed was not served", order) } } } // A reply that read the withheld set for a gimme before the request stored // a dangling seed still keeps the seed out of its inventory. func TestHorizonSeedWithheldAfterEarlierGimme(t *testing.T) { o := newPCOrigin(t) _, base := o.labelled("u", "never sent") _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, hash.SHA3(base), seed) resp, err := HandleSync(context.Background(), o.r, &xfer.Message{Cards: []xfer.Card{ &xfer.PushCard{ServerCode: "s", ProjectCode: testPC}, &xfer.PullCard{ServerCode: "s", ProjectCode: testPC}, &xfer.GimmeCard{UUID: o.p}, s}}) if err != nil { t.Fatal(err) } for _, c := range resp.Cards { if g, ok := c.(*xfer.IGotCard); ok && g.UUID == s.UUID { t.Fatal("the inventory announced the seed") } } } // A refused seed is served by no path: not cloned, and not pushed on by the // origin acting as a client. func TestHorizonRefusedSeedNotServed(t *testing.T) { o := newPCOrigin(t) _, base := o.labelled("u", "b") s := danglingSeed(t, o, base) baseCard := &xfer.FileCard{UUID: hash.SHA3(base), Content: base} if errs := errorsOf(o.push(baseCard)); len(errs) != 0 { t.Fatalf("the base's push: errors %q", errs) } assertRefused(t, o.r, s.UUID, true) clone, _, err := Clone(context.Background(), filepath.Join(t.TempDir(), "c.fossil"), handlerTransport(t, o.r), CloneOpts{}) if err != nil { t.Fatal(err) } defer clone.Close() if _, ok := blob.Exists(clone.DB(), s.UUID); ok { t.Fatal("a clone received the refused seed") } if _, ok := blob.Exists(clone.DB(), baseCard.UUID); !ok { t.Fatal("a clone did not receive the base") } // The peer asks for the seed by name, as one that learned it elsewhere // would: the origin, as a client, still does not send it. peer := setupPushableTestRepo(t) asked := false tr := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message { for _, c := range req.Cards { if f, ok := c.(*xfer.FileCard); ok && f.UUID == s.UUID { t.Error("the origin pushed the refused seed on") } if g, ok := c.(*xfer.IGotCard); ok && g.UUID == s.UUID { t.Error("the origin announced the refused seed") } } resp, err := HandleSync(context.Background(), peer, req) if err != nil { t.Errorf("HandleSync: %v", err) return &xfer.Message{} } if !asked { asked = true resp.Cards = append(resp.Cards, &xfer.GimmeCard{UUID: s.UUID}) } return resp }} if _, err := Sync(context.Background(), o.r, tr, SyncOpts{Push: true, ServerCode: "s", ProjectCode: testPC}); err != nil { t.Fatal(err) } } // The refusal record is store-local: no configuration group carries it. func TestSeedcheckTableNotSynced(t *testing.T) { for _, tbl := range configTables { if strings.Contains(tbl.table, "seedcheck") { t.Fatalf("configuration group %s carries the seedcheck table", tbl.card) } } } // An origin that refused a seed by its hash refuses a re-push of it at // once, as full text or as a delta whose basis it lacks. func TestHorizonRePushRefusedByHash(t *testing.T) { o := newPCOrigin(t) _, base := o.labelled("u", "b") s := danglingSeed(t, o, base) if errs := errorsOf(o.push(&xfer.FileCard{UUID: hash.SHA3(base), Content: base})); len(errs) != 0 { t.Fatalf("the base's push: errors %q", errs) } assertRefused(t, o.r, s.UUID, true) _, seed := o.labelled(seedLogin, "a") _, other := o.labelled("u", "elsewhere") for _, card := range []*xfer.FileCard{ {UUID: s.UUID, Content: seed}, deltaCard(t, other, hash.SHA3(other), seed), } { assertSeedRefused(t, o, o.push(o.blobCard(), card), o.fileUID) assertRefused(t, o.r, s.UUID, true) } // A pusher announcing the hash is not asked for it. for _, c := range o.push(&xfer.IGotCard{UUID: s.UUID}).Cards { if g, ok := c.(*xfer.GimmeCard); ok && g.UUID == s.UUID { t.Fatal("the origin asked for a refused seed a pusher announced") } } } // A tag whose control artifact is a pending seed's basis judges the seed in // its own transaction: it is refused and dropped. func TestHorizonTagFillsSeedBase(t *testing.T) { when := time.Date(2026, 10, 1, 20, 0, 0, 0, time.UTC) addTag := func(o *pcOrigin) libfossil.FslID { t.Helper() target, ok := blob.Exists(o.r.DB(), o.p) if !ok { t.Fatal("origin does not hold p") } rid, err := tag.AddTag(o.r, tag.TagOpts{TargetRID: target, TagName: "release", TagType: tag.TagSingleton, User: "u", Time: when}) if err != nil { t.Fatal(err) } return rid } // The same tag on an identical origin gives the control artifact's bytes. twin := newPCOrigin(t) base, err := content.Expand(twin.r.DB(), addTag(twin)) if err != nil { t.Fatal(err) } o := newPCOrigin(t) s := danglingSeed(t, o, base) addTag(o) assertRefused(t, o.r, s.UUID, true) } // A pending seed made readable by a path that judges nothing (a raw store // here) is still caught by the link step: it is refused and never linked. func TestHorizonLinkJudgesPendingSeed(t *testing.T) { o := newPCOrigin(t) base := []byte("a basis stored with no seed check\n") s := danglingSeed(t, o, base) if _, _, err := blob.Store(o.r.DB(), base); err != nil { t.Fatal(err) } if _, err := manifest.Crosslink(o.r); err != nil { t.Fatal(err) } if refused, err := seedcheck.IsRefusedUUID(o.r.DB(), s.UUID); err != nil || !refused { t.Fatalf("the link step did not refuse the seed (%v)", err) } if n := seedLinkRows(t, o.r, s.UUID); n != 0 { t.Fatalf("the seed was crosslinked: %d rows", n) } } // pushOnRecording pushes o's store to a fresh peer and returns every request // the peer saw, failing if the push does not converge. func pushOnRecording(t *testing.T, o *pcOrigin) []*xfer.Message { t.Helper() peer := &requestRecorder{upstream: setupPushableTestRepo(t)} if _, err := Sync(context.Background(), o.r, peer, SyncOpts{Push: true, ServerCode: "s", ProjectCode: testPC}); err != nil { t.Fatalf("push: %v", err) } return peer.requests } // requestRecorder serves upstream and records every request. type requestRecorder struct { upstream *repo.Repo requests []*xfer.Message } func (p *requestRecorder) Exchange(ctx context.Context, req *xfer.Message) (*xfer.Message, error) { p.requests = append(p.requests, req) return HandleSync(ctx, p.upstream, req) } // assertNeverNamed checks that no request named uuid in any card. func assertNeverNamed(t *testing.T, requests []*xfer.Message, uuid string) { t.Helper() for _, req := range requests { for _, c := range req.Cards { switch c := c.(type) { case *xfer.IGotCard: if c.UUID == uuid { t.Fatalf("a push announced %.10s", uuid) } case *xfer.FileCard: if c.UUID == uuid { t.Fatalf("a push sent %.10s", uuid) } } } } } // A pending seed is not announced when the origin pushes on to a peer. func TestHorizonPendingSeedNotAnnouncedOnPush(t *testing.T) { o := newPCOrigin(t) _, base := o.labelled("u", "never sent") s := danglingSeed(t, o, base) assertNeverNamed(t, pushOnRecording(t, o), s.UUID) } // After a rebuild, which marks every blob unclustered and unsent again, a // refused seed is still not announced: a push on to a peer converges and // the peer never learns its hash. func TestHorizonRefusedSeedNotAnnouncedAfterRebuild(t *testing.T) { o := newPCOrigin(t) _, base := o.labelled("u", "b") s := danglingSeed(t, o, base) if errs := errorsOf(o.push(&xfer.FileCard{UUID: hash.SHA3(base), Content: base})); len(errs) != 0 { t.Fatalf("the base's push: errors %q", errs) } assertRefused(t, o.r, s.UUID, true) if _, err := verify.Rebuild(o.r); err != nil { t.Fatal(err) } assertNeverNamed(t, pushOnRecording(t, o), s.UUID) } // privateDanglingSeed pushes a seed to o privately, as a delta whose basis // o lacks, so it is pending and private. func privateDanglingSeed(t *testing.T, o *pcOrigin) *xfer.FileCard { t.Helper() if _, err := o.r.DB().Exec("UPDATE user SET cap=cap||'x' WHERE login='nobody'"); err != nil { t.Fatal(err) } _, base := o.labelled("u", "never sent") _, seed := o.labelled(seedLogin, "a") s := deltaCard(t, base, hash.SHA3(base), seed) resp, err := HandleSync(context.Background(), o.r, &xfer.Message{Cards: []xfer.Card{ &xfer.PragmaCard{Name: "send-private"}, &xfer.PushCard{ServerCode: "s", ProjectCode: testPC}, &xfer.PrivateCard{}, s}}) if err != nil { t.Fatal(err) } if errs := errorsOf(resp); len(errs) != 0 { t.Fatalf("the private dangling seed: errors %q", errs) } return s } // A private push on to a peer does not announce a privately pushed pending // seed. func TestHorizonPendingSeedNotAnnouncedPrivately(t *testing.T) { o := newPCOrigin(t) s := privateDanglingSeed(t, o) upstream := setupPushableTestRepo(t) if _, err := upstream.DB().Exec("UPDATE user SET cap=cap||'x' WHERE login='nobody'"); err != nil { t.Fatal(err) } peer := &requestRecorder{upstream: upstream} if _, err := Sync(context.Background(), o.r, peer, SyncOpts{Push: true, Private: true, ServerCode: "s", ProjectCode: testPC}); err != nil { t.Fatal(err) } assertNeverNamed(t, peer.requests, s.UUID) } // A peer asking for a privately pushed pending seed outside a private sync // is not told it exists. func TestHorizonPendingSeedNotNamedPrivate(t *testing.T) { o := newPCOrigin(t) s := privateDanglingSeed(t, o) peer := setupPushableTestRepo(t) asked := false var requests []*xfer.Message tr := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message { requests = append(requests, req) resp, err := HandleSync(context.Background(), peer, req) if err != nil { t.Errorf("HandleSync: %v", err) return &xfer.Message{} } if !asked { asked = true resp.Cards = append(resp.Cards, &xfer.GimmeCard{UUID: s.UUID}) } return resp }} if _, err := Sync(context.Background(), o.r, tr, SyncOpts{Push: true, ServerCode: "s", ProjectCode: testPC}); err != nil { t.Fatal(err) } assertNeverNamed(t, requests, s.UUID) } // Horizon F: a pushed artifact is judged when it first becomes readable, // whatever path supplies its basis. Here a pull supplies the basis as a // delta, first on a basis the origin lacks, then on a held one: the pushed // file and control artifact waiting on it are judged when the chain // resolves, then served, announced and, the control artifact, linked. func TestHorizonPulledDeltaFillsPendingBasis(t *testing.T) { for _, held := range []bool{false, true} { o := newPCOrigin(t) root := []byte("the root of the basis chain\n") basis := append(slices.Clone(root), []byte("the basis the pending artifacts need\n")...) fileBytes := append(slices.Clone(basis), []byte("a plain file pushed on the basis\n")...) ctlCard, ctl := o.labelled("u", "a control artifact pushed on the basis") file := deltaCard(t, basis, hash.SHA3(basis), fileBytes) control := deltaCard(t, basis, hash.SHA3(basis), ctl) if errs := errorsOf(o.push(file, control)); len(errs) != 0 { t.Fatalf("held=%v: pending pushes: errors %q", held, errs) } if held { if _, _, err := blob.Store(o.r.DB(), root); err != nil { t.Fatal(err) } } // The basis arrives by pull as a delta on root; root, unless held, // arrives in the next round. d, err := delta.Create(root, basis) if err != nil { t.Fatal(err) } basisCard := &xfer.FileCard{UUID: hash.SHA3(basis), DeltaSrc: hash.SHA3(root), Content: d} round := 0 tr := &MockTransport{Handler: func(req *xfer.Message) *xfer.Message { round++ if round == 1 { return &xfer.Message{Cards: []xfer.Card{basisCard}} } for _, c := range req.Cards { if g, ok := c.(*xfer.GimmeCard); ok && g.UUID == hash.SHA3(root) { return &xfer.Message{Cards: []xfer.Card{&xfer.FileCard{UUID: g.UUID, Content: root}}} } } return &xfer.Message{} }} if _, err := Sync(context.Background(), o.r, tr, SyncOpts{Pull: true, ServerCode: "s", ProjectCode: testPC}); err != nil { t.Fatal(err) } for _, u := range []string{file.UUID, ctlCard.UUID} { rid, _ := blob.Exists(o.r.DB(), u) if !content.IsAvailable(o.r.DB(), rid) { t.Fatalf("held=%v: %.10s is not readable", held, u) } if withheld, err := seedcheck.IsWithheld(o.r.DB(), rid); err != nil || withheld { t.Fatalf("held=%v: %.10s is still withheld (%v)", held, u, err) } } if n := seedLinkRows(t, o.r, ctlCard.UUID); n == 0 { t.Fatalf("held=%v: the control artifact was not linked", held) } announced := map[string]bool{} for _, req := range pushOnRecording(t, o) { for _, c := range req.Cards { if g, ok := c.(*xfer.IGotCard); ok { announced[g.UUID] = true } } } if !announced[file.UUID] || !announced[ctlCard.UUID] { t.Fatalf("held=%v: announced file %v, control %v", held, announced[file.UUID], announced[ctlCard.UUID]) } } }