Files
heicode-mananger/heicode/controller/agent_deliverable_secret_test.go
T
chenchenandClaude Opus 4.8 0fe1d20d67 feat(agent): unify agnet→agent and implement client/runtime unification spec v0.1 core
按桌面客户端统一方案 v0.1 + agent_management Sub Mode Runtime 对接,强制全量统一,不留兼容。

命名统一(强制,无兼容):
- 全仓 agnet/Agnet/AGNET → agent/Agent/AGENT:后端 Go(路由 /api/agent/*、env AGENT_*、
  结构体/函数、19 个文件改名)、前端(agent-console/agent-hub、/api/agent 调用、i18n)、
  DB(表 agent_*、列 agent_id)、compose/.env、文档、脚本。
- DB 加幂等迁移 renameAgnetTablesToAgent():启动时 rename 老 agnet_* 表/列,保住生产数据。

统一方案核心(10 项):
- callback 统一 /api/agent/callbacks/runtime-events(路由/广播URL/函数名)。
- artifact 兜底判定改用 Runtime 权威信号 metadata.synthesized(§7.2)+ 结构化 artifact_type。
- Manager→Runtime 路径对齐 /api/agent/sub-agile/deployments(§2.2),{deployment_id} 回退 swarm_id。
- 状态裁决 display_status:Manager 唯一裁判,completed 无有效产物→needs_codegen/
  completed_without_deliverable(§10.6),接入 detail/timeline/workflow。
- GET /api/heicode/capabilities 能力发现(§6)。
- 模型策略 per_role(role_models)+ 收集 allowed_model_ids(§9)。
- resource_binding_id→secret_ref 服务端解析,客户端不再 inline secret_ref(§17.6)。
- 客户端统一路由层 /api/heicode/sub-agile|swarm/*(task≡deployment,复用控制面)+ workflow 投影。
- 日志分层 user_logs/debug_logs(§13)。

验证:go build ./... + go test(controller/router/model/middleware)全绿;前端 tsc -b + rsbuild build 通过。
待部署:VM .env 的 AGNET_*→AGENT_*;启动迁移自动 rename 表;其他三仓库需同步切到 /api/agent。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 23:45:10 +08:00

153 lines
4.8 KiB
Go

package controller
import (
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
// P2: deliverable judgment must read structured artifact_type / file signals,
// not just the legacy title/summary/uri string heuristics.
func TestRuntimeArtifactsAreSummaryOnly_StructuredFields(t *testing.T) {
cases := []struct {
name string
artifacts []gin.H
want bool
}{
{
name: "empty list is not summary-only",
artifacts: nil,
want: false,
},
{
// The regression the review flagged: Runtime now emits
// artifact_type=document under runtime://.../artifacts/<task>,
// which the old "/artifacts/summary" uri heuristic missed.
name: "document type with new uri scheme and no files is summary-only",
artifacts: []gin.H{{
"artifact_id": "art_1",
"artifact_type": "document",
"title": "Backend delivery",
"summary": "下面是方案总结",
"uri": "runtime://swm_x/artifacts/backend_1",
}},
want: true,
},
{
name: "code_patch is a real deliverable",
artifacts: []gin.H{{
"artifact_type": "code_patch",
"uri": "runtime://swm_x/artifacts/backend_1",
}},
want: false,
},
{
name: "deployment_manifest is a real deliverable",
artifacts: []gin.H{{"artifact_type": "deployment_manifest"}},
want: false,
},
{
name: "document type but with file-change signal is a deliverable",
artifacts: []gin.H{{
"artifact_type": "document",
"metadata": map[string]any{"files_modified": float64(2)},
}},
want: false,
},
{
name: "mixed set with one real deliverable is not summary-only",
artifacts: []gin.H{
{"artifact_type": "document"},
{"artifact_type": "code_patch"},
},
want: false,
},
{
name: "legacy runtime execution summary marker still detected",
artifacts: []gin.H{{
"artifact_type": "other",
"title": "Runtime execution summary",
}},
want: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, runtimeArtifactsAreSummaryOnly(tc.artifacts))
})
}
}
// P6a: Resource CRUD must reject a secret_ref that is not an azkv:// reference.
func TestNormalizeResourcePayload_SecretRefMustBeAzkv(t *testing.T) {
base := func(secretRef string) resourcePayload {
return resourcePayload{Name: "repo", ResourceType: "git", SecretRef: secretRef}
}
_, err := normalizeResourcePayload(base("https://example.com/token"))
require.Error(t, err)
require.Contains(t, err.Error(), "azkv://")
_, err = normalizeResourcePayload(base("sk-live-plaintext-leak"))
require.Error(t, err)
got, err := normalizeResourcePayload(base("azkv://heicode-kv.vault.azure.net/secrets/repo"))
require.NoError(t, err)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo", got.SecretRef)
// Empty secret_ref stays allowed (secret can be set later via UpsertResourceSecret).
_, err = normalizeResourcePayload(base(""))
require.NoError(t, err)
}
// P6b: plaintext-secret detection must scan string VALUES, not only key names.
func TestValueLooksLikeSecretAndPlaintextScan(t *testing.T) {
positives := []string{
"sk-abcdefghij1234567890",
"sk-live-abcdefghijklmnopqrst",
"ghp_abcdefghijklmnopqrstuvwxyz0123",
"AKIAIOSFODNN7EXAMPLE",
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.s5h6Qk0c5Qx2hQ",
"-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n-----END",
}
for _, s := range positives {
require.Truef(t, valueLooksLikeSecret(s), "expected secret-like: %q", s)
}
negatives := []string{
"",
"main",
"https://github.com/org/repo.git",
"azkv://heicode-kv.vault.azure.net/secrets/repo",
"a normal sentence with sk in it",
}
for _, s := range negatives {
require.Falsef(t, valueLooksLikeSecret(s), "expected NOT secret-like: %q", s)
}
// Value hidden under an innocuous key must now be caught.
require.True(t, containsPlaintextSecret(map[string]any{"note": "sk-abcdefghij1234567890"}))
require.False(t, containsPlaintextSecret(map[string]any{"repo_url": "https://github.com/o/r.git"}))
}
// P5: a single source of truth for the default model; no placeholder fallback.
func TestDefaultAgentModelID_SingleSourceNoPlaceholder(t *testing.T) {
def := defaultAgentModelID()
require.Equal(t, "gpt-5.4", def)
// Draft builder must use the single default, never agent-model-<role>.
plan := buildAgentDraftAgentPlan("backend", "", nil)
require.Equal(t, def, plan.DefaultModelID)
require.NotContains(t, plan.DefaultModelID, "agent-model-")
// Explicit client model is still honored.
plan = buildAgentDraftAgentPlan("backend", "gpt-5.4-mini", nil)
require.Equal(t, "gpt-5.4-mini", plan.DefaultModelID)
// Every role template resolves to the single default, no claude-* hardcoding.
for _, tpl := range agentRoleTemplates() {
require.Equal(t, def, tpl.DefaultModel, "role %s", tpl.Key)
}
}