mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-01 13:33:48 +08:00
282 lines
8.0 KiB
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)
|
|
}
|
|
}
|