Files
ragflow/internal/service/ingestion_task_service_test.go
Jack cfacaccad7 Refactor: message processing (#16852)
### Summary

1. refactor message processing
2. delete un-used componentIndexMap
3. unfold (delete) internal/ingestion/task/task_handler.go
2026-07-13 16:32:34 +08:00

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)
}
}