package research import ( "context" "errors" "testing" ) // ---- 测试替身 ---- type fakeRepo struct { inserted map[string]bool getResult *Task getErr error cancelOK bool cancelErr error lastInsert struct { id, userID, topic string } } func newFakeRepo() *fakeRepo { return &fakeRepo{inserted: map[string]bool{}} } func (f *fakeRepo) Insert(ctx context.Context, id, userID string, appID *string, topic string, config map[string]any) error { f.inserted[id] = true f.lastInsert.id = id f.lastInsert.userID = userID f.lastInsert.topic = topic return nil } func (f *fakeRepo) Get(ctx context.Context, userID, taskID string) (*Task, error) { return f.getResult, f.getErr } func (f *fakeRepo) List(ctx context.Context, userID string, limit int) ([]Task, error) { return nil, nil } func (f *fakeRepo) Cancel(ctx context.Context, userID, taskID string) (bool, error) { return f.cancelOK, f.cancelErr } type fakeQueue struct{ enqueued []string } func (q *fakeQueue) Enqueue(ctx context.Context, taskID string) error { q.enqueued = append(q.enqueued, taskID) return nil } type fakeCache struct{ cs *CachedStatus } func (c *fakeCache) GetStatus(ctx context.Context, taskID string) (*CachedStatus, bool) { if c.cs == nil { return nil, false } return c.cs, true } // ---- 测试 ---- func TestCreate_EmptyTopicRejected(t *testing.T) { svc := NewService(newFakeRepo(), &fakeQueue{}, nil) _, err := svc.Create(context.Background(), CreateInput{UserID: "u1", Topic: " "}) if !errors.Is(err, ErrEmptyTopic) { t.Fatalf("空题目应返回 ErrEmptyTopic,实际: %v", err) } } func TestCreate_InsertsAndEnqueues(t *testing.T) { repo := newFakeRepo() q := &fakeQueue{} svc := NewService(repo, q, nil) id, err := svc.Create(context.Background(), CreateInput{UserID: "u1", Topic: "数字政府研究"}) if err != nil { t.Fatalf("Create 出错: %v", err) } if id == "" || !repo.inserted[id] { t.Fatal("应已插入任务记录") } if len(q.enqueued) != 1 || q.enqueued[0] != id { t.Fatalf("应已用相同 id 下发到队列,实际: %v", q.enqueued) } if repo.lastInsert.topic != "数字政府研究" { t.Fatalf("题目透传错误: %q", repo.lastInsert.topic) } } func TestStatus_CacheOverlaysDB(t *testing.T) { repo := newFakeRepo() repo.getResult = &Task{ID: "t1", Status: "pending", Progress: 0} cache := &fakeCache{cs: &CachedStatus{Status: "searching", Progress: 30, Message: "检索中"}} svc := NewService(repo, &fakeQueue{}, cache) got, err := svc.Status(context.Background(), "u1", "t1") if err != nil { t.Fatalf("Status 出错: %v", err) } if got.Status != "searching" || got.Progress != 30 { t.Fatalf("缓存状态应覆盖 DB,实际 status=%s progress=%d", got.Status, got.Progress) } if got.StatusMessage == nil || *got.StatusMessage != "检索中" { t.Fatal("应带上缓存的状态消息") } } func TestStatus_DBErrorPropagates(t *testing.T) { repo := newFakeRepo() repo.getErr = errors.New("not found") svc := NewService(repo, &fakeQueue{}, &fakeCache{}) if _, err := svc.Status(context.Background(), "u1", "missing"); err == nil { t.Fatal("DB 错误应向上传播") } } func TestCancel_NotFound(t *testing.T) { repo := newFakeRepo() repo.cancelOK = false svc := NewService(repo, &fakeQueue{}, nil) if err := svc.Cancel(context.Background(), "u1", "t1"); !errors.Is(err, ErrNotFound) { t.Fatalf("不可取消时应返回 ErrNotFound,实际: %v", err) } } func TestCancel_Success(t *testing.T) { repo := newFakeRepo() repo.cancelOK = true svc := NewService(repo, &fakeQueue{}, nil) if err := svc.Cancel(context.Background(), "u1", "t1"); err != nil { t.Fatalf("可取消时应返回 nil,实际: %v", err) } }