Artifact 776349f6d42d8ac96b850cd80a621bce35d9ba3adecfec143834b36228d3abff:
- File
internal/seedcheck/drop_test.go
— part of check-in
[917bcc9830]
at
2026-10-28 11:11:21
on branch fix/seed-delta-drop
— seedcheck: drop a seed completed as a delta in a later round
A horizon seed stored as a delta whose base arrived only in a later round (pull, clone) or a later request (origin push) was refused by its hash but kept in the store, and a pull or clone counted the delta as received on arrival, before it could be judged (draft-fossil-horizon-00 §5.2.3, HZ-5.2-14).
On a seed verdict seedcheck now rewrites each delta built on the seed as full text, then empties the seed's row: size -1, no content, no delta link, and kept out of the phantom table so no request asks for it. The rid stays held for anything a receive pass tracks by it, and the refused hash is still recorded in store-local state.
A pull or clone counts a delta it could not read on arrival only at the end of the exchange, once readable and not dropped. A round still treats it as progress. The receive pass treats a row without content as no basis even when its cache holds the old bytes, and the push handler's same-request seed check consults the refused hash, since the dropped row no longer reads.
HZ-5.2-14 is now covered. (user: [email protected], size: 5088) [annotate] [blame] [check-ins using]
package seedcheck_test import ( "path/filepath" "testing" "time" "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/repo" "github.com/danmestas/go-libfossil/internal/seedcheck" "github.com/danmestas/go-libfossil/simio" ) func dropRepo(t *testing.T) *repo.Repo { t.Helper() r, err := repo.Create(filepath.Join(t.TempDir(), "r.fossil"), "u", simio.CryptoRand{}, "") if err != nil { t.Fatal(err) } t.Cleanup(func() { r.Close() }) return r } // seedDeck is a seed by this implementation's own label. func seedDeck(t *testing.T) (*deck.Deck, []byte) { t.Helper() d := &deck.Deck{Type: deck.Control, D: time.Date(2026, 10, 1, 16, 0, 0, 0, time.UTC), U: deck.User(seedcheck.Label), T: []deck.TagCard{{Type: deck.TagSingleton, Name: "alpha", UUID: "0123456789abcdef0123456789abcdef01234567"}}} data, err := d.Marshal() if err != nil { t.Fatal(err) } return d, data } // onto stores target as a delta on the row at src and returns its rid. func onto(t *testing.T, r *repo.Repo, src libfossil.FslID, base, target []byte) libfossil.FslID { t.Helper() d, err := delta.Create(base, target) if err != nil { t.Fatal(err) } rid, err := blob.StoreDeltaRaw(r.DB(), hash.SHA1(target), d, src, nil) if err != nil { t.Fatal(err) } return rid } // assertDropped checks that the seed at rid holds no content, is no phantom // any request would ask for, is no delta, and stays refused. func assertDropped(t *testing.T, r *repo.Repo, rid libfossil.FslID, uuid string) { t.Helper() var size int64 var held []byte if err := r.DB().QueryRow("SELECT size, content FROM blob WHERE rid=?", rid).Scan(&size, &held); err != nil { t.Fatalf("the dropped seed's rid is not held: %v", err) } if size != -1 || held != nil { t.Fatalf("the dropped seed keeps content: size=%d, %d content bytes", size, len(held)) } var n int r.DB().QueryRow("SELECT count(*) FROM phantom WHERE rid=?", rid).Scan(&n) if n != 0 { t.Fatal("the dropped seed is a phantom a request would ask for") } r.DB().QueryRow("SELECT count(*) FROM delta WHERE rid=? OR srcid=?", rid, rid).Scan(&n) if n != 0 { t.Fatalf("%d delta rows still name the dropped seed", n) } if refused, err := seedcheck.IsRefusedUUID(r.DB(), uuid); err != nil || !refused { t.Fatalf("the dropped seed is not refused (%v)", err) } if withheld, err := seedcheck.IsWithheld(r.DB(), rid); err != nil || !withheld { t.Fatalf("the dropped seed is not withheld (%v)", err) } } // assertExpands checks that rid still reads back as want. func assertExpands(t *testing.T, r *repo.Repo, rid libfossil.FslID, want []byte) { t.Helper() got, err := content.Expand(r.DB(), rid) if err != nil { t.Fatalf("rid=%d no longer expands: %v", rid, err) } if string(got) != string(want) { t.Fatalf("rid=%d expands to other bytes", rid) } } // A seed completed by a later fill is dropped (§5.2.3): its row keeps its // rid, so the deltas built on it are rewritten as full text first and still // read back, but its content is gone. func TestJudgeFilledDropsLateSeed(t *testing.T) { r := dropRepo(t) _, seed := seedDeck(t) base := []byte("the basis a later round supplies\n") src, err := blob.StorePhantom(r.DB(), hash.SHA3(base)) if err != nil { t.Fatal(err) } seedRid := onto(t, r, src, base, seed) child := append([]byte("built on the seed\n"), seed...) childRid := onto(t, r, seedRid, seed, child) grandchild := append([]byte("built on its dependent\n"), child...) grandRid := onto(t, r, childRid, child, grandchild) for _, rid := range []libfossil.FslID{seedRid, childRid, grandRid} { if err := seedcheck.MarkPending(r.DB(), rid); err != nil { t.Fatal(err) } } filled, _, err := blob.Store(r.DB(), base) if err != nil { t.Fatal(err) } if _, err := seedcheck.JudgeFilled(r.DB(), []libfossil.FslID{filled}); err != nil { t.Fatal(err) } assertDropped(t, r, seedRid, hash.SHA1(seed)) assertExpands(t, r, childRid, child) assertExpands(t, r, grandRid, grandchild) for _, rid := range []libfossil.FslID{childRid, grandRid} { if withheld, err := seedcheck.IsWithheld(r.DB(), rid); err != nil || withheld { t.Fatalf("rid=%d, judged not a seed, is still withheld (%v)", rid, err) } } } // The link step's verdict drops the seed the same way. func TestJudgeDropsSeedAtLinkStep(t *testing.T) { r := dropRepo(t) d, seed := seedDeck(t) seedRid, uuid, err := blob.Store(r.DB(), seed) if err != nil { t.Fatal(err) } child := append([]byte("built on the seed\n"), seed...) childRid := onto(t, r, seedRid, seed, child) refused, err := seedcheck.Judge(r.DB(), seedcheck.Tables{}, seedRid, d) if err != nil || !refused { t.Fatalf("the seed was not refused at the link step (%v)", err) } assertDropped(t, r, seedRid, uuid) assertExpands(t, r, childRid, child) }