Artifact f428cce95ee791dae7155d6fe8cf8713c382be557371f3091567a1f2f74efaa4:
- File
conformance/horizon_seed_test.go
— part of check-in
[45fb2c7ed4]
at
2026-10-22 14:33:04
on branch feat/horizon-cones
— feat(horizon): mint, hold and apply the structural-horizon seed
A structural-horizon clone lacks C's ancestry, so tag propagation has nowhere to start: every check-in in K would derive no branch, and closed markers and names set below C would vanish. The seed of draft-fossil-horizon-00 Section 5.2 restates the dense tag state at each entry of K so the unmodified derivation comes out exact.
internal/horizon/seed mints it at an origin (T cards per row in force at C and each check-in of K whose primary parent is outside K; D the latest summarized event rounded up to a whole second; U the origin's login), and holds it at a clone in a table of its own, outside the artifact store, so nothing that enumerates or serves artifacts can send it. It is applied when held, by Rebuild, and by seed.Apply, which the sync layer calls after each round. Replacing or discarding a seed re-derives the store, since only a re-derivation separates the old seed's rows from those real artifacts wrote. A Verifier hook carries the Section 5.2.4 provenance policy; no signing is implemented.
The crosslink-table clearing moves from verify into manifest.Rederive so Rebuild and the seed share one owner of re-derivation. The cards match zit's seed byte for byte in T and U, and each implementation derives from the other's seed what it derives from its own. (user: [email protected], size: 17676) [annotate] [blame] [check-ins using]
package conformance import ( "context" "errors" "fmt" "net/http/httptest" "os" "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/cone" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/horizon" "github.com/danmestas/go-libfossil/internal/horizon/seed" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/sync" "github.com/danmestas/go-libfossil/internal/tag" "github.com/danmestas/go-libfossil/internal/verify" "github.com/danmestas/go-libfossil/internal/xfer" "github.com/danmestas/go-libfossil/simio" ) // The seed cases (§5.2) run over hzSeed: trunk c1..c4 with C = c3, and a // branch feat forked at c1 whose feat2 merges C in, so K = {c3, c4, feat2} // and its entries are c3 and feat2. Below C a propagating hue is set on c1 // and feat is closed by a propagating closed tag on feat1; sym-v1 is set on // C itself. internal/horizon/seed mints the seed at an origin and holds and // applies it at a clone; the wire (§6.2.3) is not yet implemented, so a // clone here is a fresh store given K's manifests and every other artifact // but C's ancestry. func horizonSeedCases() []Case { return []Case{horizonSeedCards, horizonSeedDate, horizonSeedPrivate, horizonSeedOrdinary, horizonSeedProvenance} } var ( horizonSeedCards = Case{ID: "horizon.5.2.1.seed-cards", Req: "HZ-5.2-1"} horizonSeedDate = Case{ID: "horizon.5.2.2.seed-date", Req: "HZ-5.2-2"} horizonSeedPrivate = Case{ID: "horizon.5.2.3.seed-private", Req: "HZ-5.2-3"} horizonSeedOrdinary = Case{ID: "horizon.5.2.3.seed-shaped-ordinary", Req: "HZ-5.2-4"} horizonSeedProvenance = Case{ID: "horizon.5.2.4.seed-provenance", Req: "HZ-5.2-6"} ) type hzSeed struct { f *scRepo c1, c2, c3, c4, feat1, feat2 libfossil.FslID k []libfossil.FslID } func newHZSeed(t *testing.T) *hzSeed { t.Helper() s := &hzSeed{f: newSCSyncRepo(t, scSyncFiles())} s.c1 = s.f.c1 s.feat1 = hzCommit(t, s.f, "feat1", []libfossil.FslID{s.c1}, hzBranch("feat")) s.c2 = hzCommit(t, s.f, "c2", []libfossil.FslID{s.c1}, nil) s.tag(t, s.c1, "hue", tag.TagPropagating, "red") s.c3 = hzCommit(t, s.f, "c3", []libfossil.FslID{s.c2}, nil) s.tag(t, s.c3, "sym-v1", tag.TagSingleton, "") s.c4 = hzCommit(t, s.f, "c4", []libfossil.FslID{s.c3}, nil) s.feat2 = hzCommit(t, s.f, "feat2", []libfossil.FslID{s.feat1, s.c3}, nil) s.tag(t, s.feat1, "closed", tag.TagPropagating, "") k, err := horizon.HeldSet(context.Background(), s.f.r.DB(), s.c3, cone.RuleDescendants) if err != nil { t.Fatal(err) } s.k = k return s } // tag applies a tag an hour after the last event, as a control artifact. func (s *hzSeed) tag(t *testing.T, target libfossil.FslID, name string, typ int, value string) { t.Helper() s.f.when = s.f.when.Add(time.Hour) if _, err := tag.AddTag(s.f.r, tag.TagOpts{ TargetRID: target, TagName: name, TagType: typ, Value: value, User: "u", Time: s.f.when, }); err != nil { t.Fatal(err) } } func (s *hzSeed) mint(login string) (string, []byte, error) { return seed.Mint(s.f.r.DB(), s.c3, s.k, login) } // hzTagState is the tag state in force at a check-in: its rows of positive // type as name, type and value, sorted. func hzTagState(q db.Querier, uuid string) ([]string, error) { rows, err := q.Query(`SELECT tag.tagname, tagxref.tagtype, coalesce(tagxref.value, '') FROM tagxref JOIN tag USING(tagid) JOIN blob ON blob.rid=tagxref.rid WHERE blob.uuid=? AND tagxref.tagtype>0`, uuid) if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var name, value string var typ int if err := rows.Scan(&name, &typ, &value); err != nil { return nil, err } out = append(out, fmt.Sprintf("%s/%d=%s", name, typ, value)) } slices.Sort(out) return out, rows.Err() } // derivesDense is nil when every check-in of K carries in clone the tag // state and leaf verdict it carries at the origin. func (s *hzSeed) derivesDense(t *testing.T, clone *repo.Repo) error { for _, rid := range s.k { u := hzUUID(t, s.f.r, rid) want, err := hzTagState(s.f.r.DB(), u) if err != nil { return err } got, err := hzTagState(clone.DB(), u) if err != nil { return err } if !slices.Equal(got, want) { return expectf("check-in %.10s carries %v, dense %v", u, got, want) } var leafGot, leafWant int clone.DB().QueryRow("SELECT count(*) FROM leaf JOIN blob USING(rid) WHERE uuid=?", u).Scan(&leafGot) s.f.r.DB().QueryRow("SELECT count(*) FROM leaf JOIN blob USING(rid) WHERE uuid=?", u).Scan(&leafWant) if leafGot != leafWant { return expectf("check-in %.10s leaf %d, dense %d", u, leafGot, leafWant) } } return nil } // lacking is a fresh store holding every artifact of the origin but the // check-in manifests outside K, crosslinked: what a structural-horizon // clone holds before its seed. func (s *hzSeed) lacking(t *testing.T) *repo.Repo { t.Helper() clone, err := repo.Create(filepath.Join(t.TempDir(), "lacking.fossil"), "u", simio.CryptoRand{}, "") if err != nil { t.Fatal(err) } t.Cleanup(func() { clone.Close() }) // The origin names its artifacts by SHA1; so must the copy. if err := clone.SetHashPolicy("sha1"); err != nil { t.Fatal(err) } rows, err := s.f.r.DB().Query(`SELECT rid FROM blob WHERE size>=0 AND rid NOT IN (SELECT objid FROM event WHERE type='ci') ORDER BY rid`) if err != nil { t.Fatal(err) } rids := slices.Clone(s.k) for rows.Next() { var rid libfossil.FslID rows.Scan(&rid) rids = append(rids, rid) } rows.Close() err = clone.WithTx(func(tx *db.Tx) error { for _, rid := range rids { data, err := content.Expand(s.f.r.DB(), rid) if err != nil { return err } if _, _, err := blob.Store(tx, data); err != nil { return err } } return nil }) if err != nil { t.Fatal(err) } if _, err := manifest.Crosslink(clone); err != nil { t.Fatal(err) } return clone } func hzHold(r *repo.Repo, name string, data []byte, v seed.Verifier) error { return r.WithTx(func(tx *db.Tx) error { return seed.Hold(tx, name, data, v) }) } func TestHorizonSeed(t *testing.T) { s := newHZSeed(t) check(t, horizonSeedCards, func() error { return hzSeedCards(t, s) }) check(t, horizonSeedDate, func() error { return hzSeedDate(t) }) check(t, horizonSeedPrivate, func() error { return hzSeedPrivate(t, s) }) check(t, horizonSeedOrdinary, func() error { return hzSeedOrdinary(t, s) }) check(t, horizonSeedProvenance, func() error { return hzSeedProvenance(t, s) }) } // hzSeedCards: the seed's T cards apply to each entry exactly the rows in // force there densely, its U card is the origin's login, and a store lacking // C's ancestry derives K densely once it holds the seed and not before. func hzSeedCards(t *testing.T, s *hzSeed) error { _, data, err := s.mint("origin") if err != nil { return err } d, err := deck.Parse(data) if err != nil || d.Type != deck.Control { return expectf("seed parses as %v, %v; want a control artifact", d, err) } if d.U == nil || *d.U != "origin" { return expectf("U card %v, want the origin's login", d.U) } c3, feat2 := hzUUID(t, s.f.r, s.c3), hzUUID(t, s.f.r, s.feat2) for _, line := range []string{ "T *branch " + c3 + " trunk\n", "T +sym-v1 " + c3 + "\n", "T *hue " + c3 + " red\n", "T *branch " + feat2 + " feat\n", "T *closed " + feat2 + "\n", } { if !strings.Contains(string(data), line) { return expectf("seed lacks %q", line) } } cards := map[string][]string{} for _, tc := range d.T { typ := tag.TagSingleton if tc.Type == deck.TagPropagating { typ = tag.TagPropagating } cards[tc.UUID] = append(cards[tc.UUID], fmt.Sprintf("%s/%d=%s", tc.Name, typ, tc.Value)) } if len(cards) != 2 { return expectf("seed targets %d check-ins, want the 2 entries", len(cards)) } for _, u := range []string{c3, feat2} { want, err := hzTagState(s.f.r.DB(), u) if err != nil { return err } got := cards[u] slices.Sort(got) if !slices.Equal(got, want) { return expectf("cards at %.10s %v, dense rows %v", u, got, want) } } clone := s.lacking(t) if s.derivesDense(t, clone) == nil { return expectf("the store lacking C's ancestry derives densely without a seed") } if err := hzHold(clone, hash.SHA3(data), data, nil); err != nil { return err } return s.derivesDense(t, clone) } // hzSeedDate: D is the latest mtime among the rows summarized, so it is // never earlier than any of them, re-minting over unchanged state yields the // same name, and a later tag event moves D past the seed it replaces. func hzSeedDate(t *testing.T) error { s := newHZSeed(t) name, data, err := s.mint("origin") if err != nil { return err } d, err := deck.Parse(data) if err != nil { return err } var latest float64 for _, rid := range []libfossil.FslID{s.c3, s.feat2} { var m float64 s.f.r.DB().QueryRow("SELECT max(mtime) FROM tagxref WHERE rid=? AND tagtype>0", rid).Scan(&m) latest = max(latest, m) } if got := libfossil.MillisFromJulianDay(latest); d.D.UnixMilli() != got { return expectf("D %v, want the latest summarized event %v", d.D, time.UnixMilli(got).UTC()) } again, _, err := s.mint("origin") if err != nil || again != name { return expectf("re-mint over unchanged state named %s (%v), first %s", again, err, name) } s.tag(t, s.c3, "sym-v2", tag.TagSingleton, "") _, data2, err := s.mint("origin") if err != nil { return err } d2, err := deck.Parse(data2) if err != nil { return err } if !d2.D.After(d.D) { return expectf("re-minted D %v is not after the replaced seed's %v", d2.D, d.D) } return nil } // hzSeedPrivate: the held seed is no artifact of the store. Holding it adds // no blob and marks nothing to send, a peer asking for it by name or cloning // the store is never sent it, a rebuild re-applies it without making it a // blob, and the derivation keeps it. func hzSeedPrivate(t *testing.T, s *hzSeed) error { clone := s.lacking(t) if _, err := clone.DB().Exec("UPDATE user SET cap='gio' WHERE login='nobody'"); err != nil { return err } counts := func() string { var b strings.Builder for _, table := range []string{"blob", "unsent", "unclustered", "private", "phantom"} { var n int clone.DB().QueryRow("SELECT count(*) FROM " + table).Scan(&n) fmt.Fprintf(&b, "%s=%d ", table, n) } return b.String() } before := counts() name, data, err := s.mint("origin") if err != nil { return err } if err := hzHold(clone, name, data, nil); err != nil { return err } if after := counts(); after != before { return expectf("holding the seed changed the store: %s, was %s", after, before) } var pc string clone.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&pc) for _, req := range [][]xfer.Card{ {&xfer.PullCard{ServerCode: "x", ProjectCode: pc}, &xfer.GimmeCard{UUID: name}}, {&xfer.CloneCard{Version: 3, SeqNo: 1}}, } { resp, err := sync.HandleSync(context.Background(), clone, &xfer.Message{Cards: append([]xfer.Card{ &xfer.PragmaCard{Name: "client-version", Values: []string{"22800", "20260101", "000000"}}, }, req...)}) if err != nil { return err } wire, err := resp.EncodeUncompressed() if err != nil { return err } if strings.Contains(string(wire), name) { return expectf("a reply to %T names the seed", req[len(req)-1]) } } if _, err := verify.Rebuild(clone); err != nil { return err } var n int clone.DB().QueryRow("SELECT count(*) FROM blob WHERE uuid=?", name).Scan(&n) if n != 0 { return expectf("the rebuild stored the seed as a blob") } return s.derivesDense(t, clone) } // hzSeedOrdinary: an artifact of the seed's shape arriving as an ordinary // artifact is stored and crosslinked as the control artifact it is, offered // to peers like any other, and never held as the seed. func hzSeedOrdinary(t *testing.T, s *hzSeed) error { clone := s.lacking(t) name, data, err := s.mint("origin") if err != nil { return err } var rid libfossil.FslID err = clone.WithTx(func(tx *db.Tx) error { var err error rid, _, err = blob.Store(tx, data) return err }) if err != nil { return err } if _, err := manifest.Crosslink(clone); err != nil { return err } var typ string var sources, unclustered int clone.DB().QueryRow("SELECT type FROM event WHERE objid=?", rid).Scan(&typ) clone.DB().QueryRow("SELECT count(*) FROM tagxref WHERE srcid=?", rid).Scan(&sources) clone.DB().QueryRow("SELECT count(*) FROM unclustered WHERE rid=?", rid).Scan(&unclustered) if typ != "g" || sources == 0 || unclustered != 1 { return expectf("artifact %.10s: event %q, %d tag rows sourced, unclustered %d; want an ordinary control artifact", name, typ, sources, unclustered) } held, err := seed.Held(clone.DB()) if err != nil || held != nil { return expectf("an ordinary artifact was held as the seed (%v)", err) } return nil } // hzSeedProvenance: a clone with no key for its origin holds an unsigned // seed; one that knows the key holds no seed the key does not verify, and // discards a held seed that stops verifying, falling back to the content // horizon's derivation. func hzSeedProvenance(t *testing.T, s *hzSeed) error { clone := s.lacking(t) name, data, err := s.mint("origin") if err != nil { return err } unsigned := errors.New("no signature by the origin's key") reject := func([]byte) error { return unsigned } if err := hzHold(clone, name, data, reject); !errors.Is(err, seed.ErrProvenance) { return expectf("Hold under a rejecting key: %v, want ErrProvenance", err) } if held, _ := seed.Held(clone.DB()); held != nil { return expectf("a seed the key rejected is held") } if err := hzHold(clone, name, data, nil); err != nil { return expectf("Hold with no key refused an unsigned seed: %v", err) } var applied bool err = clone.WithTx(func(tx *db.Tx) error { var err error applied, err = seed.Apply(tx, reject) return err }) if err != nil { return err } if held, _ := seed.Held(clone.DB()); applied || held != nil { return expectf("a held seed the key rejected was applied (%v) or kept", applied) } if s.derivesDense(t, clone) == nil { return expectf("the discarded seed's state survived") } return nil } // TestHorizonSeedZit has zit 0.6.4 mint a seed for the same repository and // compares it with go-libfossil's: go-libfossil serves hzSeed, zit clones it // densely and serves that clone, and a second zit clone at C's structural // horizon receives the seed. zit writes D to the second and names its login // zit-serve; go-libfossil writes D to the millisecond. The T and U lines // must be byte-identical and D must name the same instant; each // implementation must then derive from the other's seed what it derives // from its own. func TestHorizonSeedZit(t *testing.T) { bin := zitBinary(t) s := newHZSeed(t) dir := t.TempDir() home := filepath.Join(dir, "home") if err := os.Mkdir(home, 0o755); err != nil { t.Fatal(err) } srv := httptest.NewServer(xferHandler(t, s.f.r, func(*xfer.Message) {})) defer srv.Close() dense := filepath.Join(dir, "dense") zitRun(t, bin, home, dir, "clone", srv.URL, dense, "--no-open", "--once") url := zitServe(t, bin, home, dense) c := hzUUID(t, s.f.r, s.c3) structural := filepath.Join(dir, "structural") zitRun(t, bin, home, dir, "clone", url, structural, "--no-open", "--once", "--horizon="+c, "--structural") zitSeed, err := os.ReadFile(filepath.Join(structural, ".zit-cache", "horizon-seed")) if err != nil { t.Fatalf("zit held no seed: %v", err) } _, goSeed, err := s.mint("zit-serve") if err != nil { t.Fatal(err) } lines := func(data []byte, prefix string) []string { var out []string for _, l := range strings.Split(string(data), "\n") { if strings.HasPrefix(l, prefix) { out = append(out, l) } } return out } for _, card := range []string{"T ", "U "} { if z, g := lines(zitSeed, card), lines(goSeed, card); !slices.Equal(z, g) { t.Errorf("%scards differ:\nzit %q\ngo %q", card, z, g) } } zd, err := deck.Parse(zitSeed) if err != nil { t.Fatal(err) } gd, err := deck.Parse(goSeed) if err != nil { t.Fatal(err) } if !zd.D.Equal(gd.D) { t.Errorf("D cards differ: zit %v, go %v", zd.D, gd.D) } // zit derives from go-libfossil's seed what it derives from its own: // feat2's branch and closed leaf, and the branch list. Without a seed // it derives neither, so the comparison shows the seed applied. sidecar := filepath.Join(structural, ".zit-cache", "horizon-seed") feat2 := hzUUID(t, s.f.r, s.feat2) derived := func() string { return zitRun(t, bin, home, structural, "info", feat2) + zitRun(t, bin, home, structural, "branch") } own := derived() if err := os.Remove(sidecar); err != nil { t.Fatal(err) } if none := derived(); none == own { t.Fatalf("zit derives the same state without its seed:\n%s", none) } if err := os.WriteFile(sidecar, goSeed, 0o644); err != nil { t.Fatal(err) } if fromGo := derived(); fromGo != own { t.Errorf("zit from go-libfossil's seed:\n%s\nfrom its own:\n%s", fromGo, own) } clone := s.lacking(t) if err := hzHold(clone, hash.SHA3(zitSeed), zitSeed, nil); err != nil { t.Fatal(err) } if err := s.derivesDense(t, clone); err != nil { t.Errorf("zit's seed applied by go-libfossil: %v", err) } }