Files
heicode-mananger/heicode/controller/agnet_control_plane_test.go
T

328 lines
11 KiB
Go

package controller
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/stretchr/testify/require"
)
type agnetCreateTestEnvelope struct {
Success bool `json:"success"`
Message string `json:"message"`
Error agnetTestError `json:"error"`
}
type agnetTestError struct {
Code string `json:"code"`
Message string `json:"message"`
}
func resetAgnetControlPlaneState(t *testing.T) {
t.Helper()
agnetMu.Lock()
defer agnetMu.Unlock()
agnetDeployments = make(map[string]agnetDeploymentRecord)
agnetSnapshots = make(map[string][]agnetSKSnapshot)
// Audit rows live in DB now; tests use a fresh SQLite per case
// (setupTokenControllerTestDB style) so there is no global map
// to flush here.
}
func baseAgnetResourceGrantPlan() agnetOrchestrationPlan {
return agnetOrchestrationPlan{
IntentID: "intent-resource-grant",
TemplateHint: "manager-resource-binding",
Objective: "Grant bounded resources to a child Agnet role",
RiskLevel: agnetRiskLow,
Budget: agnetBudget{
MaxTokens: 10000,
MaxCostUSD: 10,
MaxDurationSec: 3600,
},
UserContext: agnetUserContext{
UserID: "user-p1",
Email: "builder@example.invalid",
Role: "user",
ChannelID: "channel-p1",
},
BillingContext: agnetBillingContext{
Provider: "newapi",
NewAPIUserRef: "newapi-user-p1",
NewAPIGroup: "development",
QuotaRef: "newapi-quota-ref-p1",
},
AgentRuntime: agnetAgentRuntime{
Platform: "agnet",
Agents: []agnetRuntimeAgent{
{
Role: "builder",
ModelRef: "agnet-model-profile-builder",
InstanceCount: 1,
},
},
},
Agents: []agnetAgentPlan{
{
RoleTemplate: "builder",
Goal: "Use granted resources without receiving plaintext credentials",
DefaultModelID: "gpt-resource-test",
ResourceGrants: []agnetResourceGrant{
{
GrantID: "grant-git-builder",
ResourceID: "res-git-main",
ResourceType: agnetResourceGit,
UserID: "user-p1",
BindingScope: "https://example.invalid/acme/project.git#main",
TargetRole: "builder",
TargetAgentRef: "agent-builder-1",
PermissionScope: []string{"repo:read", "repo:write:feature-branches"},
Constraints: map[string]string{
"allowed_paths": "src/**,docs/**",
"ref": "main",
},
Metadata: map[string]string{
"provider": "gitee",
"repo_url": "https://example.invalid/acme/project.git",
},
Status: agnetGrantStatusActive,
SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main",
Audit: map[string]string{
"created_by": "manager-test",
},
},
{
GrantID: "grant-doc-builder",
ResourceID: "res-doc-plan",
ResourceType: agnetResourceProjectDoc,
UserID: "user-p1",
BindingScope: "docs/heicode.md",
TargetRole: "builder",
TargetAgentRef: "agent-builder-1",
PermissionScope: []string{"doc:read"},
Metadata: map[string]string{
"doc_ref": "docs/heicode.md",
},
Status: agnetGrantStatusActive,
},
},
},
},
Constraints: agnetConstraints{AllowedModelIDs: []string{"gpt-resource-test"}},
Metadata: agnetMetadata{
CorrelationID: "corr-p1-resource-grant",
},
}
}
func postAgnetCreateDeployment(t *testing.T, plan agnetOrchestrationPlan) (*httptest.ResponseRecorder, agnetCreateTestEnvelope) {
t.Helper()
gin.SetMode(gin.TestMode)
body, err := common.Marshal(agnetDeploymentRequest{Plan: plan})
require.NoError(t, err)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/deployments", strings.NewReader(string(body)))
ctx.Request.Header.Set("Content-Type", "application/json")
AgnetCreateDeployment(ctx)
var envelope agnetCreateTestEnvelope
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
return recorder, envelope
}
func TestAgnetCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) {
resetAgnetControlPlaneState(t)
recorder, envelope := postAgnetCreateDeployment(t, baseAgnetResourceGrantPlan())
require.Equal(t, http.StatusOK, recorder.Code)
require.True(t, envelope.Success)
agnetMu.RLock()
require.Len(t, agnetDeployments, 1)
var stored agnetDeploymentRecord
for _, record := range agnetDeployments {
stored = record
}
agnetMu.RUnlock()
grants := stored.Plan.Agents[0].ResourceGrants
require.Len(t, grants, 2)
require.Equal(t, agnetResourceGit, grants[0].ResourceType)
require.Equal(t, "user-p1", grants[0].UserID)
require.Equal(t, "https://example.invalid/acme/project.git#main", grants[0].BindingScope)
require.Equal(t, "builder", grants[0].TargetRole)
require.Equal(t, "agent-builder-1", grants[0].TargetAgentRef)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", grants[0].SecretRef)
require.Empty(t, grants[1].SecretRef, "project document grants should not require credential material")
require.Equal(t, "newapi", stored.Plan.BillingContext.Provider)
require.Equal(t, "channel-p1", stored.Plan.UserContext.ChannelID)
require.Equal(t, "agnet", stored.Plan.AgentRuntime.Platform)
require.Equal(t, "agnet-model-profile-builder", stored.Plan.AgentRuntime.Agents[0].ModelRef)
require.Equal(t, "queued", stored.RuntimeState)
require.Empty(t, stored.FailureReason)
require.Len(t, stored.AgentInstances, 1)
require.Equal(t, "queued", stored.AgentInstances[0].RuntimeState)
require.Len(t, stored.ResourceGrantManifest.ResourceGrants, 2)
require.Equal(t, "user-p1", stored.ResourceGrantManifest.UserID)
require.Equal(t, "builder", stored.ResourceGrantManifest.AgentRole)
require.Equal(t, "agent-builder-1", stored.ResourceGrantManifest.TargetAgentRef)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", stored.ResourceGrantManifest.ResourceGrants[0].SecretRef)
}
func TestAgnetPermissionManifestExcludesInactiveResourceGrants(t *testing.T) {
resetAgnetControlPlaneState(t)
plan := baseAgnetResourceGrantPlan()
plan.Agents[0].ResourceGrants[1].Status = agnetGrantStatusRevoked
_, envelope := postAgnetCreateDeployment(t, plan)
require.True(t, envelope.Success)
agnetMu.RLock()
var stored agnetDeploymentRecord
for _, record := range agnetDeployments {
stored = record
}
agnetMu.RUnlock()
require.Len(t, stored.ResourceGrantManifest.ResourceGrants, 1)
require.Equal(t, "grant-git-builder", stored.ResourceGrantManifest.ResourceGrants[0].GrantID)
}
func TestAgnetCreateDeploymentRejectsPlaintextResourceGrantCredentialFields(t *testing.T) {
resetAgnetControlPlaneState(t)
plan := baseAgnetResourceGrantPlan()
plan.Agents[0].ResourceGrants[0].Metadata["access_token"] = "must-not-be-stored"
_, envelope := postAgnetCreateDeployment(t, plan)
require.False(t, envelope.Success)
require.Equal(t, "RESOURCE_GRANT_SECRET_REJECTED", envelope.Error.Code)
require.Empty(t, agnetDeployments)
}
func TestAgnetCreateDeploymentRejectsResourceGrantWithoutSecretRef(t *testing.T) {
resetAgnetControlPlaneState(t)
plan := baseAgnetResourceGrantPlan()
plan.Agents[0].ResourceGrants[0].SecretRef = ""
_, envelope := postAgnetCreateDeployment(t, plan)
require.False(t, envelope.Success)
require.Equal(t, "RESOURCE_GRANT_SECRET_REF_REQUIRED", envelope.Error.Code)
require.Empty(t, agnetDeployments)
}
func TestAgnetCreateDeploymentRejectsMissingUserContext(t *testing.T) {
resetAgnetControlPlaneState(t)
plan := baseAgnetResourceGrantPlan()
plan.UserContext.UserID = ""
_, envelope := postAgnetCreateDeployment(t, plan)
require.False(t, envelope.Success)
require.Equal(t, "POLICY_REJECTED", envelope.Error.Code)
require.Empty(t, agnetDeployments)
}
func TestAgnetCreateDeploymentRejectsResourceGrantUserMismatch(t *testing.T) {
resetAgnetControlPlaneState(t)
plan := baseAgnetResourceGrantPlan()
plan.Agents[0].ResourceGrants[0].UserID = "user-other"
_, envelope := postAgnetCreateDeployment(t, plan)
require.False(t, envelope.Success)
require.Equal(t, "RESOURCE_GRANT_INVALID", envelope.Error.Code)
require.Empty(t, agnetDeployments)
}
func TestAgnetCreateDeploymentRejectsResourceGrantWithoutBindingScope(t *testing.T) {
resetAgnetControlPlaneState(t)
plan := baseAgnetResourceGrantPlan()
plan.Agents[0].ResourceGrants[0].BindingScope = ""
_, envelope := postAgnetCreateDeployment(t, plan)
require.False(t, envelope.Success)
require.Equal(t, "RESOURCE_GRANT_INVALID", envelope.Error.Code)
require.Empty(t, agnetDeployments)
}
func TestAgnetCreateDeploymentRejectsNewAPIBillingWithoutUserMapping(t *testing.T) {
resetAgnetControlPlaneState(t)
plan := baseAgnetResourceGrantPlan()
plan.UserContext.ChannelID = ""
plan.BillingContext.NewAPIUserRef = ""
plan.BillingContext.NewAPIGroup = ""
plan.BillingContext.QuotaRef = ""
_, envelope := postAgnetCreateDeployment(t, plan)
require.False(t, envelope.Success)
require.Equal(t, "BILLING_CONTEXT_INVALID", envelope.Error.Code)
require.Empty(t, agnetDeployments)
}
func TestAgnetCreateDeploymentRejectsRuntimeRoleOutsideAgentPlan(t *testing.T) {
resetAgnetControlPlaneState(t)
plan := baseAgnetResourceGrantPlan()
plan.AgentRuntime.Agents[0].Role = "ops"
_, envelope := postAgnetCreateDeployment(t, plan)
require.False(t, envelope.Success)
require.Equal(t, "AGENT_RUNTIME_INVALID", envelope.Error.Code)
require.Empty(t, agnetDeployments)
}
func TestAgnetDeploymentLogsAndMetricsExposeRedactedReadiness(t *testing.T) {
resetAgnetControlPlaneState(t)
_, envelope := postAgnetCreateDeployment(t, baseAgnetResourceGrantPlan())
require.True(t, envelope.Success)
agnetMu.RLock()
var deploymentID string
for id := range agnetDeployments {
deploymentID = id
break
}
agnetMu.RUnlock()
require.NotEmpty(t, deploymentID)
logRecorder := httptest.NewRecorder()
logCtx, _ := gin.CreateTestContext(logRecorder)
logCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/deployments/"+deploymentID+"/logs", nil)
logCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
AgnetListDeploymentLogs(logCtx)
require.Equal(t, http.StatusOK, logRecorder.Code)
require.Contains(t, logRecorder.Body.String(), `"redacted":true`)
require.NotContains(t, strings.ToLower(logRecorder.Body.String()), "password")
require.NotContains(t, strings.ToLower(logRecorder.Body.String()), "token")
metricRecorder := httptest.NewRecorder()
metricCtx, _ := gin.CreateTestContext(metricRecorder)
metricCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/deployments/"+deploymentID+"/metrics?window=15m&step=60s", nil)
metricCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
AgnetGetDeploymentMetrics(metricCtx)
require.Equal(t, http.StatusOK, metricRecorder.Code)
require.Contains(t, metricRecorder.Body.String(), `"deployment_id":"`+deploymentID+`"`)
require.Contains(t, metricRecorder.Body.String(), `"platform_estimated":true`)
require.Contains(t, metricRecorder.Body.String(), `"runtime_state":"queued"`)
require.Contains(t, metricRecorder.Body.String(), `"failure_reason":""`)
}