Artifact 61006b9a5d5d354c521e7aaf086da2b240cd09ba6c1f769a278c4e196371a510:
- File
internal/verify/ucard_presence_test.go
— part of check-in
[33959a6e3e]
at
2026-10-12 17:34:38
on branch main
— fix(verify): derive Rebuild state through the crosslink sweep (#285) (#336)
verify.Rebuild kept its own partial derivation of repository state: it wrote event/plink/mlink/tagxref rows only for check-ins and control artifacts, applied tags from artifacts the crosslink would refuse, skipped absent tag targets and parents instead of reserving phantoms, wrote no plink baseid, and computed leaves as "no child at all" regardless of branch. Against fossil rebuild it missed hundreds of event, plink, tagxref and leaf rows on real repositories.
Make Rebuild empty the crosslink-derived tables and run the same sweep a clone runs, so derived state has a single owner. The three partial derivation files (rebuild_tags.go, rebuild_manifest.go, rebuild_leaves.go) are deleted; Rebuild keeps only what the sweep does not do -- blob hash verification and the unclustered/unsent bookkeeping.
A rebuild is terminal: nothing more will arrive, so a check-in whose file or baseline content is absent must be linked with phantoms rather than deferred for a blob that never comes, as fossil's add_one_mlink does via uuid_to_rid(...,1). Add manifest.CrosslinkRebuild, a sweep that does not defer; on a complete repository it is identical to Crosslink. Rebuild's leaf rule now follows fossil's branch-aware sweep, so an untagged parentless check-in is no longer a leaf -- the same Section 7.3 deviation the sweep and per-artifact paths already carry. (user: [email protected], size: 3111) [annotate] [blame] [check-ins using]
package verify_test import ( "crypto/md5" "database/sql" "fmt" "testing" "github.com/danmestas/go-libfossil/internal/blob" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/verify" ) // rawCheckinManifest hand-builds a minimal, valid Checkin manifest so the // test controls the U-card line byte-for-byte — something deck.Deck can't // express for the present-but-empty case, since Marshal deliberately never // emits an empty U-card. userLine must be "" (card omitted entirely), // "U \n" (card present, empty), or "U <name>\n" (card present with a // value). Mirrors manifest.rawCheckinManifest in // internal/manifest/ucard_presence_test.go — duplicated here rather than // exported since it's test-only fixture, not production surface. func rawCheckinManifest(userLine string) []byte { body := "D 2024-01-15T10:30:00.000\nR d41d8cd98f00b204e9800998ecf8427e\n" + userLine h := md5.Sum([]byte(body)) return []byte(fmt.Sprintf("%sZ %x\n", body, h)) } // TestRebuild_UCardPresenceStates covers the rebuild path with the same // three-state U-card coverage as manifest.TestCrosslink_UCardPresenceStates. // verify.Rebuild now derives event through manifest.Crosslink, so the U-card // binding is crosslink's; this exercises it from the rebuild entry point so a // future edit that collapses SQL NULL back into "" on either path is caught. func TestRebuild_UCardPresenceStates(t *testing.T) { r := newTestRepo(t) absentRID, _, err := blob.Store(r.DB(), rawCheckinManifest("")) if err != nil { t.Fatalf("blob.Store(absent): %v", err) } emptyRID, _, err := blob.Store(r.DB(), rawCheckinManifest("U \n")) if err != nil { t.Fatalf("blob.Store(empty): %v", err) } valuedRID, _, err := blob.Store(r.DB(), rawCheckinManifest("U alice\n")) if err != nil { t.Fatalf("blob.Store(valued): %v", err) } // Rebuild empties the derived tables and reconstructs event (and friends) // from raw blob content through the crosslink sweep. if _, err := verify.Rebuild(r); err != nil { t.Fatalf("verify.Rebuild: %v", err) } assertRebuiltEventUser(t, r, absentRID, sql.NullString{Valid: false}, "absent U-card") assertRebuiltEventUser(t, r, emptyRID, sql.NullString{String: "anonymous", Valid: true}, "present-empty U-card") assertRebuiltEventUser(t, r, valuedRID, sql.NullString{String: "alice", Valid: true}, "present-valued U-card") } // assertRebuiltEventUser reads the event.user column for objid=rid and // compares it against want, distinguishing SQL NULL (Valid=false) from any // string value (including the empty string) so a regression that collapses // NULL back into "" is caught. func assertRebuiltEventUser(t *testing.T, r *repo.Repo, rid libfossil.FslID, want sql.NullString, label string) { t.Helper() var got sql.NullString if err := r.DB().QueryRow("SELECT user FROM event WHERE objid=?", rid).Scan(&got); err != nil { t.Fatalf("%s: query event.user: %v", label, err) } if got != want { t.Fatalf("%s: event.user = %+v, want %+v", label, got, want) } }