328 lines
11 KiB
Go
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":""`)
|
|
}
|