mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-16 20:57:21 +08:00
### Summary 1. refactor message processing 2. delete un-used componentIndexMap 3. unfold (delete) internal/ingestion/task/task_handler.go
751 lines
24 KiB
Go
751 lines
24 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"ragflow/internal/common"
|
|
"ragflow/internal/dao"
|
|
"ragflow/internal/entity"
|
|
)
|
|
|
|
type recordingTaskPublisher struct {
|
|
subject string
|
|
messages []common.TaskMessage
|
|
err error
|
|
}
|
|
|
|
func (p *recordingTaskPublisher) PublishTaskMessage(subject string, msg common.TaskMessage) error {
|
|
p.subject = subject
|
|
p.messages = append(p.messages, msg)
|
|
return p.err
|
|
}
|
|
|
|
func TestIngestionTaskServiceCreateForDocumentsPublishesTaskMessages(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestKB(t, "kb-1", "tenant-1", 1, 0, 0)
|
|
insertTestDoc(t, "doc-1", "kb-1", 0, 0)
|
|
|
|
publisher := &recordingTaskPublisher{}
|
|
svc := NewIngestionTaskService()
|
|
svc.taskPublisher = publisher
|
|
|
|
resp, err := svc.CreateForDocuments("kb-1", "user-1", []string{"doc-1"})
|
|
if err != nil {
|
|
t.Fatalf("CreateForDocuments failed: %v", err)
|
|
}
|
|
if len(resp) != 1 {
|
|
t.Fatalf("expected 1 response, got %d", len(resp))
|
|
}
|
|
if publisher.subject != "tasks.RAGFLOW" {
|
|
t.Fatalf("subject = %q, want %q", publisher.subject, "tasks.RAGFLOW")
|
|
}
|
|
if len(publisher.messages) != 1 {
|
|
t.Fatalf("expected 1 published message, got %d", len(publisher.messages))
|
|
}
|
|
msg := publisher.messages[0]
|
|
if msg.TaskType != common.TaskTypeIngestionTask {
|
|
t.Fatalf("task type = %q, want %q", msg.TaskType, common.TaskTypeIngestionTask)
|
|
}
|
|
task, err := dao.NewIngestionTaskDAO().GetByID(msg.TaskID)
|
|
if err != nil {
|
|
t.Fatalf("load task: %v", err)
|
|
}
|
|
if task.DocumentID != "doc-1" || task.DatasetID != "kb-1" || task.UserID != "user-1" {
|
|
t.Fatalf("unexpected task: %+v", task)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceListByUserFiltersDataset(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
insertTestIngestionTask(t, "task-2", "user-1", "doc-2", "kb-2")
|
|
insertTestIngestionTask(t, "task-3", "user-2", "doc-3", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
datasetID := "kb-1"
|
|
tasks, err := svc.ListByUser("user-1", &datasetID, 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("ListByUser failed: %v", err)
|
|
}
|
|
if len(tasks) != 1 {
|
|
t.Fatalf("expected 1 task, got %d", len(tasks))
|
|
}
|
|
if tasks[0].ID != "task-1" {
|
|
t.Fatalf("task ID = %q, want %q", tasks[0].ID, "task-1")
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceRequestStopManyStopsOwnedTasks(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
userID := "user-1"
|
|
svc := NewIngestionTaskService()
|
|
tasks, err := svc.RequestStopMany([]string{"task-1"}, &userID)
|
|
if err != nil {
|
|
t.Fatalf("RequestStopMany failed: %v", err)
|
|
}
|
|
if len(tasks) != 1 {
|
|
t.Fatalf("expected 1 task response, got %d", len(tasks))
|
|
}
|
|
if tasks[0].Status != common.STOPPED {
|
|
t.Fatalf("status = %q, want %q", tasks[0].Status, common.STOPPED)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceRequestStopManyRejectsOtherUsersTask(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
userID := "user-2"
|
|
svc := NewIngestionTaskService()
|
|
if _, err := svc.RequestStopMany([]string{"task-1"}, &userID); err == nil {
|
|
t.Fatal("expected RequestStopMany to reject non-owner")
|
|
}
|
|
task, err := dao.NewIngestionTaskDAO().GetByID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("load task: %v", err)
|
|
}
|
|
if task.Status != common.CREATED {
|
|
t.Fatalf("status = %q, want %q", task.Status, common.CREATED)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceRequestStopManyAllowsAdmin(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
tasks, err := svc.RequestStopMany([]string{"task-1"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("RequestStopMany admin failed: %v", err)
|
|
}
|
|
if len(tasks) != 1 {
|
|
t.Fatalf("expected 1 task response, got %d", len(tasks))
|
|
}
|
|
if tasks[0].Status != common.STOPPED {
|
|
t.Fatalf("status = %q, want %q", tasks[0].Status, common.STOPPED)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceRemoveManyRemovesOwnedTasks(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
userID := "user-1"
|
|
svc := NewIngestionTaskService()
|
|
result, err := svc.RemoveMany([]string{"task-1"}, &userID)
|
|
if err != nil {
|
|
t.Fatalf("RemoveMany failed: %v", err)
|
|
}
|
|
if len(result) != 1 || result[0]["remove"] != "success" {
|
|
t.Fatalf("unexpected remove result: %+v", result)
|
|
}
|
|
if _, err := dao.NewIngestionTaskDAO().GetByID("task-1"); err == nil {
|
|
t.Fatal("task should be removed")
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceListAllForAdminIncludesRunAndUserEmail(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
status := "1"
|
|
if err := dao.DB.Create(&entity.User{
|
|
ID: "user-1",
|
|
Email: "user-1@test.com",
|
|
Nickname: "user-1",
|
|
IsAuthenticated: "1",
|
|
IsActive: "1",
|
|
IsAnonymous: "0",
|
|
Status: &status,
|
|
}).Error; err != nil {
|
|
t.Fatalf("insert user: %v", err)
|
|
}
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
if err := dao.DB.Create(&entity.IngestionTaskLog{
|
|
TaskID: "task-1",
|
|
Checkpoint: entity.JSONMap{"run_count": 3},
|
|
}).Error; err != nil {
|
|
t.Fatalf("insert task log: %v", err)
|
|
}
|
|
|
|
svc := NewIngestionTaskService()
|
|
tasks, err := svc.ListAllForAdmin()
|
|
if err != nil {
|
|
t.Fatalf("ListAllForAdmin failed: %v", err)
|
|
}
|
|
if len(tasks) != 1 {
|
|
t.Fatalf("expected 1 task, got %d", len(tasks))
|
|
}
|
|
if tasks[0]["user"] != "user-1@test.com" {
|
|
t.Fatalf("user = %v, want user-1@test.com", tasks[0]["user"])
|
|
}
|
|
if tasks[0]["run_count"] != 3 {
|
|
t.Fatalf("run_count = %v, want 3", tasks[0]["run_count"])
|
|
}
|
|
if tasks[0]["component_total"] != 0 {
|
|
t.Fatalf("component_total = %v, want 0", tasks[0]["component_total"])
|
|
}
|
|
if tasks[0]["component_done"] != 0 {
|
|
t.Fatalf("component_done = %v, want 0", tasks[0]["component_done"])
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceStartRunningTransitionsCreatedTask(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
task, err := svc.StartRunning("task-1")
|
|
if err != nil {
|
|
t.Fatalf("StartRunning failed: %v", err)
|
|
}
|
|
if task.Status != common.RUNNING {
|
|
t.Fatalf("status = %q, want %q", task.Status, common.RUNNING)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceRequestStopTransitionsCreatedTaskToStopped(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
task, err := svc.RequestStop("task-1")
|
|
if err != nil {
|
|
t.Fatalf("RequestStop failed: %v", err)
|
|
}
|
|
if task.Status != common.STOPPED {
|
|
t.Fatalf("status = %q, want %q", task.Status, common.STOPPED)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceMarkCompletedRejectsNonRunningTask(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.MarkCompleted("task-1"); err == nil {
|
|
t.Fatal("expected MarkCompleted to reject non-running task")
|
|
}
|
|
task, err := dao.NewIngestionTaskDAO().GetByID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("load task: %v", err)
|
|
}
|
|
if task.Status != common.CREATED {
|
|
t.Fatalf("status = %q, want %q", task.Status, common.CREATED)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceMarkCompletedUpdatesTaskStatus(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
if err := dao.DB.Model(&entity.IngestionTask{}).Where("id = ?", "task-1").Update("status", common.RUNNING).Error; err != nil {
|
|
t.Fatalf("set running status: %v", err)
|
|
}
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.MarkCompleted("task-1"); err != nil {
|
|
t.Fatalf("MarkCompleted failed: %v", err)
|
|
}
|
|
task, err := dao.NewIngestionTaskDAO().GetByID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("load task: %v", err)
|
|
}
|
|
if task.Status != common.COMPLETED {
|
|
t.Fatalf("status = %q, want %q", task.Status, common.COMPLETED)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceMarkFailedUpdatesTaskStatus(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
if err := dao.DB.Model(&entity.IngestionTask{}).Where("id = ?", "task-1").Update("status", common.RUNNING).Error; err != nil {
|
|
t.Fatalf("set running status: %v", err)
|
|
}
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.MarkFailed("task-1"); err != nil {
|
|
t.Fatalf("MarkFailed failed: %v", err)
|
|
}
|
|
task, err := dao.NewIngestionTaskDAO().GetByID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("load task: %v", err)
|
|
}
|
|
if task.Status != common.FAILED {
|
|
t.Fatalf("status = %q, want %q", task.Status, common.FAILED)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceNewTaskStatusConflictErrorLoadsActualStatus(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
if err := dao.DB.Model(&entity.IngestionTask{}).Where("id = ?", "task-1").Update("status", common.STOPPING).Error; err != nil {
|
|
t.Fatalf("set stopping status: %v", err)
|
|
}
|
|
|
|
svc := NewIngestionTaskService()
|
|
err := svc.newTaskStatusConflictError("task-1", common.CREATED, common.RUNNING)
|
|
var conflictErr *TaskStatusConflictError
|
|
if !errors.As(err, &conflictErr) {
|
|
t.Fatalf("expected TaskStatusConflictError, got %T", err)
|
|
}
|
|
if conflictErr.TaskID != "task-1" || conflictErr.ExpectedFrom != common.CREATED || conflictErr.AttemptedTo != common.RUNNING || conflictErr.ActualCurrent != common.STOPPING {
|
|
t.Fatalf("unexpected conflict error: %+v", conflictErr)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceMarkCompletedReturnsTaskIDInTransitionError(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
err := svc.MarkCompleted("task-1")
|
|
var transitionErr *InvalidTaskTransitionError
|
|
if !errors.As(err, &transitionErr) {
|
|
t.Fatalf("expected InvalidTaskTransitionError, got %T", err)
|
|
}
|
|
if transitionErr.TaskID != "task-1" || transitionErr.From != common.CREATED || transitionErr.To != common.COMPLETED {
|
|
t.Fatalf("unexpected transition error: %+v", transitionErr)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceCreateAndEnqueueRetriesTerminalTask(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
publisher := &recordingTaskPublisher{}
|
|
svc := NewIngestionTaskService()
|
|
svc.taskPublisher = publisher
|
|
|
|
testCases := []struct {
|
|
name string
|
|
status string
|
|
}{
|
|
{name: "failed", status: common.FAILED},
|
|
{name: "stopped", status: common.STOPPED},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
publisher.subject = ""
|
|
publisher.messages = nil
|
|
if err := dao.DB.Where("id = ?", "task-1").Delete(&entity.IngestionTask{}).Error; err != nil {
|
|
t.Fatalf("clear task: %v", err)
|
|
}
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
if err := dao.DB.Model(&entity.IngestionTask{}).Where("id = ?", "task-1").Update("status", tc.status).Error; err != nil {
|
|
t.Fatalf("set terminal status: %v", err)
|
|
}
|
|
|
|
task, err := svc.CreateAndEnqueue(&entity.IngestionTask{
|
|
DocumentID: "doc-1",
|
|
UserID: "user-1",
|
|
DatasetID: "kb-1",
|
|
Status: common.CREATED,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateAndEnqueue failed: %v", err)
|
|
}
|
|
if task.ID != "task-1" {
|
|
t.Fatalf("task ID = %q, want task-1", task.ID)
|
|
}
|
|
if task.Status != common.CREATED {
|
|
t.Fatalf("status = %q, want %q", task.Status, common.CREATED)
|
|
}
|
|
if len(publisher.messages) != 1 || publisher.messages[0].TaskID != "task-1" {
|
|
t.Fatalf("unexpected published messages: %+v", publisher.messages)
|
|
}
|
|
reloaded, err := dao.NewIngestionTaskDAO().GetByID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("reload task: %v", err)
|
|
}
|
|
if reloaded.Status != common.CREATED {
|
|
t.Fatalf("reloaded status = %q, want %q", reloaded.Status, common.CREATED)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceCreateAndEnqueueRejectsActiveExistingTask(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
publisher := &recordingTaskPublisher{}
|
|
svc := NewIngestionTaskService()
|
|
svc.taskPublisher = publisher
|
|
|
|
_, err := svc.CreateAndEnqueue(&entity.IngestionTask{DocumentID: "doc-1", UserID: "user-1", DatasetID: "kb-1", Status: common.CREATED})
|
|
if err == nil {
|
|
t.Fatal("expected CreateAndEnqueue to reject existing created task")
|
|
}
|
|
if len(publisher.messages) != 0 {
|
|
t.Fatalf("expected no published messages, got %+v", publisher.messages)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceCreateAndEnqueueRollsBackNewTaskOnPublishFailure(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
publisher := &recordingTaskPublisher{err: errors.New("publish failed")}
|
|
svc := NewIngestionTaskService()
|
|
svc.taskPublisher = publisher
|
|
|
|
_, err := svc.CreateAndEnqueue(&entity.IngestionTask{
|
|
DocumentID: "doc-1",
|
|
UserID: "user-1",
|
|
DatasetID: "kb-1",
|
|
Status: common.CREATED,
|
|
})
|
|
if err == nil || err.Error() != "publish failed" {
|
|
t.Fatalf("expected publish failure, got %v", err)
|
|
}
|
|
task, getErr := dao.NewIngestionTaskDAO().GetByDocumentID("doc-1")
|
|
if getErr != nil {
|
|
t.Fatalf("reload task by document id: %v", getErr)
|
|
}
|
|
if task != nil {
|
|
t.Fatalf("expected created task to be deleted after publish failure, got %+v", task)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceCreateAndEnqueueRollsBackRetriedTaskOnPublishFailure(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
if err := dao.DB.Model(&entity.IngestionTask{}).Where("id = ?", "task-1").Update("status", common.FAILED).Error; err != nil {
|
|
t.Fatalf("set failed status: %v", err)
|
|
}
|
|
|
|
publisher := &recordingTaskPublisher{err: errors.New("publish failed")}
|
|
svc := NewIngestionTaskService()
|
|
svc.taskPublisher = publisher
|
|
|
|
_, err := svc.CreateAndEnqueue(&entity.IngestionTask{
|
|
DocumentID: "doc-1",
|
|
UserID: "user-1",
|
|
DatasetID: "kb-1",
|
|
Status: common.CREATED,
|
|
})
|
|
if err == nil || err.Error() != "publish failed" {
|
|
t.Fatalf("expected publish failure, got %v", err)
|
|
}
|
|
reloaded, getErr := dao.NewIngestionTaskDAO().GetByID("task-1")
|
|
if getErr != nil {
|
|
t.Fatalf("reload task: %v", getErr)
|
|
}
|
|
if reloaded.Status != common.FAILED {
|
|
t.Fatalf("status = %q, want %q", reloaded.Status, common.FAILED)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceRemoveDeletesOwnedTask(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
userID := "user-1"
|
|
svc := NewIngestionTaskService()
|
|
info, err := svc.Remove("task-1", &userID)
|
|
if err != nil {
|
|
t.Fatalf("Remove failed: %v", err)
|
|
}
|
|
if info == nil || info.TaskID != "task-1" {
|
|
t.Fatalf("unexpected task info: %+v", info)
|
|
}
|
|
if _, err := dao.NewIngestionTaskDAO().GetByID("task-1"); err == nil {
|
|
t.Fatal("task should be removed")
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceUpdateComponentTotalPersistsDenominator(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.UpdateComponentTotal("task-1", 4); err != nil {
|
|
t.Fatalf("UpdateComponentTotal failed: %v", err)
|
|
}
|
|
task, err := dao.NewIngestionTaskDAO().GetByID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("load task: %v", err)
|
|
}
|
|
if task.ComponentTotal != 4 {
|
|
t.Fatalf("component_total = %d, want 4", task.ComponentTotal)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceRecordComponentProgressAppendsRow(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.RecordComponentProgress("task-1", "Parser", 1, "Parser Done"); err != nil {
|
|
t.Fatalf("RecordComponentProgress failed: %v", err)
|
|
}
|
|
logs, err := dao.NewIngestionTaskLogDAO().ListLogsByTaskID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("list logs: %v", err)
|
|
}
|
|
if len(logs) != 1 {
|
|
t.Fatalf("expected 1 log row, got %d", len(logs))
|
|
}
|
|
row := logs[0]
|
|
if row.Component != "Parser" || row.Phase != 1 || row.Message != "Parser Done" {
|
|
t.Fatalf("unexpected log row: %+v", row)
|
|
}
|
|
if len(row.Checkpoint) != 0 {
|
|
t.Fatalf("component progress row must have empty checkpoint, got %v", row.Checkpoint)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceAggregateTaskProgressClassifiesByPhase(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.RecordComponentProgress("task-1", "Parser", 1, "Parser Done"); err != nil {
|
|
t.Fatalf("record Parser: %v", err)
|
|
}
|
|
if err := svc.RecordComponentProgress("task-1", "Chunker", 0, "Chunker Started"); err != nil {
|
|
t.Fatalf("record Chunker: %v", err)
|
|
}
|
|
agg, err := svc.AggregateTaskProgress("task-1", 2)
|
|
if err != nil {
|
|
t.Fatalf("AggregateTaskProgress failed: %v", err)
|
|
}
|
|
if agg.Done != 1 || agg.Running != 1 || agg.Failed != 0 {
|
|
t.Fatalf("aggregate = %+v, want Done=1 Running=1 Failed=0", agg)
|
|
}
|
|
if agg.Percent != 50 {
|
|
t.Fatalf("percent = %v, want 50", agg.Percent)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceIncrementRunCountInitializesAndBumps(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.IncrementRunCount("task-1"); err != nil {
|
|
t.Fatalf("IncrementRunCount (first call) failed: %v", err)
|
|
}
|
|
run, ok := svc.lastRunCount("task-1")
|
|
if !ok || run != 1 {
|
|
t.Fatalf("run_count = %v (ok=%v), want 1", run, ok)
|
|
}
|
|
|
|
// Second call bumps the existing counter to 2.
|
|
if err := svc.IncrementRunCount("task-1"); err != nil {
|
|
t.Fatalf("IncrementRunCount (second call) failed: %v", err)
|
|
}
|
|
run, _ = svc.lastRunCount("task-1")
|
|
if run != 2 {
|
|
t.Fatalf("run_count after second bump = %v, want 2", run)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceIncrementRunCountSkippedCorruptedRunCount(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
if err := dao.DB.Create(&entity.IngestionTaskLog{
|
|
TaskID: "task-1",
|
|
Checkpoint: entity.JSONMap{"run_count": "not-a-number"},
|
|
}).Error; err != nil {
|
|
t.Fatalf("insert bad task log: %v", err)
|
|
}
|
|
|
|
svc := NewIngestionTaskService()
|
|
// Corrupted value is skipped; a fresh run_count=1 row is created.
|
|
if err := svc.IncrementRunCount("task-1"); err != nil {
|
|
t.Fatalf("IncrementRunCount should skip corrupted value, got: %v", err)
|
|
}
|
|
run, ok := svc.lastRunCount("task-1")
|
|
if !ok || run != 1 {
|
|
t.Fatalf("run_count = %v (ok=%v), want 1", run, ok)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceIncrementRunCountRecoversFromComponentProgressLog(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
// Simulate a previous run that created some component-progress logs
|
|
// but died before recording a run_count row. The latest log has no run_count.
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.RecordComponentProgress("task-1", "Parser", 1, "Parser Done"); err != nil {
|
|
t.Fatalf("record Parser: %v", err)
|
|
}
|
|
if err := svc.RecordComponentProgress("task-1", "Chunker", 1, "Chunker Done"); err != nil {
|
|
t.Fatalf("record Chunker: %v", err)
|
|
}
|
|
// Verify latest log has empty checkpoint (no run_count).
|
|
latest, err := dao.NewIngestionTaskLogDAO().LatestLogByTaskID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("load latest: %v", err)
|
|
}
|
|
if len(latest.Checkpoint) != 0 {
|
|
t.Fatalf("component-progress row should have empty checkpoint, got %v", latest.Checkpoint)
|
|
}
|
|
|
|
// IncrementRunCount should create a new row with run_count=1.
|
|
if err := svc.IncrementRunCount("task-1"); err != nil {
|
|
t.Fatalf("IncrementRunCount failed: %v", err)
|
|
}
|
|
run, ok := svc.lastRunCount("task-1")
|
|
if !ok || run != 1 {
|
|
t.Fatalf("run_count = %v (ok=%v), want 1", run, ok)
|
|
}
|
|
|
|
// AggregateProgress should still work (run_count row with component=""
|
|
// has phase=0, which doesn't affect counts).
|
|
agg, err := svc.AggregateTaskProgress("task-1", 2)
|
|
if err != nil {
|
|
t.Fatalf("AggregateTaskProgress: %v", err)
|
|
}
|
|
if agg.Done != 2 {
|
|
t.Fatalf("Done = %d, want 2 (run_count row didn't interfere)", agg.Done)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceIncrementRunCountAccumulatesAcrossRetries(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
|
|
svc := NewIngestionTaskService()
|
|
|
|
// First attempt: IncrementRunCount creates run_count=1.
|
|
if err := svc.IncrementRunCount("task-1"); err != nil {
|
|
t.Fatalf("first IncrementRunCount: %v", err)
|
|
}
|
|
// Simulate first run: some components progress, then failure.
|
|
if err := svc.RecordComponentProgress("task-1", "Parser", 1, "Parser Done"); err != nil {
|
|
t.Fatalf("record Parser: %v", err)
|
|
}
|
|
|
|
// Second attempt (retry): should find previous run_count=1 and create row with run_count=2.
|
|
if err := svc.IncrementRunCount("task-1"); err != nil {
|
|
t.Fatalf("second IncrementRunCount: %v", err)
|
|
}
|
|
// More progress, then failure.
|
|
if err := svc.RecordComponentProgress("task-1", "Chunker", 1, "Chunker Done"); err != nil {
|
|
t.Fatalf("record Chunker: %v", err)
|
|
}
|
|
|
|
// Third attempt (retry): should find previous run_count=2 and create row with run_count=3.
|
|
if err := svc.IncrementRunCount("task-1"); err != nil {
|
|
t.Fatalf("third IncrementRunCount: %v", err)
|
|
}
|
|
|
|
run, ok := svc.lastRunCount("task-1")
|
|
if !ok || run != 3 {
|
|
t.Fatalf("run_count = %v (ok=%v), want 3", run, ok)
|
|
}
|
|
|
|
// ListAllForAdmin should still pick up the correct run_count.
|
|
status := "1"
|
|
if err := dao.DB.Create(&entity.User{
|
|
ID: "user-1",
|
|
Email: "user-1@test.com",
|
|
Nickname: "user-1",
|
|
IsAuthenticated: "1",
|
|
IsActive: "1",
|
|
IsAnonymous: "0",
|
|
Status: &status,
|
|
}).Error; err != nil {
|
|
t.Fatalf("insert user: %v", err)
|
|
}
|
|
adminTasks, err := svc.ListAllForAdmin()
|
|
if err != nil {
|
|
t.Fatalf("ListAllForAdmin: %v", err)
|
|
}
|
|
if len(adminTasks) != 1 || adminTasks[0]["id"] != "task-1" {
|
|
t.Fatalf("ListAllForAdmin = %+v, want single task task-1", adminTasks)
|
|
}
|
|
if adminTasks[0]["run_count"] != 3 {
|
|
t.Fatalf("ListAllForAdmin run_count = %v, want 3", adminTasks[0]["run_count"])
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceMarkStoppedTransitionsStoppingTask(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
// Override to STOPPING.
|
|
if err := dao.DB.Model(&entity.IngestionTask{}).Where("id = ?", "task-1").
|
|
Update("status", common.STOPPING).Error; err != nil {
|
|
t.Fatalf("set task STOPPING: %v", err)
|
|
}
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.MarkStopped("task-1"); err != nil {
|
|
t.Fatalf("MarkStopped failed: %v", err)
|
|
}
|
|
|
|
task, err := dao.NewIngestionTaskDAO().GetByID("task-1")
|
|
if err != nil {
|
|
t.Fatalf("load task: %v", err)
|
|
}
|
|
if task.Status != common.STOPPED {
|
|
t.Fatalf("task status = %s, want STOPPED", task.Status)
|
|
}
|
|
}
|
|
|
|
func TestIngestionTaskServiceMarkStoppedIdempotentOnAlreadyStopped(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestIngestionTask(t, "task-1", "user-1", "doc-1", "kb-1")
|
|
if err := dao.DB.Model(&entity.IngestionTask{}).Where("id = ?", "task-1").
|
|
Update("status", common.STOPPED).Error; err != nil {
|
|
t.Fatalf("set task STOPPED: %v", err)
|
|
}
|
|
|
|
svc := NewIngestionTaskService()
|
|
if err := svc.MarkStopped("task-1"); err != nil {
|
|
t.Fatalf("MarkStopped on already STOPPED task should be idempotent, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDocumentServiceUpdateRunProgressMirrorsFields(t *testing.T) {
|
|
db := setupServiceTestDB(t)
|
|
pushServiceDB(t, db)
|
|
insertTestDoc(t, "doc-1", "kb-1", 0, 0)
|
|
|
|
svc := testDocumentService(t)
|
|
if err := svc.UpdateRunProgress("doc-1", 0.5, "1", "halfway"); err != nil {
|
|
t.Fatalf("UpdateRunProgress failed: %v", err)
|
|
}
|
|
doc, err := dao.NewDocumentDAO().GetByID("doc-1")
|
|
if err != nil {
|
|
t.Fatalf("load document: %v", err)
|
|
}
|
|
if doc.Progress != 0.5 {
|
|
t.Fatalf("progress = %v, want 0.5", doc.Progress)
|
|
}
|
|
if doc.Run == nil || *doc.Run != "1" {
|
|
t.Fatalf("run = %v, want 1", doc.Run)
|
|
}
|
|
if doc.ProgressMsg == nil || *doc.ProgressMsg != "halfway" {
|
|
t.Fatalf("progress_msg = %v, want halfway", doc.ProgressMsg)
|
|
}
|
|
}
|