Files
heicode-mananger/heicode/controller/agent_preflight_test.go
T
chenchenandClaude Opus 4.8 19640b44f5 fix(preflight): address #41 review — persisted confirmation, deploy ready re-check, template-aware version
回应 Fasthei 复审(PR #51 CHANGES_REQUESTED):
1. 持久化确认记录(强一致):新增 model.PreflightConfirmation 表 + InsertPreflightConfirmation +
   PreflightConfirmationExists。confirm 时落库(默认 TTL=HEICODE_PREFLIGHT_CONFIRMATION_TTL_SECONDS
   =3600s,可设 0 不过期),写失败直接报错(非 best-effort)。部署侧要求该版本存在未过期确认记录
   → 杜绝直接拿 GET version 绕过 confirm/审计。
2. 部署重新校验 Ready:verifyDeployPreflight 增加 summary.Ready 检查 —— 预算/agent_slot 等
   易变项不进版本哈希,故部署时重查,防 confirm 后余额耗尽/槽位占满仍启动。
3. 版本哈希纳入模板安全面:computePreflightVersion 加 tplDigest(definition+model+name 摘要),
   管理员改同一 template_key 的 definition/model 后旧确认失效。补 TestComputePreflightVersion_ChangesOnTemplateEdit。
4. 审计降为附加流:强一致确认记录作为部署门禁;审计 preflight.confirmed 互补。

测试:PreflightConfirmationExists(命中/版本不符/跨用户/过期/不过期/空参)、PreflightBindingKey、
模板变更翻转版本。TestMain + 生产迁移注册 PreflightConfirmation。controller+model 全回归通过。
文档 §4.1.1 更新。

Affects: Manager only(新增 preflight_confirmations 表 + 部署门禁强化)。无计费改动。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:28:07 +08:00

154 lines
6.8 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]) // 标准确认项
}
// #41: 版本哈希对稳定安全面确定且稳定;不随易变预算/在跑数变化。
func TestComputePreflightVersion_StableAndDeterministic(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", Status: "active", HasSecret: true},
}
s1 := computePreflight(tpl(), res, nil, 1_000_000, 500000, 5, 1)
s2 := computePreflight(tpl(), res, nil, 7_777_777, 500000, 5, 3) // 预算/在跑数不同
v1 := computePreflightVersion(s1)
require.Equal(t, v1, computePreflightVersion(s2), "版本不应随易变的预算/在跑数变化")
require.True(t, len(v1) > 5 && v1[:5] == "pfv1_")
// 资源顺序不影响版本(内部排序)
resReordered := []preflightResource{res[1], res[0]}
require.Equal(t, v1, computePreflightVersion(computePreflight(tpl(), resReordered, nil, 1, 500000, 5, 0)))
}
// #41: 资源篡改(改 has_secret / 增删资源)→ 版本翻转。
func TestComputePreflightVersion_ChangesOnTamper(t *testing.T) {
base := computePreflight(tpl(), []preflightResource{
{BindingID: 1, Type: "git", Provider: "github", Name: "repo", Status: "active", HasSecret: true},
}, nil, 1_000_000, 500000, 5, 0)
v0 := computePreflightVersion(base)
tampered := computePreflight(tpl(), []preflightResource{
{BindingID: 1, Type: "git", Provider: "github", Name: "repo", Status: "active", HasSecret: false},
}, nil, 1_000_000, 500000, 5, 0)
require.NotEqual(t, v0, computePreflightVersion(tampered), "改 has_secret 应翻转版本")
added := computePreflight(tpl(), []preflightResource{
{BindingID: 1, Type: "git", Provider: "github", Name: "repo", Status: "active", HasSecret: true},
{BindingID: 2, Type: "database", Provider: "postgres", Name: "db", Status: "active", HasSecret: true},
}, nil, 1_000_000, 500000, 5, 0)
require.NotEqual(t, v0, computePreflightVersion(added), "新增资源(引入 db_write 高危)应翻转版本")
}
// #41 复审 #3:模板安全面(definition/model/name)变化 → 版本翻转(同一 template_key 被改也失效)。
func TestComputePreflightVersion_ChangesOnTemplateEdit(t *testing.T) {
res := []preflightResource{
{BindingID: 1, Type: "git", Provider: "github", Name: "repo", Status: "active", HasSecret: true},
}
base := computePreflight(model.AgentTemplate{TemplateKey: "architect", NameZh: "架构顾问", Model: "opus", Definition: "v1 body"}, res, nil, 1_000_000, 500000, 5, 0)
v0 := computePreflightVersion(base)
editedDef := computePreflight(model.AgentTemplate{TemplateKey: "architect", NameZh: "架构顾问", Model: "opus", Definition: "v2 body changed"}, res, nil, 1_000_000, 500000, 5, 0)
require.NotEqual(t, v0, computePreflightVersion(editedDef), "改 definition 应翻转版本")
editedModel := computePreflight(model.AgentTemplate{TemplateKey: "architect", NameZh: "架构顾问", Model: "sonnet", Definition: "v1 body"}, res, nil, 1_000_000, 500000, 5, 0)
require.NotEqual(t, v0, computePreflightVersion(editedModel), "改 model 应翻转版本")
}
// #41: normalizeBindingIDs 去重 + 去非正数 + 保序。
func TestNormalizeBindingIDs(t *testing.T) {
require.Equal(t, []int{3, 1, 2}, normalizeBindingIDs([]int{3, 1, 3, 0, 2, -5, 1}))
require.Empty(t, normalizeBindingIDs(nil))
}
// #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]")
}