Artifact 378b4fceafc26976bec17209e1270194ca2ab754e02c5ae18669de8568524141:
- File
internal/manifest/dephantomize_defer_test.go
— part of check-in
[08820292b8]
at
2026-10-19 20:20:36
on branch perf/tag-repair-batch
— perf(manifest): bound the repair gate's tag propagation by the batch
repairTagPropagation re-propagated tags from every primary parent in the repository at every gate, so the gate at the end of a sweep, clone or sync cost the repository, not the batch. The batch now records the primary parents it writes plink rows for, and the new parents of a reparent, in a set beside its leaf schedule; the set is marked, rolled back and reset with the schedule, and the gate propagates from it alone. A rebuild links every check-in, so its set is every primary parent and nothing changes there.
A phantom-fill cascade's parents are known only to the cascade, so each cascade batch runs the gate itself before it commits rather than leaving propagation to the end of the sync.
Tracked in #432. (user: [email protected], size: 5791) [annotate] [blame] [check-ins using] [more...]
package manifest import ( "context" "testing" "time" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" _ "github.com/danmestas/go-libfossil/internal/testdriver" ) // TestAfterDephantomizeCascadeDefersCheckinWithMissingFileBlob (issue #180): // the mid-round phantom-fill cascade must hold back a Checkin manifest whose // referenced file blob has not arrived -- more blobs are still on the way -- // so a later Crosslink sweep can still rediscover and complete it. Unlike the // cascade, that sweep never waits on files (#406; see // TestCrosslink_LinksCheckinWithMissingFileBlob). // // Before the fix, afterDephantomize called linkArtifact for a Checkin // unconditionally: it wrote the event row even though insertCheckinMlinks // silently skipped the missing-blob F-card. Because // collectCrosslinkCandidates only reselects a rid with NO event row, that // half-linked checkin was permanently stranded -- no future sweep, no matter // how many rounds ran or when the blob arrived, would ever revisit it. A // downstream Checkout.Update walking to that checkin would hit "blob not // found" with no path to recover. func TestAfterDephantomizeCascadeDefersCheckinWithMissingFileBlob(t *testing.T) { r := setupTestRepo(t) fileContent := []byte("cascade race: file content arrives later") fileUUID := repoHash(r, fileContent) // A Checkin manifest referencing fileUUID, stored as full (non-delta) // content -- what storeResolvedContent produces for a "file" card, and // exactly the dephantomizedRid that storeReceivedFile then hands to // manifest.AfterDephantomize. The file blob is deliberately never stored. d := &deck.Deck{ Type: deck.Checkin, C: "cascade race with missing file blob", U: deck.User("tester"), D: time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC), F: []deck.FileCard{{Name: "race/file.txt", UUID: fileUUID}}, // A root check-in declares its branch -- both fossil's own `init` and // this package's Checkin do -- and leaf only ever considers a check-in // that carries a branch tag. See leaf.Rebuild. T: []deck.TagCard{ {Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: "trunk"}, {Type: deck.TagSingleton, Name: "sym-trunk", UUID: "*"}, }, } manifestBytes, err := d.Marshal() if err != nil { t.Fatalf("Marshal: %v", err) } manifestRID, _, err := blob.Store(r.DB(), manifestBytes) if err != nil { t.Fatalf("blob.Store(manifest): %v", err) } // Drive the manifest through the cascade exactly as // storeReceivedFile -> manifest.AfterDephantomize does for a just-stored // full-content blob. The file blob it references is missing, so this must // defer: no event/leaf/mlink rows at all. AfterDephantomize(context.Background(), r, manifestRID) assertCounts(t, r, manifestRID, 0, 0, 0, "after cascade, blob missing") // The missing file blob arrives (a later round). The next Crosslink sweep // -- HandleSync's post-round trigger, or sync.Clone's terminal sweep -- // must rediscover the manifest (no event row exists yet) and link it // fully. This half is what proves recovery is actually restored: a defer // that never gets picked back up would still pass the assertion above. if _, _, err := blob.Store(r.DB(), fileContent); err != nil { t.Fatalf("blob.Store(file): %v", err) } linked, err := Crosslink(r) if err != nil { t.Fatalf("Crosslink (post-arrival phase): %v", err) } if linked != 1 { t.Errorf("Crosslink (post-arrival phase): linked = %d, want 1", linked) } assertCounts(t, r, manifestRID, 1, 1, 1, "after blob arrival") } // TestAfterDephantomizeLinksOrphanWhenBaselineArrives: a delta manifest whose // baseline is absent is refused and filed in orphan under the baseline's // phantom, as fossil's fetch_baseline files it; when the baseline arrives, the // phantom-fill cascade finds it there, links it and clears the row. func TestAfterDephantomizeLinksOrphanWhenBaselineArrives(t *testing.T) { r := setupTestRepo(t) baseline := &deck.Deck{ Type: deck.Checkin, C: "baseline arrives later", D: time.Date(2026, 7, 27, 11, 0, 0, 0, time.UTC), U: deck.User("tester"), T: []deck.TagCard{ {Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: "trunk"}, {Type: deck.TagSingleton, Name: "sym-trunk", UUID: "*"}, }, } baselineBytes, err := baseline.Marshal() if err != nil { t.Fatalf("Marshal(baseline): %v", err) } baselineUUID := repoHash(r, baselineBytes) delta := &deck.Deck{ Type: deck.Checkin, B: baselineUUID, C: "delta before its baseline", D: time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC), P: []string{baselineUUID}, U: deck.User("tester"), } deltaBytes, err := delta.Marshal() if err != nil { t.Fatalf("Marshal(delta): %v", err) } deltaRID, _, err := blob.Store(r.DB(), deltaBytes) if err != nil { t.Fatalf("blob.Store(delta): %v", err) } AfterDephantomize(context.Background(), r, deltaRID) assertCounts(t, r, deltaRID, 0, 0, 0, "refused without its baseline") baseRID := assertPhantom(t, r, baselineUUID, true) assertOrphan(t, r, int64(deltaRID), baseRID) if _, _, err := blob.Store(r.DB(), baselineBytes); err != nil { t.Fatalf("blob.Store(baseline): %v", err) } AfterDephantomize(context.Background(), r, libfossil.FslID(baseRID)) // The cascade's own gate propagates the baseline's branch to the delta and // applies the recheck that schedules, so the delta is already a leaf. assertCounts(t, r, deltaRID, 1, 1, 0, "linked once the baseline arrived") var orphans int if err := r.DB().QueryRow("SELECT count(*) FROM orphan").Scan(&orphans); err != nil || orphans != 0 { t.Errorf("orphan rows = %d, %v; want 0", orphans, err) } }