fix(repo): /api/v1/agents 带出 mode_enforcement —— 列表接口静默少了一个事实
# 现象 `/api/v1/agents` 对全部四个 Agent 返回 `mode_enforcement: ""`, 而库里四个值一直是正确的(pi=native / dsh=partial / opencode=advisory / homeagent=advisory)。这是全功能演练 Phase A 的前置检查抓到的。 # 根因 `ListAgents` 的 SELECT 里没有 `mode_enforcement`,Scan 自然也没扫它。 这与「SELECT 加了列但没加进 Scan」(列数不匹配,直接报错)**不是**一回事: 少取一列不报任何错,只是安静地少一个事实。而「这个 Agent 到底能不能真的拦住 危险操作」是使用者在派活前必须知道的事 —— 缺了它,advisory 档会被当成 native 档用。 # 修法 SELECT 补上 `COALESCE(NULLIF(mode_enforcement, ''), 'advisory')`,与 `permission_mode.go` 同一套兜底(那里也这么写,说明空串确实可能出现)。 # 测试 `repo/agent_mode_list_test.go`:三档各一条 + 一条空串(脏数据),断言**值真的 传出来**而不是「函数没报错」;另加反向对照确认行真的被查到了(避免一个都没查到 也能过)。 反向验证过判据非空转:把修复退掉,测试立刻失败。
This commit is contained in:
67
server/internal/repo/agent_mode_list_test.go
Normal file
67
server/internal/repo/agent_mode_list_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/agentmail/gateway/internal/db"
|
||||
)
|
||||
|
||||
// ListAgents 必须把 mode_enforcement 带出来。
|
||||
//
|
||||
// # 为什么需要这条测试
|
||||
//
|
||||
// 该字段曾是**静默丢失**的:`ListAgents` 的 SELECT 里没有它,Scan 自然也没扫,
|
||||
// 于是 `/api/v1/agents` 对全部 Agent 返回空串 —— 而库里四个 Agent 的值
|
||||
// (native / partial / advisory)一直是对的。
|
||||
//
|
||||
// 这类故障跟「SELECT 加列但没加进 Scan」(列数不匹配,直接报错)**不是**一回事:
|
||||
// 少取一列不会报任何错,只是安静地少一个事实。而「这个 Agent 到底能不能真的
|
||||
// 拦住危险操作」是使用者在派活前必须知道的事,缺了它,advisory 档会被当成
|
||||
// native 档用 —— 正是本项目一直在治的那种「静默不一致」。
|
||||
//
|
||||
// 所以判据是**值真的传出来**,不是「函数没报错」。
|
||||
func TestListAgentsCarriesModeEnforcement(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
setupTestDB(t)
|
||||
database := db.DB
|
||||
|
||||
// 三轮覆盖三档,并包含「空串」这种脏数据(permission_mode.go 用
|
||||
// NULLIF(...,'') 兜底,说明空串确实可能出现)。
|
||||
cases := []struct{ name, mode, want string }{
|
||||
{"t-mode-native", "native", "native"},
|
||||
{"t-mode-partial", "partial", "partial"},
|
||||
{"t-mode-advisory", "advisory", "advisory"},
|
||||
{"t-mode-empty", "", "advisory"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if _, err := database.ExecContext(ctx,
|
||||
`INSERT INTO agents (agent_name, secret, platform, status, mode_enforcement)
|
||||
VALUES ($1, 's', 'pi', 'online', $2)`, c.name, c.mode); err != nil {
|
||||
t.Fatalf("插入 %s 失败: %v", c.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
agents, err := ListAgents(ctx, "all")
|
||||
if err != nil {
|
||||
t.Fatalf("ListAgents 出错: %v", err)
|
||||
}
|
||||
|
||||
got := map[string]string{}
|
||||
for _, a := range agents {
|
||||
got[a.Name] = a.ModeEnforcement
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got[c.name] != c.want {
|
||||
t.Errorf("%s:mode_enforcement 传出来是 %q,应为 %q —— "+
|
||||
"列表接口必须带出档位,否则调用方会把 advisory 当 native 用",
|
||||
c.name, got[c.name], c.want)
|
||||
}
|
||||
}
|
||||
|
||||
// 反向对照:确认这些行确实被查到了(避免「一个都没查到」也能过)。
|
||||
if len(agents) < len(cases) {
|
||||
t.Errorf("只查到 %d 行,期望至少 %d 行 —— 上面的断言可能是空转",
|
||||
len(agents), len(cases))
|
||||
}
|
||||
}
|
||||
@ -187,8 +187,18 @@ func SetAgentDisabled(ctx context.Context, agentName string, disabled bool) (int
|
||||
func ListAgents(ctx context.Context, statusFilter string) ([]models.Agent, error) {
|
||||
// 带上 default_rounds:前端补全收件人时要显示「派给它的任务默认几个来回」,
|
||||
// 否则人得先去管理员页查一遍才敢派活。
|
||||
//
|
||||
// 也要带 mode_enforcement:它是插件自报的**档位强制能力**
|
||||
// (native / partial / advisory)。"这个 Agent 到底能不能真的拦住危险操作"
|
||||
// 是使用者在派活前必须知道的事。
|
||||
//
|
||||
// 该字段曾在列表接口**静默丢失**(SELECT 里没有,Scan 也就没扫),
|
||||
// 而库里四个 Agent 的值一直是正确的 —— 表现为 /api/v1/agents 全部返回空串。
|
||||
// 与列数不匹配不同,这种"少取一列"不会报错,只会安静地少一个事实。
|
||||
// 回归测试:repo/agent_mode_list_test.go。
|
||||
q := `SELECT agent_id, agent_name, workspaces, platform, status,
|
||||
COALESCE(default_rounds, 0) FROM agents`
|
||||
COALESCE(default_rounds, 0),
|
||||
COALESCE(NULLIF(mode_enforcement, ''), 'advisory') FROM agents`
|
||||
args := []any{}
|
||||
switch statusFilter {
|
||||
case "":
|
||||
@ -212,7 +222,7 @@ func ListAgents(ctx context.Context, statusFilter string) ([]models.Agent, error
|
||||
var a models.Agent
|
||||
var wsJSON []byte
|
||||
if err := rows.Scan(&a.ID, &a.Name, &wsJSON, &a.Platform, &a.Status,
|
||||
&a.DefaultRounds); err != nil {
|
||||
&a.DefaultRounds, &a.ModeEnforcement); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wsJSON != nil {
|
||||
|
||||
Reference in New Issue
Block a user