package store import ( "context" "testing" ) // 管理端任务下钻必须跨租户读。Task/Eval 都在租户插件作用域内,若用请求 ctx 直接查, // 管理员看别的租户的任务不会报错,而是**静默返回空**——UI 上表现为"这任务没产出", // 比报错难查得多。这组测试把"跨租户可读"钉死。 func seedTask(t *testing.T, p *Postgres, tenantID, taskID string) { t.Helper() task := &Task{ BaseModel: BaseModel{ID: taskID}, TenantID: tenantID, Owner: "u-" + tenantID, TaskID: taskID, Status: "done", Output: "最终输出内容", Trace: `[{"node":"n1","type":"llm","msg":"跑了一步"}]`, Graph: `{"topic":"测试主题"}`, } // 显式 WithoutTenant 建数据:插件会按 ctx 覆写 tenant_id,不绕开就种不进指定租户。 if err := p.db.WithContext(WithoutTenant(context.Background())).Create(task).Error; err != nil { t.Fatalf("建任务失败: %v", err) } } // 前提验证:先证明租户过滤在测试环境里真的生效(用租户作用域的读去查别人的任务应查不到)。 // 否则下面"跨租户能读到"的断言可能只是因为插件压根没装,属于假过。 func TestTaskDetail_TenantScopeIsActuallyOn(t *testing.T) { p := newTestStore(t) seedTenant(t, p, "t1") seedTask(t, p, "t1", "task_a") var got Task err := p.db.WithContext(WithTenant(context.Background(), "t2")). Where("task_id = ?", "task_a").First(&got).Error if err == nil { t.Fatal("租户过滤没生效:t2 的作用域竟能读到 t1 的任务,本测试文件的其余断言都不可信") } } func TestTaskDetail_ReadsAcrossTenants(t *testing.T) { p := newTestStore(t) seedTenant(t, p, "t1") seedTask(t, p, "t1", "task_a") // 以「另一个租户」的 ctx 调用——模拟管理员本人属于 t2、却要看 t1 的任务。 d := p.TaskDetail(WithTenant(context.Background(), "t2"), "task_a") if d == nil { t.Fatal("跨租户下钻取不到任务(TaskDetail 少了 WithoutTenant?)") } if d.Output != "最终输出内容" { t.Fatalf("输出应完整读出,得 %q", d.Output) } if d.Trace == "" { t.Fatal("轨迹不该为空") } if d.Topic != "测试主题" { t.Fatalf("topic 应从 graph 里取出,得 %q", d.Topic) } } func TestTaskDetail_MissingReturnsNil(t *testing.T) { p := newTestStore(t) if d := p.TaskDetail(context.Background(), "task_不存在"); d != nil { t.Fatalf("不存在的任务应返回 nil,得 %+v", d) } } func TestTaskDetail_EvalAttachedWhenPresent(t *testing.T) { p := newTestStore(t) seedTenant(t, p, "t1") seedTask(t, p, "t1", "task_a") ctx := WithoutTenant(context.Background()) if err := p.db.WithContext(ctx).Create(&Eval{ BaseModel: BaseModel{ID: "ev1"}, TenantID: "t1", TaskID: "task_a", Overall: 0.82, Level: "ok", Reason: "还行", Sources: 3, }).Error; err != nil { t.Fatalf("建评测失败: %v", err) } d := p.TaskDetail(WithTenant(context.Background(), "t2"), "task_a") if d == nil || d.Eval == nil { t.Fatal("有评测时应带出评测明细") } if d.Eval.Level != "ok" || d.Eval.Sources != 3 { t.Fatalf("评测字段没读对: %+v", d.Eval) } } // 没有评测的任务,eval 应为 nil 而不是零值——否则前端会把 0 分当成"评了 0 分"。 func TestTaskDetail_NoEvalIsNil(t *testing.T) { p := newTestStore(t) seedTenant(t, p, "t1") seedTask(t, p, "t1", "task_a") d := p.TaskDetail(context.Background(), "task_a") if d == nil { t.Fatal("应能取到任务") } if d.Eval != nil { t.Fatalf("无评测时 eval 应为 nil,得 %+v", d.Eval) } }