c949204662
借鉴 odysseus 的能力设计,全程净室实现、零 AGPL 代码、不引入 AGPL 依赖。
T1 提示注入防护: pkg/promptguard 包裹外部/知识库内容为不可信数据,buildMessages 移出 system 指令区。 T2 安全 CI: .github/workflows(ci+security: govulncheck/gitleaks/actionlint/hadolint/trivy)+dependabot+.hadolint.yaml;go.mod 加 toolchain go1.25.11 修复 20 个 stdlib CVE。 T3 管理员 2FA: 迁移 000016 + RFC6238 TOTP/备份码(pkg/auth, 零依赖) + 登录流程集成(后端)。 T4 本地模型: LLM/embedding 支持本地 vLLM/Ollama(OpenAI 兼容, 鉴权头条件发送, NoAuth) + docs/local-deploy.md。 T6 深度研究: 迁移 000017 + Python research-worker(净室多步流水线, 检索避开 SearXNG) + Go research 服务/handler/路由。 T7 service 层: 新增 internal/service/{research,twofa}, 2FA 业务逻辑从胖 handler 下沉, 接口注入可单测。 T10 缓存/可观测性: internal/cache(Redis+内存, 优雅降级) 接入 store 热点列表; Prometheus 指标+/metrics; docs/openapi.yaml。 验证: go build/vet/test ./... 全绿(8 包); research-worker 12 单测过; 真实 PG 应用迁移并烟测。
131 lines
3.6 KiB
Go
131 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|