回应 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>
154 lines
6.8 KiB
Go
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]")
|
|
}
|