Files
heicode-mananger/heicode/model/agent_audit_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

144 lines
4.0 KiB
Go

package model
import (
"fmt"
"testing"
"time"
)
// The model package shares a single in-memory SQLite across all tests
// via TestMain in task_cas_test.go. We MUST NOT swap DB out or null
// it in Cleanup — doing so wedges every other test in the package.
//
// Strategy: AutoMigrate the audit table once (idempotent on SQLite),
// then DELETE rows at the top of each test to isolate cases.
func setupAuditTest(t *testing.T) {
t.Helper()
if err := DB.AutoMigrate(&AgentAuditEvent{}); err != nil {
t.Fatalf("migrate: %v", err)
}
if err := DB.Exec("DELETE FROM agent_audit_events").Error; err != nil {
t.Fatalf("truncate: %v", err)
}
}
func TestInsertAgentAuditEvent_PersistsRow(t *testing.T) {
// Regression for H1 — the previous implementation appended to an
// in-process map that was wiped on every container restart. This
// test pins the new behaviour: rows survive in DB.
setupAuditTest(t)
evt := &AgentAuditEvent{
EventID: "evt_test_001",
Event: "deployment.accepted",
Actor: "agent_control_plane",
Resource: "dep_abc",
UserID: "user-42",
ChannelID: "channel-1",
BindingScope: "project-main",
DeploymentID: "dep_abc",
CorrelationID: "corr-xyz",
RequestID: "req-001",
}
InsertAgentAuditEvent(evt)
var got AgentAuditEvent
if err := DB.Where("event_id = ?", "evt_test_001").First(&got).Error; err != nil {
t.Fatalf("not persisted: %v", err)
}
if got.Result != "ok" {
t.Errorf("default Result should be 'ok', got %q", got.Result)
}
if got.SchemaVersion != 1 {
t.Errorf("default SchemaVersion should be 1, got %d", got.SchemaVersion)
}
if got.OccurredAt == 0 {
t.Errorf("OccurredAt should be auto-stamped, got 0")
}
}
func TestInsertAgentAuditEvent_NilGuards(t *testing.T) {
// Production safety: audit writes run inside hot paths (right
// after a deployment mutation). A nil DB or nil event MUST NOT
// panic — better to drop the audit row than to fail the user
// API call.
defer func() {
if r := recover(); r != nil {
t.Fatalf("InsertAgentAuditEvent panicked: %v", r)
}
}()
InsertAgentAuditEvent(nil) // nil evt
prev := DB
DB = nil
InsertAgentAuditEvent(&AgentAuditEvent{EventID: "x"}) // nil DB
DB = prev
}
func TestListAgentAuditEvents_FilterAndPaginate(t *testing.T) {
setupAuditTest(t)
now := time.Now().UnixMilli()
for i := 0; i < 5; i++ {
uid := "user-A"
if i%2 == 0 {
uid = "user-B"
}
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: fmt.Sprintf("evt_%d", i),
Event: "deployment.accepted",
UserID: uid,
DeploymentID: "dep_X",
OccurredAt: now + int64(i)*1000,
})
}
// All rows visible without filter.
rows, total, err := ListAgentAuditEvents(ListAgentAuditEventsFilter{})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 5 || len(rows) != 5 {
t.Errorf("expected 5 rows, got total=%d len=%d", total, len(rows))
}
// user_id filter narrows to 3 (i=0,2,4 → user-B).
_, total, err = ListAgentAuditEvents(ListAgentAuditEventsFilter{UserID: "user-B"})
if err != nil {
t.Fatalf("list with userid: %v", err)
}
if total != 3 {
t.Errorf("user-B should have 3 rows, got %d", total)
}
// Newest-first ordering. evt_4 inserted last → top.
rows, _, _ = ListAgentAuditEvents(ListAgentAuditEventsFilter{})
if rows[0].EventID != "evt_4" {
t.Errorf("expected newest-first, got %q on top", rows[0].EventID)
}
}
func TestListAgentAuditEventsByDeployment_Chronological(t *testing.T) {
// Detail-drawer reads need oldest-first for a top-to-bottom
// timeline. Confirms ascending order independent of insertion
// order.
setupAuditTest(t)
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: "evt_late", Event: "x", DeploymentID: "dep_T",
OccurredAt: 9000,
})
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: "evt_early", Event: "x", DeploymentID: "dep_T",
OccurredAt: 1000,
})
rows, err := ListAgentAuditEventsByDeployment("dep_T")
if err != nil {
t.Fatalf("query: %v", err)
}
if len(rows) != 2 || rows[0].EventID != "evt_early" {
t.Fatalf("expected [evt_early, evt_late] order, got %+v", rows)
}
}