e4ec74893f
全平台任务页此前只能看列表,点进去什么都没有。现在点一行开抽屉,四个页签:
执行轨迹、最终输出、评测明细、提交时的 DSL。不含审批操作——审批是客户端
用户的行为(桌面端 ApprovalBar),管理端只做观测。
不复用用户面的 /tasks/:id/replay:Task/Eval 都在租户插件作用域内,用请求 ctx
查别的租户的任务不会报错,而是静默返回空输出/空轨迹,UI 上表现为"这任务没
产出",比报错难查得多。新增 admin 端点走 WithoutTenant。
数据取自 sundynix_task 收尾落库的 output/trace 列,不依赖 Redis 流(10min TTL)。
所以这是复盘视图,运行中的任务轨迹为空——UI 里明确写出来,免得被当成轨迹丢了。
修的两处与测试环境失真有关(写测试时暴露的):
- 测试库没配 NamingStrategy,与 OpenPostgres 不一致:多数模型有显式
TableName() 碰巧对得上,但 Task 这类没有的会退化成 "tasks",导致写裸
SQL 的查询在测试里查无此表。现已对齐 sundynix_ 前缀 + 单数表名。
- graph 的 ::text 换成标准 cast(... as text):前者是 Postgres 专有,
换掉后这条查询才能被内存库覆盖。
新增 5 组测试,其中一组专门先证明"租户过滤在测试环境里确实开着"——否则
"跨租户能读到"的断言可能只是因为插件没装,属于假过。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|