Files
heicode-mananger/heicode/controller/agent_preflight_test.go
T
chenchenandClaude Opus 4.8 4502d6b5bc feat(preflight): read-only preflight checklist + execution summary (#39, #40)
新增 GET /api/heicode/preflight?template_id=&binding_ids=1,2,3 —— agent 启动前的统一
确认层(#29 EPIC 的 #39 缺失项检测 + #40 可读执行摘要):

- #39 缺失项检测:必需资源类别(git/sk/project_document/cloud_account)未绑定、budget
  (余额≤0)、agent_slot(在跑数达 tier 上限)。ready = missing 为空。
- #40 可读执行摘要:agent 角色、脱敏资源视图、高危操作、预算(剩余额度/quota_per_unit/
  tier 上限/当前在跑数)、审批策略。
  - 红线:resources 只暴露 type/provider/name/status/has_secret(布尔),绝不含
    secret_ref/channelId/base_url/price。
  - 高危操作固定 enum:production_deploy/db_write/cloud_resource_delete/
    production_secret/large_budget,由已绑资源类型推导,均 requires_approval。

判定逻辑抽为纯函数 computePreflight,单测覆盖:全缺失、就绪、槽位满、高危 enum、
敏感字段不泄露(序列化断言)。复用既有 ResourceBinding/模板/GetUserMaxAgents/部署门禁
口径,不改部署/计费逻辑。文档补 §4.1。

#41(confirm + 审计 + 防篡改版本校验)作为后续 POST /preflight/confirm 实现。

Affects: Manager only(新增只读端点)。无 Client/Swarm 代码改动,无计费/审计 schema 改动。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:29:34 +08:00

97 lines
3.6 KiB
Go

package controller
import (
"encoding/json"
"testing"
"github.com/heicode/manager/model"
"github.com/stretchr/testify/require"
)
func tpl() model.AgentTemplate {
return model.AgentTemplate{TemplateKey: "architect", NameZh: "架构顾问", Model: "opus"}
}
// #39: 全部必需类别缺失 + 预算不足 → missing 覆盖各项,ready=false。
func TestComputePreflight_AllMissing(t *testing.T) {
s := computePreflight(tpl(), nil, nil, 0, 500000, 5, 0)
kinds := map[string]bool{}
for _, m := range s.Missing {
kinds[m.Kind] = true
}
require.True(t, kinds["git"])
require.True(t, kinds["sk"])
require.True(t, kinds["project_document"])
require.True(t, kinds["cloud_account"])
require.True(t, kinds["budget"], "余额为 0 应报 budget 缺失")
require.False(t, kinds["agent_slot"], "0/5 未满,不应报 agent_slot")
require.False(t, s.Ready)
}
// #39: 全部齐备 + 有余额 + 槽位未满 → ready=true。
func TestComputePreflight_Ready(t *testing.T) {
res := []preflightResource{
{BindingID: 1, Type: "git", Provider: "github", Name: "repo", Status: "active", HasSecret: true},
{BindingID: 2, Type: "sk", Provider: "custom", Name: "sk-pack", Status: "active", HasSecret: true},
{BindingID: 3, Type: "project_document", Provider: "custom", Name: "doc", Status: "active"},
{BindingID: 4, Type: "cloud_account", Provider: "azure", Name: "sub", Status: "active", HasSecret: true},
}
s := computePreflight(tpl(), res, nil, 1_000_000, 500000, 5, 1)
require.Empty(t, s.Missing)
require.True(t, s.Ready)
}
// #39: agent 槽位已满 → ready=false + agent_slot 缺失项。
func TestComputePreflight_AgentSlotFull(t *testing.T) {
res := []preflightResource{
{BindingID: 1, Type: "git"}, {BindingID: 2, Type: "sk"},
{BindingID: 3, Type: "project_document"}, {BindingID: 4, Type: "cloud_account"},
}
s := computePreflight(tpl(), res, nil, 1_000_000, 500000, 5, 5)
require.False(t, s.Ready)
found := false
for _, m := range s.Missing {
if m.Kind == "agent_slot" {
found = true
}
}
require.True(t, found)
}
// #40: 高危操作只用固定 enum,并按已绑资源类型推导。
func TestComputePreflight_HighRiskEnum(t *testing.T) {
res := []preflightResource{
{BindingID: 1, Type: "git"},
{BindingID: 2, Type: "database"},
{BindingID: 3, Type: "cloud_account"},
}
s := computePreflight(tpl(), res, nil, 1_000_000, 500000, 5, 0)
ops := map[string]bool{}
for _, h := range s.HighRiskOps {
require.Contains(t, highRiskOpLabels, h.Op, "high-risk op 必须是固定 enum")
require.True(t, h.RequiresApproval)
ops[h.Op] = true
}
require.True(t, ops[highRiskProductionDeploy]) // git
require.True(t, ops[highRiskDBWrite]) // database
require.True(t, ops[highRiskCloudDelete]) // cloud_account
require.True(t, ops[highRiskProductionSecret]) // cloud_account
require.True(t, ops[highRiskLargeBudget]) // 标准确认项
}
// #40 红线:resource 视图序列化后绝不含 secret_ref/channel_id/base_url/price。
func TestComputePreflight_NoSensitiveFieldsLeaked(t *testing.T) {
res := []preflightResource{
{BindingID: 1, Type: "git", Provider: "github", Name: "repo", Status: "active", HasSecret: true},
}
s := computePreflight(tpl(), res, []int{99}, 1_000_000, 500000, 5, 0)
b, err := json.Marshal(s)
require.NoError(t, err)
out := string(b)
for _, banned := range []string{"secret_ref", "channel_id", "channelId", "base_url", "baseUrl", "price"} {
require.NotContains(t, out, banned, "执行摘要不得暴露敏感字段: "+banned)
}
require.Contains(t, out, "\"has_secret\":true") // 只暴露布尔
require.Contains(t, out, "\"invalid_bindings\":[99]")
}