Artifact 1bc499aa1dafc37e59ce94e18759a04c87d9f53ab8ce2f610e3f05d24a69d318:
- File
conformance/horizon_pushcheck_zit_test.go
— part of check-in
[8f89198ca1]
at
2026-10-22 18:00:28
on branch feat/horizon-cones
— feat(sync): refuse a push whose branch cancellation names a stale seed
A check-in that creates a branch cancels its parent's branch tag by the name its clone derived, and under a structural horizon that name came from the seed. Section 5.7 has the origin check it against its own derivation: the handler now looks at each check-in a push stored, once crosslinked, and when the parent is on another branch here it rolls the whole push back with an error naming the stale seed and the parent's branch, spelled as zit's pushSeedCancelFault spells it. A check-in is admitted when the push or the store holds a control artifact cancelling the parent's real branch on it, and so is one whose parent this origin cannot place.
zit's check reads only the push for the correction; this one also reads the store, as the draft allows. Interop runs both ways against zit 9bf7ebfe0f. (user: [email protected], size: 5588) [annotate] [blame] [check-ins using] [more...]
package conformance import ( "context" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/danmestas/go-libfossil/internal/branch" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "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/simio" ) // TestZitHorizonPushCheckInterop runs the push-time cancellation check of // draft-fossil-horizon-00 ยง5.7 between go-libfossil and zit both ways over // trunk c1 -> c4. Each client creates a branch from c4 believing c4 is on trunk // after the origin moved c4 to branch moved, so its check-in cancels // sym-trunk: zit's push to go-libfossil's handler and go-libfossil's push to // `zit serve` are both refused, naming the stale seed and the parent's // branch. go-libfossil then pushes the correcting control artifact with the // check-in, and zit admits it. func TestZitHorizonPushCheckInterop(t *testing.T) { bin := zitHorizonBinary(t) // zit 9bf7ebfe0f's commit asserts on a SHA1-named tree // (src/cmd/checkout.zig:3165), so this origin names by SHA3. origin, err := repo.Create(filepath.Join(t.TempDir(), "origin.fossil"), "u", simio.CryptoRand{}, "") if err != nil { t.Fatal(err) } defer origin.Close() if _, err := origin.DB().Exec("UPDATE user SET cap='gio' WHERE login='nobody'"); err != nil { t.Fatal(err) } when := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) commit := func(name string, parent libfossil.FslID) libfossil.FslID { when = when.Add(time.Hour) rid, _, err := manifest.Checkin(origin, manifest.CheckinOpts{Files: []manifest.File{{Name: name, Content: []byte(name + "\n")}}, Comment: name, User: "u", Parent: parent, Time: when}) if err != nil { t.Fatal(err) } return rid } c4rid := commit("c4", commit("c1", 0)) c4 := hzUUID(t, origin, c4rid) dir := t.TempDir() home := filepath.Join(dir, "home") if err := os.Mkdir(home, 0o755); err != nil { t.Fatal(err) } server := httptest.NewServer(&hzRecorder{r: origin}) defer server.Close() // Every client takes its copy while c4 is still on trunk. zc := filepath.Join(dir, "zcheckout") zitRun(t, bin, home, dir, "clone", server.URL, zc) zitRun(t, bin, home, zc, "update", "--no-sync", c4) zd := filepath.Join(dir, "zdense") zitRun(t, bin, home, dir, "clone", server.URL, zd, "--no-open", "--once") peer := &sync.HTTPTransport{URL: zitServe(t, bin, home, zd)} r, _, err := sync.Clone(context.Background(), filepath.Join(dir, "dense.fossil"), peer, sync.CloneOpts{}) if err != nil { t.Fatal(err) } defer r.Close() for _, m := range []struct { name string typ int value string }{{"branch", tag.TagPropagating, "moved"}, {"sym-moved", tag.TagPropagating, ""}, {"sym-trunk", tag.TagCancel, ""}} { when = when.Add(time.Hour) if _, err := tag.AddTag(origin, tag.TagOpts{TargetRID: c4rid, TagName: m.name, TagType: m.typ, Value: m.value, User: "u", Time: when}); err != nil { t.Fatal(err) } } zitRun(t, bin, home, zd, "pull", server.URL) // zit pushes to go-libfossil. if err := os.WriteFile(filepath.Join(zc, "nb"), []byte("nb\n"), 0o644); err != nil { t.Fatal(err) } zitRun(t, bin, home, zc, "add", "nb") zitRun(t, bin, home, zc, "commit", "--no-sync", "--branch", "nb", "-m", "nb") before := hzCheckins(t, origin) out, err := zitCommand(bin, home, zc, "push", server.URL) if !strings.Contains(out, "stale seed") || !strings.Contains(out, "'moved'") { t.Fatalf("zit's push of a stale cancellation to go-libfossil: %v\n%s", err, out) } if after := hzCheckins(t, origin); after != before { t.Fatalf("go-libfossil stored the refused push: %d check-ins, was %d", after, before) } // go-libfossil pushes to zit. rc4, err := hzRID(r, c4) if err != nil { t.Fatal(err) } when = when.Add(24 * time.Hour) nb, _, err := branch.Create(r, branch.CreateOpts{Name: "nb", Parent: rc4, User: "u", Time: when}) if err != nil { t.Fatal(err) } res, err := sync.Sync(context.Background(), r, peer, sync.SyncOpts{Push: true}) if !hzRefusedStale(err, res, "'moved'") { t.Fatalf("go-libfossil's push of a stale cancellation to zit: %v %+v", err, res) } nbUUID := hzUUID(t, r, nb) if out, err := zitCommand(bin, home, zd, "info", nbUUID); err == nil { t.Fatalf("zit stored the refused check-in:\n%s", out) } if _, err := tag.AddTag(r, tag.TagOpts{TargetRID: nb, TagName: "sym-moved", TagType: tag.TagCancel, User: "u", Time: when.Add(time.Hour)}); err != nil { t.Fatal(err) } res, err = sync.Sync(context.Background(), r, peer, sync.SyncOpts{Push: true}) if err != nil || len(res.Errors) != 0 { t.Fatalf("go-libfossil's push with the correction: %v %q", err, res.Errors) } zitRun(t, bin, home, zd, "info", nbUUID) } // hzCheckins counts the check-ins r holds. func hzCheckins(t *testing.T, r *repo.Repo) int { t.Helper() var n int if err := r.DB().QueryRow("SELECT count(*) FROM event WHERE type='ci'").Scan(&n); err != nil { t.Fatal(err) } return n } // hzRefusedStale reports a sync refused with a stale-seed diagnostic naming // want, whether it surfaced as the error or among the result's errors. func hzRefusedStale(err error, res *sync.SyncResult, want string) bool { var texts []string if err != nil { texts = append(texts, err.Error()) } if res != nil { texts = append(texts, res.Errors...) } all := strings.Join(texts, "\n") return strings.Contains(all, "stale seed") && strings.Contains(all, want) }