Artifact 04251007ab93a420456ca6e8f00301be623d63ccc2674a6ece245f42b7fed2be:
- File
internal/tag/tag_leaf_test.go
— part of check-in
[628f932301]
at
2026-10-13 16:51:51
on branch main
— fix(manifest): maintain the leaf table incrementally (RS-7.3-3) (#434)
* fix(manifest): maintain the leaf table incrementally (RS-7.3-3)
The leaf table was recomputed from scratch at the end of every crosslink batch, so a check-in lost its leaf verdict whenever a later batch linked an untagged child to it. repo-state-01 Section 7.3, as corrected, and fossil keep that verdict: a check-in and its plink parents are rechecked only when a branch tag is written to it or propagation updates or deletes its branch row (src/tag.c:195, :100-102), and the checks run when the batch ends (src/leaf.c leaf_do_pending_checks).
The rule now lives in a new internal/leaf package: Check (leaf_check), Rebuild (the explicit full rebuild) and Pending (the deferred schedule). internal/tag schedules a recheck at exactly the branch-row writes; the crosslink sweep, receive linker and repair gate apply a batch's schedule at the gate, each phantom-fill cascade transaction and each commit or tag transaction apply their own before commit, and verify.Rebuild keeps the whole-table recompute. Reparenting schedules nothing, as in fossil, so the reparent case is split into a one-batch case and a per-artifact case.
Tracked in #426.
* fix(manifest): recheck leaves a cancel reaches; drop rechecks of a failed receive tx (user: [email protected], size: 2816) [annotate] [blame] [check-ins using]
package tag_test import ( "slices" "testing" "time" 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/tag" ) func leafRids(t *testing.T, r *repo.Repo) []libfossil.FslID { t.Helper() rows, err := r.DB().Query("SELECT rid FROM leaf ORDER BY rid") if err != nil { t.Fatal(err) } defer rows.Close() var out []libfossil.FslID for rows.Next() { var rid int64 if err := rows.Scan(&rid); err != nil { t.Fatal(err) } out = append(out, libfossil.FslID(rid)) } return out } // TestAddBranchTagRechecksLeaves pins draft-fossil-repo-state-01 Sections 7.3 // and 7.6: a branch tag applied by a tag control artifact rechecks the // check-in and its parent before the transaction commits. Moving c2 onto // feat leaves c1, on trunk, without a same-branch child, so c1 is a leaf // again; c2 stays one. func TestAddBranchTagRechecksLeaves(t *testing.T) { r := setupTestRepo(t) at := time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC) commit := func(label string, parent libfossil.FslID, n int) libfossil.FslID { t.Helper() rid, _, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{{Name: "f.txt", Content: []byte(label)}}, Comment: label, User: "testuser", Parent: parent, Time: at.Add(time.Duration(n) * time.Minute), }) if err != nil { t.Fatalf("Checkin(%s): %v", label, err) } return rid } c1 := commit("c1", 0, 0) c2 := commit("c2", c1, 1) if got := leafRids(t, r); !slices.Equal(got, []libfossil.FslID{c2}) { t.Fatalf("before: leaves = %v, want [%d]", got, c2) } if _, err := tag.AddTag(r, tag.TagOpts{ TargetRID: c2, TagName: "branch", TagType: tag.TagPropagating, Value: "feat", User: "testuser", Time: at.Add(time.Hour), }); err != nil { t.Fatalf("AddTag: %v", err) } if got := leafRids(t, r); !slices.Equal(got, []libfossil.FslID{c1, c2}) { t.Fatalf("after: leaves = %v, want [%d %d]", got, c1, c2) } } // TestApplyTagWithTxBranchNeedsLeaves pins the programmer-error contract: a // branch tag applied on a caller's transaction must name the batch's leaf // schedule, or its rechecks would be lost. func TestApplyTagWithTxBranchNeedsLeaves(t *testing.T) { r := setupTestRepo(t) rid, _, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{{Name: "f.txt", Content: []byte("x")}}, Comment: "c1", User: "testuser", }) if err != nil { t.Fatal(err) } defer func() { if recover() == nil { t.Fatal("ApplyTagWithTx with a branch tag and no Leaves did not panic") } }() _ = tag.ApplyTagWithTx(r.DB(), tag.ApplyOpts{ TargetRID: rid, SrcRID: rid, TagName: "branch", TagType: tag.TagSingleton, Value: "x", MTime: 2470000, }) }