diff --git a/server/internal/repo/agent_mode_list_test.go b/server/internal/repo/agent_mode_list_test.go new file mode 100644 index 0000000..a3f2bc6 --- /dev/null +++ b/server/internal/repo/agent_mode_list_test.go @@ -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)) + } +} diff --git a/server/internal/repo/repo.go b/server/internal/repo/repo.go index 1f12b5e..e7b96cb 100644 --- a/server/internal/repo/repo.go +++ b/server/internal/repo/repo.go @@ -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 {