Files
ragflow/internal/service/file/file_commit_page_test.go

282 lines
8.0 KiB
Go

package file
import (
"context"
"strconv"
"strings"
"sync"
"testing"
"ragflow/internal/dao"
"ragflow/internal/entity"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func newPageCommitTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{TranslateError: true})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
// Keep a single connection so the :memory: database is shared across all
// goroutines (sqlite :memory: is otherwise per-connection); this also
// serializes the concurrent page-commit test through one connection.
if sqlDB, serr := db.DB(); serr == nil {
sqlDB.SetMaxOpenConns(1)
sqlDB.SetMaxIdleConns(1)
}
if err := db.AutoMigrate(&entity.FileCommit{}, &entity.FileCommitItem{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}
old := dao.DB
dao.DB = db
t.Cleanup(func() { dao.DB = old })
return db
}
func TestRecordPageEdit_CreatesCommitAndItem(t *testing.T) {
newPageCommitTestDB(t)
svc := NewFileCommitService()
ctx := context.Background()
in := PageEditCommitInput{
DatasetID: "kb1",
DocID: "wiki/page-a",
Slug: "page-a",
PageType: "wiki",
Title: "First edit",
AuthorID: "u1",
OldContent: "hello world",
NewContent: "hello world, edited",
}
commit, err := svc.RecordPageEdit(ctx, in)
if err != nil {
t.Fatalf("RecordPageEdit: %v", err)
}
if commit.ID == "" {
t.Fatal("expected a generated commit id")
}
if commit.AuthorID != "u1" || commit.Message != "First edit" || commit.FileCount != 1 {
t.Fatalf("unexpected commit: %+v", commit)
}
if commit.FolderID != "kb1" {
t.Fatalf("expected folder_id kb1 for dataset scope, got %s", commit.FolderID)
}
if commit.ParentID != nil {
t.Fatalf("first edit should have no parent, got %v", *commit.ParentID)
}
var items []entity.FileCommitItem
if err := dao.DB.Where("commit_id = ?", commit.ID).Find(&items).Error; err != nil {
t.Fatalf("load items: %v", err)
}
if len(items) != 1 {
t.Fatalf("expected 1 item, got %d", len(items))
}
it := items[0]
if it.Operation != "modify" {
t.Fatalf("expected operation modify, got %s", it.Operation)
}
if it.FileID != "kb1/wiki/page-a" {
t.Fatalf("expected file_id kb1/wiki/page-a (dataset-scoped), got %s", it.FileID)
}
if it.SlugKwd == nil || *it.SlugKwd != "page-a" {
t.Fatalf("expected slug_kwd page-a, got %v", it.SlugKwd)
}
if it.PageTypeKwd == nil || *it.PageTypeKwd != "wiki" {
t.Fatalf("expected page_type_kwd wiki, got %v", it.PageTypeKwd)
}
if it.Diff == nil || !strings.Contains(*it.Diff, "hello world, edited") {
t.Fatalf("expected diff containing new content, got %v", it.Diff)
}
if it.ContentAfterStorage == nil || *it.ContentAfterStorage != "es" {
t.Fatalf("expected content_after_storage es, got %v", it.ContentAfterStorage)
}
if it.ContentAfterLocation == nil || *it.ContentAfterLocation != "wiki/page-a" {
t.Fatalf("expected content_after_location wiki/page-a, got %v", it.ContentAfterLocation)
}
}
func TestRecordPageEdit_SecondEditLinksParent(t *testing.T) {
newPageCommitTestDB(t)
svc := NewFileCommitService()
ctx := context.Background()
base := PageEditCommitInput{
DatasetID: "kb1",
DocID: "wiki/page-a",
Slug: "page-a",
PageType: "wiki",
Title: "first",
AuthorID: "u1",
}
if _, err := svc.RecordPageEdit(ctx, base); err != nil {
t.Fatalf("first RecordPageEdit: %v", err)
}
second := base
second.Title = "second"
second.OldContent = "hello"
second.NewContent = "hello world"
commit2, err := svc.RecordPageEdit(ctx, second)
if err != nil {
t.Fatalf("second RecordPageEdit: %v", err)
}
if commit2.ParentID == nil {
t.Fatal("second edit should link to the first commit as parent")
}
var first entity.FileCommit
if err := dao.DB.Where("title = ?", "first").First(&first).Error; err != nil {
t.Fatalf("load first commit: %v", err)
}
if *commit2.ParentID != first.ID {
t.Fatalf("parent id mismatch: got %s want %s", *commit2.ParentID, first.ID)
}
}
func TestRecordPageEdit_IsolatesDatasets(t *testing.T) {
newPageCommitTestDB(t)
svc := NewFileCommitService()
ctx := context.Background()
mk := func(datasetID string) PageEditCommitInput {
return PageEditCommitInput{
DatasetID: datasetID,
DocID: datasetID + "/wiki/page-a",
Slug: "page-a",
PageType: "wiki",
Title: datasetID + "-edit",
AuthorID: "u1",
OldContent: "old",
NewContent: "new",
}
}
if _, err := svc.RecordPageEdit(ctx, mk("kb1")); err != nil {
t.Fatalf("kb1 RecordPageEdit: %v", err)
}
if _, err := svc.RecordPageEdit(ctx, mk("kb2")); err != nil {
t.Fatalf("kb2 RecordPageEdit: %v", err)
}
// A second edit in kb1 must parent to kb1's own first commit, not kb2's.
kb1Again := mk("kb1")
kb1Again.Title = "kb1-edit-2"
kb1Again.OldContent = "old"
kb1Again.NewContent = "newer"
kb1Commit2, err := svc.RecordPageEdit(ctx, kb1Again)
if err != nil {
t.Fatalf("kb1 second RecordPageEdit: %v", err)
}
if kb1Commit2.ParentID == nil {
t.Fatal("kb1 second edit should have a parent")
}
var kb1First, kb2First entity.FileCommit
if err := dao.DB.Where("title = ?", "kb1-edit").First(&kb1First).Error; err != nil {
t.Fatalf("load kb1 first commit: %v", err)
}
if err := dao.DB.Where("title = ?", "kb2-edit").First(&kb2First).Error; err != nil {
t.Fatalf("load kb2 first commit: %v", err)
}
if *kb1Commit2.ParentID != kb1First.ID {
t.Fatalf("kb1 parent mismatch: got %s want %s", *kb1Commit2.ParentID, kb1First.ID)
}
if *kb1Commit2.ParentID == kb2First.ID {
t.Fatal("kb1 parent must not cross into kb2 history")
}
}
func TestRecordPageEdit_ConcurrentEditsFormLinearChain(t *testing.T) {
newPageCommitTestDB(t)
svc := NewFileCommitService()
ctx := context.Background()
const edits = 8
var wg sync.WaitGroup
errs := make([]error, edits)
for i := 0; i < edits; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
in := PageEditCommitInput{
DatasetID: "kb1",
DocID: "wiki/page-a",
Slug: "page-a",
PageType: "wiki",
Title: "edit-" + strconv.Itoa(idx),
AuthorID: "u1",
OldContent: "old",
NewContent: "new-" + strconv.Itoa(idx),
}
_, errs[idx] = svc.RecordPageEdit(ctx, in)
}(i)
}
wg.Wait()
for i, e := range errs {
if e != nil {
t.Fatalf("edit %d failed: %v", i, e)
}
}
var commits []entity.FileCommit
if err := dao.DB.Where("folder_id = ?", "kb1").Order("create_time ASC").Find(&commits).Error; err != nil {
t.Fatalf("load commits: %v", err)
}
if len(commits) != edits {
t.Fatalf("expected %d commits, got %d", edits, len(commits))
}
// Every commit except the first must have a parent, and the parents must
// form a linear chain (no two commits share the same parent).
seenParents := map[string]bool{}
for i, c := range commits {
if i == 0 {
if c.ParentID != nil {
t.Fatalf("first commit should have no parent")
}
continue
}
if c.ParentID == nil {
t.Fatalf("commit %s should have a parent", c.ID)
}
if seenParents[*c.ParentID] {
t.Fatalf("two commits share parent %s -> forked chain", *c.ParentID)
}
seenParents[*c.ParentID] = true
}
}
func TestUnifiedDiff_EmptyWhenNoChange(t *testing.T) {
if d := unifiedDiff("same", "same"); d != "" {
t.Fatalf("expected empty diff for identical text, got %q", d)
}
}
func TestUnifiedDiff_DetectsAddition(t *testing.T) {
d := unifiedDiff("a\nb\nc\n", "a\nb\nc\nd\n")
if !strings.Contains(d, "+d") {
t.Fatalf("expected diff to contain added line, got %q", d)
}
}
func TestUnifiedDiff_DetectsRemoval(t *testing.T) {
d := unifiedDiff("a\nb\nc\n", "a\nc\n")
if !strings.Contains(d, "-b") {
t.Fatalf("expected diff to contain removed line, got %q", d)
}
}
func TestUnifiedDiff_TruncatesLongDiff(t *testing.T) {
oldLines := make([]string, 50)
newLines := make([]string, 50)
for i := range oldLines {
oldLines[i] = "old-line"
newLines[i] = "new-line"
}
d := unifiedDiff(strings.Join(oldLines, "\n"), strings.Join(newLines, "\n"))
if !strings.Contains(d, "... ") || !strings.Contains(d, "lines omitted") {
t.Fatalf("expected long diff to be truncated, got %q", d)
}
}