Artifact e2ee9c8d6e1b987b30700c0f0e0fbc59bfdd60a385e54a8f0e64029b192ece49:
- File
internal/manifest/finfo_test.go
— part of check-in
[04f41a4c9c]
at
2026-07-28 22:08:51
on branch trunk
— fix(manifest): unify mlink derivation (#216)
Route direct check-ins and internal rebuilds through canonical mlink derivation. Update sparse-history consumers and preserve legacy conflict-fork rows with an explicit RID discriminator migration.\n\nCloses #213 (user: [email protected], size: 11487) [annotate] [blame] [check-ins using]
package manifest import ( "bytes" "testing" "time" "github.com/danmestas/go-libfossil/internal/content" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" _ "github.com/danmestas/go-libfossil/internal/testdriver" ) func TestFileHistory_Basic(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) // Commit 1: add hello.txt rid1, _, err := Checkin(r, CheckinOpts{ Files: []File{{Name: "hello.txt", Content: []byte("v1")}}, Comment: "add hello", User: "alice", Time: ts, }) if err != nil { t.Fatal(err) } // Commit 2: modify hello.txt _, _, err = Checkin(r, CheckinOpts{ Files: []File{{Name: "hello.txt", Content: []byte("v2")}}, Comment: "update hello", User: "bob", Time: ts.Add(time.Hour), Parent: rid1, }) if err != nil { t.Fatal(err) } versions, err := FileHistory(r, FileHistoryOpts{Path: "hello.txt"}) if err != nil { t.Fatal(err) } if len(versions) != 2 { t.Fatalf("expected 2 versions, got %d", len(versions)) } // Most recent first if versions[0].Action != FileModified { t.Errorf("v[0] action = %v, want Modified", versions[0].Action) } if versions[0].User != "bob" { t.Errorf("v[0] user = %q, want bob", versions[0].User) } if versions[1].Action != FileAdded { t.Errorf("v[1] action = %v, want Added", versions[1].Action) } if versions[1].User != "alice" { t.Errorf("v[1] user = %q, want alice", versions[1].User) } } func TestFileHistory_MultipleFiles(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) rid1, _, _ := Checkin(r, CheckinOpts{ Files: []File{ {Name: "a.txt", Content: []byte("a1")}, {Name: "b.txt", Content: []byte("b1")}, }, Comment: "add both", User: "alice", Time: ts, }) // Only modify a.txt Checkin(r, CheckinOpts{ Files: []File{ {Name: "a.txt", Content: []byte("a2")}, {Name: "b.txt", Content: []byte("b1")}, }, Comment: "update a only", User: "bob", Time: ts.Add(time.Hour), Parent: rid1, }) histA, _ := FileHistory(r, FileHistoryOpts{Path: "a.txt"}) histB, _ := FileHistory(r, FileHistoryOpts{Path: "b.txt"}) if len(histA) != 2 { t.Fatalf("a.txt: expected 2 versions, got %d", len(histA)) } // b.txt should only show the initial add — mlink only records changes if len(histB) != 1 { t.Fatalf("b.txt: expected 1 version (add only, no change in commit 2), got %d", len(histB)) } } func TestFileHistory_Limit(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) rid1, _, _ := Checkin(r, CheckinOpts{ Files: []File{{Name: "f.txt", Content: []byte("v1")}}, Comment: "c1", User: "u", Time: ts, }) rid2, _, _ := Checkin(r, CheckinOpts{ Files: []File{{Name: "f.txt", Content: []byte("v2")}}, Comment: "c2", User: "u", Time: ts.Add(time.Hour), Parent: rid1, }) Checkin(r, CheckinOpts{ Files: []File{{Name: "f.txt", Content: []byte("v3")}}, Comment: "c3", User: "u", Time: ts.Add(2 * time.Hour), Parent: rid2, }) versions, _ := FileHistory(r, FileHistoryOpts{Path: "f.txt", Limit: 2}) if len(versions) != 2 { t.Fatalf("expected 2 versions with limit, got %d", len(versions)) } } func TestFileHistory_NotFound(t *testing.T) { r := setupTestRepo(t) _, err := FileHistory(r, FileHistoryOpts{Path: "nonexistent.txt"}) if err == nil { t.Fatal("expected error for nonexistent file") } } func TestFileHistory_EmptyPath(t *testing.T) { r := setupTestRepo(t) _, err := FileHistory(r, FileHistoryOpts{Path: ""}) if err == nil { t.Fatal("expected error for empty path") } } func TestFileHistory_FileUUID(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) Checkin(r, CheckinOpts{ Files: []File{{Name: "data.txt", Content: []byte("content")}}, Comment: "add data", User: "u", Time: ts, }) versions, _ := FileHistory(r, FileHistoryOpts{Path: "data.txt"}) if len(versions) != 1 { t.Fatalf("expected 1 version, got %d", len(versions)) } if versions[0].FileUUID == "" { t.Error("FileUUID should be set for non-deleted file") } if versions[0].CheckinUUID == "" { t.Error("CheckinUUID should be set") } if versions[0].FileRID <= 0 { t.Error("FileRID should be positive") } } func TestFileAt_Basic(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) rid1, _, _ := Checkin(r, CheckinOpts{ Files: []File{{Name: "hello.txt", Content: []byte("v1")}}, Comment: "c1", User: "u", Time: ts, }) rid2, _, _ := Checkin(r, CheckinOpts{ Files: []File{{Name: "hello.txt", Content: []byte("v2")}}, Comment: "c2", User: "u", Time: ts.Add(time.Hour), Parent: rid1, }) fid1, ok1 := FileAt(r, rid1, "hello.txt") fid2, ok2 := FileAt(r, rid2, "hello.txt") if !ok1 || fid1 <= 0 { t.Fatal("expected file at commit 1") } if !ok2 || fid2 <= 0 { t.Fatal("expected file at commit 2") } if fid1 == fid2 { t.Fatal("file rids should differ between versions") } } func TestFileAt_NotFound(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) rid1, _, _ := Checkin(r, CheckinOpts{ Files: []File{{Name: "hello.txt", Content: []byte("v1")}}, Comment: "c1", User: "u", Time: ts, }) _, ok := FileAt(r, rid1, "nonexistent.txt") if ok { t.Fatal("expected false for nonexistent file") } } func TestFileHistory_ThreeCommitChain(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) rid1, _, _ := Checkin(r, CheckinOpts{ Files: []File{{Name: "f.txt", Content: []byte("v1")}}, Comment: "first", User: "alice", Time: ts, }) rid2, _, _ := Checkin(r, CheckinOpts{ Files: []File{{Name: "f.txt", Content: []byte("v2")}}, Comment: "second", User: "bob", Time: ts.Add(time.Hour), Parent: rid1, }) Checkin(r, CheckinOpts{ Files: []File{{Name: "f.txt", Content: []byte("v3")}}, Comment: "third", User: "carol", Time: ts.Add(2 * time.Hour), Parent: rid2, }) versions, err := FileHistory(r, FileHistoryOpts{Path: "f.txt"}) if err != nil { t.Fatal(err) } if len(versions) != 3 { t.Fatalf("expected 3 versions, got %d", len(versions)) } // Verify ordering (most recent first) and comments wantUsers := []string{"carol", "bob", "alice"} wantComments := []string{"third", "second", "first"} wantActions := []FileAction{FileModified, FileModified, FileAdded} for i, v := range versions { if v.User != wantUsers[i] { t.Errorf("v[%d] user = %q, want %q", i, v.User, wantUsers[i]) } if v.Comment != wantComments[i] { t.Errorf("v[%d] comment = %q, want %q", i, v.Comment, wantComments[i]) } if v.Action != wantActions[i] { t.Errorf("v[%d] action = %v, want %v", i, v.Action, wantActions[i]) } } // Verify file content at each version via FileAt + content.Expand for i, v := range versions { fid, ok := FileAt(r, v.CheckinRID, "f.txt") if !ok { t.Fatalf("v[%d]: FileAt returned false", i) } data, err := content.Expand(r.DB(), fid) if err != nil { t.Fatalf("v[%d]: Expand: %v", i, err) } want := []byte("v" + string(rune('3'-i))) // v3, v2, v1 if !bytes.Equal(data, want) { t.Errorf("v[%d]: content = %q, want %q", i, data, want) } } } func TestFileHistory_CrosslinkGeneratedMlink(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) // Create two commits normally rid1, _, _ := Checkin(r, CheckinOpts{ Files: []File{{Name: "x.txt", Content: []byte("x1")}}, Comment: "add x", User: "u", Time: ts, }) _, _, _ = Checkin(r, CheckinOpts{ Files: []File{{Name: "x.txt", Content: []byte("x2")}}, Comment: "update x", User: "u", Time: ts.Add(time.Hour), Parent: rid1, }) // Wipe mlink and re-crosslink — this uses insertCheckinMlinks, which // since #89 diffs each check-in against its parent manifest and sets // pmid/pid from that manifest (previously it left pid=0 on every row). r.DB().Exec("DELETE FROM mlink") r.DB().Exec("DELETE FROM event") r.DB().Exec("DELETE FROM plink") r.DB().Exec("DELETE FROM leaf") r.DB().Exec("DELETE FROM filename") // Re-crosslink all manifests _, err := Crosslink(r) if err != nil { t.Fatalf("Crosslink: %v", err) } versions, err := FileHistory(r, FileHistoryOpts{Path: "x.txt"}) if err != nil { t.Fatalf("FileHistory after crosslink: %v", err) } // Only the second commit is re-crosslinked here: the wipe above leaves // tagxref intact, and the candidate query skips any blob that is a tagxref // source, which the tag-bearing first commit is. That single re-linked // commit modified x.txt relative to its parent, so with pid now sourced // from the parent manifest finfo classifies it as a modification. Before // #89 the row carried pid=0 and finfo mis-reported it as an add. if len(versions) != 1 { t.Fatalf("expected 1 version for x.txt after crosslink, got %d", len(versions)) } v := versions[0] if v.CheckinUUID == "" { t.Error("CheckinUUID should be set") } if v.Action != FileModified { t.Errorf("x.txt action = %v, want modified; pid must be set from the parent manifest, not default to 0 (which would classify as added)", v.Action) } } func TestFileHistory_MergeCanonicalAuxiliaryRows(t *testing.T) { r := setupTestRepo(t) mergeRID := branchAndMergeHistory(t, r) trunkHistory, err := FileHistory(r, FileHistoryOpts{Path: "trunk.txt"}) if err != nil { t.Fatalf("FileHistory(trunk.txt): %v", err) } mergeVersions := 0 for _, version := range trunkHistory { if version.CheckinRID == mergeRID { mergeVersions++ } } if mergeVersions != 1 { t.Fatalf("trunk.txt: expected exactly one version at merge %d, got %d", mergeRID, mergeVersions) } featureHistory, err := FileHistory(r, FileHistoryOpts{Path: "feature.txt"}) if err != nil { t.Fatalf("FileHistory(feature.txt): %v", err) } for _, version := range featureHistory { if version.CheckinRID == mergeRID { if version.Action != FileAdded { t.Errorf("feature.txt merge action = %v, want added", version.Action) } return } } t.Fatalf("feature.txt: merge version %d is absent", mergeRID) } func TestFileAt_WithContentVerification(t *testing.T) { r := setupTestRepo(t) ts := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) rid1, _, _ := Checkin(r, CheckinOpts{ Files: []File{ {Name: "a.txt", Content: []byte("alpha")}, {Name: "b.txt", Content: []byte("beta")}, }, Comment: "initial", User: "u", Time: ts, }) rid2, _, _ := Checkin(r, CheckinOpts{ Files: []File{ {Name: "a.txt", Content: []byte("alpha-v2")}, {Name: "b.txt", Content: []byte("beta")}, }, Comment: "update a", User: "u", Time: ts.Add(time.Hour), Parent: rid1, }) // Verify we can resolve and expand file content at each commit cases := []struct { rid libfossil.FslID path string want string }{ {rid1, "a.txt", "alpha"}, {rid1, "b.txt", "beta"}, {rid2, "a.txt", "alpha-v2"}, {rid2, "b.txt", "beta"}, } for _, tc := range cases { fid, ok := FileAt(r, tc.rid, tc.path) if !ok { t.Fatalf("FileAt(%d, %q) = false", tc.rid, tc.path) } data, err := content.Expand(r.DB(), fid) if err != nil { t.Fatalf("Expand(%d): %v", fid, err) } if string(data) != tc.want { t.Errorf("FileAt(%d, %q): content = %q, want %q", tc.rid, tc.path, data, tc.want) } } }