1995 lines
80 KiB
Go
1995 lines
80 KiB
Go
package controller
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/heicode/manager/common"
|
|
"github.com/heicode/manager/model"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type agentCreateTestEnvelope struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
Error agentTestError `json:"error"`
|
|
}
|
|
|
|
type agentTestError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func resetAgentControlPlaneState(t *testing.T) {
|
|
t.Helper()
|
|
|
|
agentMu.Lock()
|
|
defer agentMu.Unlock()
|
|
agentDeployments = make(map[string]agentDeploymentRecord)
|
|
agentSnapshots = make(map[string][]agentSKSnapshot)
|
|
// 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 setupAgentControlPlaneTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
common.UsingSQLite = true
|
|
common.UsingMySQL = false
|
|
common.UsingPostgreSQL = false
|
|
common.RedisEnabled = false
|
|
|
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
|
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
model.DB = db
|
|
model.LOG_DB = db
|
|
require.NoError(t, db.AutoMigrate(
|
|
&model.ResourceBinding{},
|
|
&model.ResourceGrant{},
|
|
&model.AgentAuditEvent{},
|
|
&model.AgentDeployment{},
|
|
&model.AgentCallbackEvent{},
|
|
&model.AgentArtifact{},
|
|
&model.AgentSKSnapshot{},
|
|
&model.AgentApprovalRequest{},
|
|
))
|
|
t.Cleanup(func() {
|
|
if model.DB == db {
|
|
model.DB = nil
|
|
}
|
|
if model.LOG_DB == db {
|
|
model.LOG_DB = nil
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
})
|
|
return db
|
|
}
|
|
|
|
func baseAgentResourceGrantPlan() agentOrchestrationPlan {
|
|
return agentOrchestrationPlan{
|
|
IntentID: "intent-resource-grant",
|
|
TemplateHint: "manager-resource-binding",
|
|
Objective: "Grant bounded resources to a child Agent role",
|
|
RiskLevel: agentRiskLow,
|
|
Budget: agentBudget{
|
|
MaxTokens: 10000,
|
|
MaxCostUSD: 10,
|
|
MaxDurationSec: 3600,
|
|
},
|
|
UserContext: agentUserContext{
|
|
UserID: "user-p1",
|
|
Email: "builder@example.invalid",
|
|
Role: "user",
|
|
ChannelID: "channel-p1",
|
|
},
|
|
BillingContext: agentBillingContext{
|
|
Provider: "newapi",
|
|
NewAPIUserRef: "newapi-user-p1",
|
|
NewAPIGroup: "development",
|
|
QuotaRef: "newapi-quota-ref-p1",
|
|
},
|
|
AgentRuntime: agentAgentRuntime{
|
|
Platform: "agent",
|
|
Agents: []agentRuntimeAgent{
|
|
{
|
|
Role: "builder",
|
|
ModelRef: "agent-model-profile-builder",
|
|
InstanceCount: 1,
|
|
},
|
|
},
|
|
},
|
|
Agents: []agentAgentPlan{
|
|
{
|
|
RoleTemplate: "builder",
|
|
Goal: "Use granted resources without receiving plaintext credentials",
|
|
DefaultModelID: "gpt-resource-test",
|
|
ResourceGrants: []agentResourceGrant{
|
|
{
|
|
GrantID: "grant-git-builder",
|
|
ResourceID: "res-git-main",
|
|
ResourceType: agentResourceGit,
|
|
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: agentGrantStatusActive,
|
|
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: agentResourceProjectDoc,
|
|
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: agentGrantStatusActive,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Constraints: agentConstraints{AllowedModelIDs: []string{"gpt-resource-test"}},
|
|
Metadata: agentMetadata{
|
|
CorrelationID: "corr-p1-resource-grant",
|
|
},
|
|
}
|
|
}
|
|
|
|
func postAgentCreateDeployment(t *testing.T, plan agentOrchestrationPlan) (*httptest.ResponseRecorder, agentCreateTestEnvelope) {
|
|
t.Helper()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
body, err := common.Marshal(agentDeploymentRequest{Plan: plan})
|
|
require.NoError(t, err)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/deployments", strings.NewReader(string(body)))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
AgentCreateDeployment(ctx)
|
|
|
|
var envelope agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
|
|
return recorder, envelope
|
|
}
|
|
|
|
func postAgentCreateUserDeployment(t *testing.T, userID int, plan agentOrchestrationPlan) (*httptest.ResponseRecorder, agentCreateTestEnvelope) {
|
|
t.Helper()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
body, err := common.Marshal(agentDeploymentRequest{Plan: plan})
|
|
require.NoError(t, err)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Set("id", userID)
|
|
ctx.Set("group", "development")
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/user/deployments", strings.NewReader(string(body)))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
AgentCreateUserDeployment(ctx)
|
|
|
|
var envelope agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
|
|
return recorder, envelope
|
|
}
|
|
|
|
func TestAgentDeploymentSurvivesInProcessStateReset(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, baseAgentResourceGrantPlan())
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
require.NotEmpty(t, deploymentID)
|
|
|
|
var stored model.AgentDeployment
|
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
|
require.Equal(t, "user-p1", stored.UserID)
|
|
require.Equal(t, "channel-p1", stored.ChannelID)
|
|
require.Contains(t, stored.PayloadJSON, `"orchestration_plan"`)
|
|
require.Contains(t, stored.PermissionManifestJSON, `"grant-git-builder"`)
|
|
require.Contains(t, stored.PermissionManifestJSON, `"secret_ref"`)
|
|
require.NotContains(t, strings.ToLower(stored.PayloadJSON), "password")
|
|
require.NotContains(t, strings.ToLower(stored.PayloadJSON), "private_key")
|
|
require.NotContains(t, strings.ToLower(stored.PermissionManifestJSON), "access_token")
|
|
|
|
resetAgentControlPlaneState(t)
|
|
|
|
getRecorder := httptest.NewRecorder()
|
|
getCtx, _ := gin.CreateTestContext(getRecorder)
|
|
getCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
getCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/deployments/"+deploymentID, nil)
|
|
AgentGetDeployment(getCtx)
|
|
|
|
var getBody map[string]any
|
|
require.NoError(t, common.Unmarshal(getRecorder.Body.Bytes(), &getBody))
|
|
require.Equal(t, true, getBody["success"])
|
|
data := getBody["data"].(map[string]any)
|
|
require.Equal(t, deploymentID, data["deployment_id"])
|
|
require.Equal(t, "accepted", data["status"])
|
|
|
|
listRecorder := httptest.NewRecorder()
|
|
listCtx, _ := gin.CreateTestContext(listRecorder)
|
|
listCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/deployments?user_id=user-p1", nil)
|
|
AgentListDeployments(listCtx)
|
|
|
|
var listBody map[string]any
|
|
require.NoError(t, common.Unmarshal(listRecorder.Body.Bytes(), &listBody))
|
|
require.Equal(t, true, listBody["success"])
|
|
listData := listBody["data"].(map[string]any)
|
|
require.Equal(t, float64(1), listData["total"])
|
|
}
|
|
|
|
func TestAgentDeploymentPersistsSubMode(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.SubMode = "waterfall"
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, plan)
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
require.Contains(t, recorder.Body.String(), `"sub_mode":"waterfall"`)
|
|
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
var stored model.AgentDeployment
|
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
|
require.Equal(t, "waterfall", stored.SubMode)
|
|
|
|
resetAgentControlPlaneState(t)
|
|
getRecorder := httptest.NewRecorder()
|
|
getCtx, _ := gin.CreateTestContext(getRecorder)
|
|
getCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
getCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/deployments/"+deploymentID, nil)
|
|
AgentGetDeployment(getCtx)
|
|
|
|
require.Equal(t, http.StatusOK, getRecorder.Code)
|
|
require.Contains(t, getRecorder.Body.String(), `"sub_mode":"waterfall"`)
|
|
}
|
|
|
|
func TestAgentDeploymentDefaultsSubModeToAgile(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.SubMode = ""
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, plan)
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
require.Contains(t, recorder.Body.String(), `"sub_mode":"agile"`)
|
|
}
|
|
|
|
func TestAgentRuntimeHealthNotConfigured(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", "")
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/runtime/health", nil)
|
|
|
|
AgentRuntimeHealth(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"status":"not_configured"`)
|
|
require.Contains(t, recorder.Body.String(), `"configured":false`)
|
|
}
|
|
|
|
func TestAgentRuntimeHealthProxy(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "/api/agent/health", r.URL.Path)
|
|
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"status":"healthy"}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", server.URL)
|
|
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "service-token")
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/runtime/health", nil)
|
|
|
|
AgentRuntimeHealth(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"status":"healthy"`)
|
|
require.Contains(t, recorder.Body.String(), `"http_status":200`)
|
|
require.Contains(t, recorder.Body.String(), `"remote"`)
|
|
}
|
|
|
|
func TestAgentRuntimeShadowCreateStoresRuntimeMapping(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "/api/agent/sub-agile/deployments", r.URL.Path)
|
|
require.Equal(t, http.MethodPost, r.Method)
|
|
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
|
|
require.NotEmpty(t, r.Header.Get("X-Correlation-ID"))
|
|
body, err := io.ReadAll(r.Body)
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(body), `"sub_mode":"agile"`)
|
|
require.Contains(t, string(body), `"manager_deployment_id"`)
|
|
var runtimeBody map[string]any
|
|
require.NoError(t, common.Unmarshal(body, &runtimeBody))
|
|
agents := runtimeBody["agents"].([]any)
|
|
require.Equal(t, "builder", agents[0].(map[string]any)["role"])
|
|
metadata := runtimeBody["metadata"].(map[string]any)
|
|
require.NotEmpty(t, metadata["manager_deployment_id"])
|
|
grants := runtimeBody["resource_grants"].([]any)
|
|
require.NotEmpty(t, grants)
|
|
require.NotEmpty(t, grants[0].(map[string]any)["ref"])
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"deployment_id":"runtime-dep-1","swarm_id":"swarm-1","runtime_status":"accepted"}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
|
|
t.Setenv("AGENT_RUNTIME_ASYNC", "false")
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", server.URL)
|
|
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "service-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.SubMode = "agile"
|
|
recorder, envelope := postAgentCreateDeployment(t, plan)
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
require.Contains(t, recorder.Body.String(), `"runtime_deployment_id":"runtime-dep-1"`)
|
|
require.Contains(t, recorder.Body.String(), `"runtime_swarm_id":"swarm-1"`)
|
|
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
var stored model.AgentDeployment
|
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
|
require.Equal(t, "runtime-dep-1", stored.RuntimeDeploymentID)
|
|
require.Equal(t, "swarm-1", stored.RuntimeSwarmID)
|
|
require.Equal(t, "accepted", stored.RuntimeState)
|
|
}
|
|
|
|
func TestAgentRuntimeDiagnosticsWarnsOnCompletedRuntimeWithFailedAgents(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/sub-agile/deployments":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"deployment_id":"runtime-dep-1","swarm_id":"swarm-1","status":"running"}}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/swarms/swarm-1/status":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"deployment_id":"swarm-1",
|
|
"swarm_id":"swarm-1",
|
|
"status":"completed",
|
|
"phase":"development",
|
|
"agents":[{"agent_id":"agi_backend_1","role":"backend","status":"failed","output":"Cannot connect to host agent svc"}],
|
|
"artifacts":[{"artifact_id":"art_summary","artifact_type":"document","title":"Runtime execution summary","summary":"Runtime completed without per-agent artifacts; review swarm logs for details.","uri":"runtime://swarm-1/artifacts/summary"}],
|
|
"metrics":{"tokens_used":0}
|
|
}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
|
|
t.Setenv("AGENT_RUNTIME_ASYNC", "false")
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", server.URL)
|
|
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "service-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.ResourceGrants {
|
|
plan.ResourceGrants[idx].UserID = "7"
|
|
}
|
|
for agentIdx := range plan.Agents {
|
|
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
|
|
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = "7"
|
|
}
|
|
}
|
|
createRecorder, envelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, envelope.Success, createRecorder.Body.String())
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Set("id", 7)
|
|
ctx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments/"+deploymentID+"/runtime-diagnostics", nil)
|
|
|
|
AgentGetUserDeploymentRuntimeDiagnostics(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"data_source":"runtime_status"`)
|
|
require.Contains(t, recorder.Body.String(), `"runtime_agent_failed"`)
|
|
require.Contains(t, recorder.Body.String(), `"runtime_completed_with_failed_agents"`)
|
|
require.Contains(t, recorder.Body.String(), `"runtime_summary_artifact_only"`)
|
|
require.Contains(t, recorder.Body.String(), `"runtime_zero_model_usage"`)
|
|
}
|
|
|
|
func TestAgentArtifactContentProxiesRuntimeContent(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/sub-agile/deployments":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"deployment_id":"runtime-dep-1","swarm_id":"swarm-1","status":"running"}}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/swarms/swarm-1/artifacts/art-1/content":
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", `attachment; filename="art-1.txt"`)
|
|
_, _ = w.Write([]byte("artifact body"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
|
|
t.Setenv("AGENT_RUNTIME_ASYNC", "false")
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", server.URL)
|
|
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "service-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.ResourceGrants {
|
|
plan.ResourceGrants[idx].UserID = "7"
|
|
}
|
|
for agentIdx := range plan.Agents {
|
|
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
|
|
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = "7"
|
|
}
|
|
}
|
|
createRecorder, envelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, envelope.Success, createRecorder.Body.String())
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
require.NoError(t, model.UpsertAgentArtifact(&model.AgentArtifact{
|
|
ArtifactID: "art-1",
|
|
DeploymentID: deploymentID,
|
|
UserID: "7",
|
|
URI: "runtime://swarm-1/artifacts/art-1",
|
|
CreatedAtMs: time.Now().UnixMilli(),
|
|
}))
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Set("id", 7)
|
|
ctx.Params = gin.Params{
|
|
{Key: "deployment_id", Value: deploymentID},
|
|
{Key: "artifact_id", Value: "art-1"},
|
|
}
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments/"+deploymentID+"/artifacts/art-1/content", nil)
|
|
|
|
AgentGetUserDeploymentArtifactContent(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Equal(t, "artifact body", recorder.Body.String())
|
|
require.Equal(t, "text/plain; charset=utf-8", recorder.Header().Get("Content-Type"))
|
|
require.Equal(t, `attachment; filename="art-1.txt"`, recorder.Header().Get("Content-Disposition"))
|
|
}
|
|
|
|
func TestAgentRuntimeShadowCreateFailureDoesNotFailLocalDeployment(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "runtime unavailable", http.StatusBadGateway)
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
|
|
t.Setenv("AGENT_RUNTIME_ASYNC", "false")
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", server.URL)
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, baseAgentResourceGrantPlan())
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
require.Contains(t, recorder.Body.String(), `"runtime_state":"runtime_sync_failed"`)
|
|
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
var stored model.AgentDeployment
|
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
|
require.Equal(t, agentRuntimeStateFailed, stored.RuntimeState)
|
|
require.Contains(t, stored.FailureReason, "HTTP 502")
|
|
}
|
|
|
|
func TestAgentRuntimeShadowCreateTreatsSuccessFalseAsFailure(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":false,"error":{"code":"POLICY_REJECTED","message":"sub mode rejected"}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
|
|
t.Setenv("AGENT_RUNTIME_ASYNC", "false")
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", server.URL)
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, baseAgentResourceGrantPlan())
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
var stored model.AgentDeployment
|
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
|
require.Equal(t, agentRuntimeStateFailed, stored.RuntimeState)
|
|
require.Contains(t, stored.FailureReason, "sub mode rejected")
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsInvalidSubMode(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.SubMode = "scrum"
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "POLICY_REJECTED", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsTooManyAgents(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
// 9 agents > default per-plan cap of 8. The size guard short-circuits before
|
|
// per-agent validation, so the filler agents need no further detail. (#8)
|
|
plan := baseAgentResourceGrantPlan()
|
|
for i := 0; i < 8; i++ {
|
|
plan.Agents = append(plan.Agents, agentAgentPlan{
|
|
RoleTemplate: "extra-" + strconv.Itoa(i),
|
|
Goal: "filler",
|
|
})
|
|
}
|
|
require.Len(t, plan.Agents, 9)
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "POLICY_REJECTED", envelope.Error.Code)
|
|
require.Contains(t, envelope.Error.Message, "maximum of 8 agents")
|
|
require.Empty(t, agentDeployments) // rejected before any deployment is persisted
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsNonAzureSecretRef(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.Agents[0].ResourceGrants[0].SecretRef = "vault:heicode/repo-main"
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "SECRET_REF_INVALID", envelope.Error.Code)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentAcceptsTopLevelResourceGrants(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.ResourceGrants = plan.Agents[0].ResourceGrants
|
|
plan.Agents[0].ResourceGrants = nil
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
require.Contains(t, recorder.Body.String(), `"resource_grants":[`)
|
|
require.Contains(t, recorder.Body.String(), `"grant_id":"grant-git-builder"`)
|
|
require.Contains(t, recorder.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main"`)
|
|
}
|
|
|
|
func TestAgentUserDeploymentForcesAuthenticatedUserScope(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = ""
|
|
plan.UserContext.ChannelID = ""
|
|
for agentIdx := range plan.Agents {
|
|
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
|
|
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = ""
|
|
}
|
|
}
|
|
|
|
recorder, envelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
var stored model.AgentDeployment
|
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
|
require.Equal(t, "7", stored.UserID)
|
|
require.Equal(t, "development", stored.ChannelID)
|
|
require.Contains(t, stored.PlanJSON, `"user_id":"7"`)
|
|
require.NotContains(t, stored.PlanJSON, `"user_id":"user-p1"`)
|
|
}
|
|
|
|
func TestAgentUserDeploymentRejectsMismatchedUserScope(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "8"
|
|
|
|
_, envelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "USER_CONTEXT_FORBIDDEN", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentUserDeploymentListIsScopedToAuthenticatedUser(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
user7Plan := baseAgentResourceGrantPlan()
|
|
user7Plan.UserContext.UserID = "7"
|
|
user7Plan.Metadata.CorrelationID = "corr-user-7"
|
|
for idx := range user7Plan.Agents[0].ResourceGrants {
|
|
user7Plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
_, user7Envelope := postAgentCreateDeployment(t, user7Plan)
|
|
require.True(t, user7Envelope.Success)
|
|
|
|
user8Plan := baseAgentResourceGrantPlan()
|
|
user8Plan.UserContext.UserID = "8"
|
|
user8Plan.Metadata.CorrelationID = "corr-user-8"
|
|
for idx := range user8Plan.Agents[0].ResourceGrants {
|
|
user8Plan.Agents[0].ResourceGrants[idx].UserID = "8"
|
|
}
|
|
_, user8Envelope := postAgentCreateDeployment(t, user8Plan)
|
|
require.True(t, user8Envelope.Success)
|
|
|
|
listRecorder := httptest.NewRecorder()
|
|
listCtx, _ := gin.CreateTestContext(listRecorder)
|
|
listCtx.Set("id", 7)
|
|
listCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments", nil)
|
|
AgentListUserDeployments(listCtx)
|
|
|
|
require.Equal(t, http.StatusOK, listRecorder.Code)
|
|
var listBody map[string]any
|
|
require.NoError(t, common.Unmarshal(listRecorder.Body.Bytes(), &listBody))
|
|
require.Equal(t, true, listBody["success"])
|
|
listData := listBody["data"].(map[string]any)
|
|
require.Equal(t, float64(1), listData["total"])
|
|
items := listData["items"].([]any)
|
|
item := items[0].(map[string]any)
|
|
planData := item["orchestration_plan"].(map[string]any)
|
|
userContext := planData["user_context"].(map[string]any)
|
|
require.Equal(t, "7", userContext["user_id"])
|
|
}
|
|
|
|
func TestAgentTaskDeploymentDraftBuildsSafePlan(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
body := `{
|
|
"task":{"id":"task_bridge_1","name":"Ship Manager sub mode","intent":"Need a scoped implementation","status":"running","card":{"goal":"Implement the Manager-side sub task flow"}},
|
|
"sub_mode":"waterfall",
|
|
"binding_scope":"repo-main",
|
|
"role_templates":["backend"],
|
|
"default_model_id":"agent-model-builder",
|
|
"budget":{"max_tokens":10000,"max_cost_usd":2,"max_duration_sec":900}
|
|
}`
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Set("id", 7)
|
|
ctx.Set("group", "development")
|
|
ctx.Params = gin.Params{{Key: "task_id", Value: "task_bridge_1"}}
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/user/tasks/task_bridge_1/deployment-draft", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
AgentCreateTaskDeploymentDraft(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.NotContains(t, strings.ToLower(recorder.Body.String()), "password")
|
|
require.NotContains(t, strings.ToLower(recorder.Body.String()), "access_token")
|
|
var env map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &env))
|
|
require.Equal(t, true, env["success"])
|
|
data := env["data"].(map[string]any)
|
|
plan := data["orchestration_plan"].(map[string]any)
|
|
require.Equal(t, "task_bridge_1", plan["intent_id"])
|
|
require.Equal(t, "waterfall", plan["sub_mode"])
|
|
require.Equal(t, "Implement the Manager-side sub task flow", plan["objective"])
|
|
userContext := plan["user_context"].(map[string]any)
|
|
require.Equal(t, "7", userContext["user_id"])
|
|
agents := plan["agents"].([]any)
|
|
agent := agents[0].(map[string]any)
|
|
grants := agent["resource_grants"].([]any)
|
|
grant := grants[0].(map[string]any)
|
|
require.Equal(t, "project_doc", grant["resource_type"])
|
|
require.Equal(t, "7", grant["user_id"])
|
|
}
|
|
|
|
func TestAgentTaskDeploymentDraftRequiresTaskSnapshot(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Set("id", 7)
|
|
ctx.Params = gin.Params{{Key: "task_id", Value: "task_missing"}}
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/user/tasks/task_missing/deployment-draft", strings.NewReader(`{"sub_mode":"agile"}`))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
AgentCreateTaskDeploymentDraft(ctx)
|
|
|
|
var envelope agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "TASK_NOT_FOUND", envelope.Error.Code)
|
|
}
|
|
|
|
func TestAgentTaskDeploymentDraftRejectsCredentialGrantWithoutSecretRef(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
body := `{
|
|
"task":{"id":"task_bridge_git","name":"Use a git repo","intent":"Need repo access","status":"running"},
|
|
"sub_mode":"agile",
|
|
"binding_scope":"repo-main",
|
|
"role_templates":["backend"],
|
|
"default_model_id":"agent-model-builder",
|
|
"resource_grants":[{
|
|
"resource_id":"repo-1",
|
|
"resource_type":"git",
|
|
"permission_scope":["repo:read"]
|
|
}]
|
|
}`
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Set("id", 7)
|
|
ctx.Params = gin.Params{{Key: "task_id", Value: "task_bridge_git"}}
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/user/tasks/task_bridge_git/deployment-draft", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
AgentCreateTaskDeploymentDraft(ctx)
|
|
|
|
var envelope agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "RESOURCE_GRANT_SECRET_REF_REQUIRED", envelope.Error.Code)
|
|
}
|
|
|
|
func TestAgentUserDeploymentSimulatedEventsArePersistedAndPrefixed(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.Equal(t, http.StatusOK, createRecorder.Code)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
simRecorder := httptest.NewRecorder()
|
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
|
simCtx.Set("id", 7)
|
|
simCtx.Set("role", common.RoleAdminUser)
|
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{"events":["deployment.started","simulation.agent.done"]}`))
|
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
|
AgentSimulateUserDeploymentEvents(simCtx)
|
|
|
|
require.Equal(t, http.StatusOK, simRecorder.Code)
|
|
require.Contains(t, simRecorder.Body.String(), `"simulated":true`)
|
|
require.Contains(t, simRecorder.Body.String(), `"simulation.deployment.started"`)
|
|
require.Contains(t, simRecorder.Body.String(), `"simulation.agent.done"`)
|
|
|
|
resetAgentControlPlaneState(t)
|
|
eventsRecorder := httptest.NewRecorder()
|
|
eventsCtx, _ := gin.CreateTestContext(eventsRecorder)
|
|
eventsCtx.Set("id", 7)
|
|
eventsCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
eventsCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments/"+deploymentID+"/events", nil)
|
|
AgentListUserDeploymentEvents(eventsCtx)
|
|
|
|
require.Equal(t, http.StatusOK, eventsRecorder.Code)
|
|
require.Contains(t, eventsRecorder.Body.String(), `"simulation.deployment.started"`)
|
|
require.Contains(t, eventsRecorder.Body.String(), `"simulation.agent.done"`)
|
|
}
|
|
|
|
func TestAgentUserDeploymentSimulationRejectsOtherUsersDeployment(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.Equal(t, http.StatusOK, createRecorder.Code)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
simRecorder := httptest.NewRecorder()
|
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
|
simCtx.Set("id", 8)
|
|
simCtx.Set("role", common.RoleAdminUser)
|
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{}`))
|
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
|
AgentSimulateUserDeploymentEvents(simCtx)
|
|
|
|
var envelope agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(simRecorder.Body.Bytes(), &envelope))
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "DEPLOYMENT_FORBIDDEN", envelope.Error.Code)
|
|
}
|
|
|
|
func TestAgentUserDeploymentSimulationIsDisabledForRegularUserByDefault(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.Equal(t, http.StatusOK, createRecorder.Code)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
simRecorder := httptest.NewRecorder()
|
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
|
simCtx.Set("id", 7)
|
|
simCtx.Set("role", common.RoleCommonUser)
|
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{}`))
|
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
|
AgentSimulateUserDeploymentEvents(simCtx)
|
|
|
|
var envelope agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(simRecorder.Body.Bytes(), &envelope))
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "SIMULATION_DISABLED", envelope.Error.Code)
|
|
}
|
|
|
|
func TestAgentUserDeploymentDefaultSimulationCreatesRelatedRecords(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.Equal(t, http.StatusOK, createRecorder.Code)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
simRecorder := httptest.NewRecorder()
|
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
|
simCtx.Set("id", 7)
|
|
simCtx.Set("role", common.RoleAdminUser)
|
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{}`))
|
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
|
AgentSimulateUserDeploymentEvents(simCtx)
|
|
require.Equal(t, http.StatusOK, simRecorder.Code)
|
|
require.Contains(t, simRecorder.Body.String(), `"simulated":true`)
|
|
|
|
artifactRecorder := httptest.NewRecorder()
|
|
artifactCtx, _ := gin.CreateTestContext(artifactRecorder)
|
|
artifactCtx.Set("id", 7)
|
|
artifactCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
artifactCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments/"+deploymentID+"/artifacts", nil)
|
|
AgentListUserDeploymentArtifacts(artifactCtx)
|
|
require.Equal(t, http.StatusOK, artifactRecorder.Code)
|
|
require.Contains(t, artifactRecorder.Body.String(), `"artifact_type":"test_report"`)
|
|
require.Contains(t, artifactRecorder.Body.String(), `"artifact_id":"art-sim-`)
|
|
|
|
approvalRecorder := httptest.NewRecorder()
|
|
approvalCtx, _ := gin.CreateTestContext(approvalRecorder)
|
|
approvalCtx.Set("id", 7)
|
|
approvalCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/approvals?deployment_id="+deploymentID+"&status=pending", nil)
|
|
ListAgentApprovalRequests(approvalCtx)
|
|
require.Equal(t, http.StatusOK, approvalRecorder.Code)
|
|
require.Contains(t, approvalRecorder.Body.String(), `"approval_id":"sim-appr-`)
|
|
require.Contains(t, approvalRecorder.Body.String(), `"status":"pending"`)
|
|
require.NotContains(t, approvalRecorder.Body.String(), "azkv://")
|
|
|
|
timelineRecorder := httptest.NewRecorder()
|
|
timelineCtx, _ := gin.CreateTestContext(timelineRecorder)
|
|
timelineCtx.Set("id", 7)
|
|
timelineCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
timelineCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments/"+deploymentID+"/timeline", nil)
|
|
AgentGetUserDeploymentTimeline(timelineCtx)
|
|
require.Equal(t, http.StatusOK, timelineRecorder.Code)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_type":"artifact.created"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_type":"approval.requested"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"source":"agent-simulator"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_type":"task.created"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_type":"task.blocked"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_type":"handoff.requested"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_type":"handoff.completed"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_type":"task.retried"`)
|
|
}
|
|
|
|
func TestAgentUserSwarmsAdapterCreatesScopedDeployment(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = ""
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = ""
|
|
}
|
|
body, err := common.Marshal(agentDeploymentRequest{Plan: plan})
|
|
require.NoError(t, err)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Set("id", 7)
|
|
ctx.Set("group", "development")
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/swarms", strings.NewReader(string(body)))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
AgentCreateUserSwarm(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"swarm_id"`)
|
|
require.Contains(t, recorder.Body.String(), `"deployment_id"`)
|
|
require.Contains(t, recorder.Body.String(), `"user_id":"7"`)
|
|
}
|
|
|
|
func TestAgentCallbackRejectsInvalidTaskEventSchema(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_CALLBACK_TOKEN", "callback-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.Equal(t, http.StatusOK, createRecorder.Code)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
body := fmt.Sprintf(`{
|
|
"event_id":"evt-invalid-handoff",
|
|
"event_type":"handoff.requested",
|
|
"deployment_id":%q,
|
|
"task_id":"task-1",
|
|
"payload":{
|
|
"from_role":"backend"
|
|
}
|
|
}`, deploymentID)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Request.Header.Set("X-Agent-Service-Token", "callback-token")
|
|
AgentReceiveRuntimeEventCallback(ctx)
|
|
|
|
var envelope agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "CALLBACK_SCHEMA_INVALID", envelope.Error.Code)
|
|
require.Contains(t, envelope.Error.Message, "to_role")
|
|
}
|
|
|
|
func TestAgentCallbackStoresEventArtifactAndIsIdempotent(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_CALLBACK_TOKEN", "callback-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
body := fmt.Sprintf(`{
|
|
"event_id":"evt-callback-1",
|
|
"idempotency_key":"idem-callback-1",
|
|
"event_type":"artifact.created",
|
|
"deployment_id":%q,
|
|
"task_id":"task-1",
|
|
"correlation_id":"corr-p1-resource-grant",
|
|
"artifact":{
|
|
"artifact_id":"art-1",
|
|
"artifact_type":"summary",
|
|
"title":"Build summary",
|
|
"summary":"Created backend scaffold",
|
|
"uri":"artifact://task-1/summary"
|
|
}
|
|
}`, deploymentID)
|
|
|
|
for i := 0; i < 2; i++ {
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Request.Header.Set("X-Agent-Service-Token", "callback-token")
|
|
AgentReceiveRuntimeEventCallback(ctx)
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"success":true`)
|
|
}
|
|
|
|
var callbackCount int64
|
|
require.NoError(t, db.Model(&model.AgentCallbackEvent{}).Where("event_id = ?", "evt-callback-1").Count(&callbackCount).Error)
|
|
require.Equal(t, int64(1), callbackCount)
|
|
var artifactCount int64
|
|
require.NoError(t, db.Model(&model.AgentArtifact{}).Where("artifact_id = ?", "art-1").Count(&artifactCount).Error)
|
|
require.Equal(t, int64(1), artifactCount)
|
|
}
|
|
|
|
func TestAgentCallbackAcceptsDocumentedHMACPayloadArtifact(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_CALLBACK_SIGNING_SECRET", "callback-signing-secret")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
body := fmt.Sprintf(`{
|
|
"event_type":"artifact.created",
|
|
"deployment_id":%q,
|
|
"swarm_id":"swarm-doc-1",
|
|
"agent_instance_id":"agi-backend-1",
|
|
"occurred_at":"2026-05-26T10:40:00Z",
|
|
"payload":{
|
|
"artifact_id":"art-doc-payload-1",
|
|
"artifact_type":"code_patch",
|
|
"title":"Backend API patch",
|
|
"summary":"Patch created",
|
|
"uri":"azblob://heicode-artifacts/task-123/backend.patch",
|
|
"checksum":"sha256:abc",
|
|
"stage":"development",
|
|
"checkpoint":"artifact_ready"
|
|
}
|
|
}`, deploymentID)
|
|
eventID := "evt-doc-hmac-1"
|
|
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|
mac := hmac.New(sha256.New, []byte("callback-signing-secret"))
|
|
mac.Write([]byte(timestamp + "." + eventID + "." + body))
|
|
signature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Request.Header.Set("X-Agent-Event-Id", eventID)
|
|
ctx.Request.Header.Set("X-Agent-Timestamp", timestamp)
|
|
ctx.Request.Header.Set("X-Agent-Signature", signature)
|
|
ctx.Request.Header.Set("X-Correlation-ID", "corr-doc-hmac")
|
|
|
|
AgentReceiveRuntimeEventCallback(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"success":true`)
|
|
|
|
var callback model.AgentCallbackEvent
|
|
require.NoError(t, db.Where("event_id = ?", eventID).First(&callback).Error)
|
|
require.Equal(t, "swarm-doc-1", callback.SwarmID)
|
|
require.Equal(t, "agi-backend-1", callback.AgentInstanceID)
|
|
require.Equal(t, "2026-05-26T10:40:00Z", callback.OccurredAt)
|
|
|
|
var artifact model.AgentArtifact
|
|
require.NoError(t, db.Where("artifact_id = ?", "art-doc-payload-1").First(&artifact).Error)
|
|
require.Equal(t, "code_patch", artifact.ArtifactType)
|
|
require.Contains(t, artifact.MetadataJSON, "artifact_ready")
|
|
}
|
|
|
|
func TestAgentCallbackHMACCanUseAzureKeyVaultSigningSecretRef(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "true", r.Header.Get("Metadata"))
|
|
_, _ = w.Write([]byte(`{"access_token":"manager-token","expires_in":"3600"}`))
|
|
}))
|
|
defer tokenServer.Close()
|
|
|
|
vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodGet, r.Method)
|
|
require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization"))
|
|
require.Equal(t, "/secrets/callback-signing", r.URL.Path)
|
|
_, _ = w.Write([]byte(`{"value":"{\"callback_signing_secret\":\"from-kv-secret\"}"}`))
|
|
}))
|
|
defer vaultServer.Close()
|
|
t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
|
|
t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
|
|
t.Setenv("AGENT_CALLBACK_SIGNING_SECRET_REF", fmt.Sprintf("azkv://%s/secrets/callback-signing", strings.TrimPrefix(vaultServer.URL, "http://")))
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
body := fmt.Sprintf(`{"event_type":"timeline.updated","deployment_id":%q,"payload":{"title":"Ready"}}`, deploymentID)
|
|
eventID := "evt-kv-hmac-1"
|
|
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|
mac := hmac.New(sha256.New, []byte("from-kv-secret"))
|
|
mac.Write([]byte(timestamp + "." + eventID + "." + body))
|
|
signature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Request.Header.Set("X-Agent-Event-Id", eventID)
|
|
ctx.Request.Header.Set("X-Agent-Timestamp", timestamp)
|
|
ctx.Request.Header.Set("X-Agent-Signature", signature)
|
|
|
|
AgentReceiveRuntimeEventCallback(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"success":true`)
|
|
var callback model.AgentCallbackEvent
|
|
require.NoError(t, db.Where("event_id = ?", eventID).First(&callback).Error)
|
|
}
|
|
|
|
func TestAgentCallbackUsesSwarmIDFallbackAndCreatesApproval(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_CALLBACK_TOKEN", "callback-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
swarmID := "runtime-swarm-only-1"
|
|
require.NoError(t, db.Model(&model.AgentDeployment{}).
|
|
Where("deployment_id = ?", deploymentID).
|
|
Update("runtime_swarm_id", swarmID).Error)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
body := `{
|
|
"event_id":"evt-approval-swarm-only",
|
|
"event_type":"approval.requested",
|
|
"swarm_id":"runtime-swarm-only-1",
|
|
"agent_instance_id":"agi-backend-approval",
|
|
"occurred_at":"2026-05-27T10:41:00Z",
|
|
"payload":{
|
|
"approval_id":"runtime-approval-1",
|
|
"operation":"repo.write",
|
|
"resource_id":"res-git-main",
|
|
"resource_type":"git",
|
|
"resource_scope":"heicode/",
|
|
"target_role":"builder",
|
|
"risk_level":"high",
|
|
"requires_credential":true,
|
|
"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo-main",
|
|
"ttl_seconds":600,
|
|
"reason":"Runtime needs write access for backend patch"
|
|
}
|
|
}`
|
|
|
|
for i := 0; i < 2; i++ {
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Request.Header.Set("X-Agent-Service-Token", "callback-token")
|
|
AgentReceiveRuntimeEventCallback(ctx)
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"success":true`)
|
|
if i == 0 {
|
|
require.Contains(t, recorder.Body.String(), `"deduplicated":false`)
|
|
} else {
|
|
require.Contains(t, recorder.Body.String(), `"deduplicated":true`)
|
|
}
|
|
}
|
|
|
|
var callback model.AgentCallbackEvent
|
|
require.NoError(t, db.Where("event_id = ?", "evt-approval-swarm-only").First(&callback).Error)
|
|
require.Equal(t, deploymentID, callback.DeploymentID)
|
|
require.Equal(t, swarmID, callback.SwarmID)
|
|
require.Equal(t, "agi-backend-approval", callback.AgentInstanceID)
|
|
|
|
var approval model.AgentApprovalRequest
|
|
require.NoError(t, db.Where("approval_id = ?", "runtime-approval-1").First(&approval).Error)
|
|
require.Equal(t, 7, approval.UserId)
|
|
require.Equal(t, deploymentID, approval.DeploymentID)
|
|
require.Equal(t, "pending", approval.Status)
|
|
require.Equal(t, "repo.write", approval.Operation)
|
|
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo-main", approval.SecretRef)
|
|
}
|
|
|
|
func TestAgentCallbackUsesRuntimeDeploymentIDFallback(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_CALLBACK_TOKEN", "callback-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
plan.BillingContext.DefaultModelID = "model-runtime-smoke"
|
|
plan.BillingContext.AllowedModelIDs = []string{"model-runtime-smoke", "model-runtime-fallback"}
|
|
plan.BillingContext.SecretRef = "azkv://heicode-kv.vault.azure.net/secrets/model-gateway-key"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
runtimeID := "swm-runtime-deployment-only-1"
|
|
require.NoError(t, db.Model(&model.AgentDeployment{}).
|
|
Where("deployment_id = ?", deploymentID).
|
|
Updates(map[string]any{
|
|
"runtime_deployment_id": runtimeID,
|
|
"runtime_swarm_id": runtimeID,
|
|
}).Error)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
body := `{
|
|
"event_id":"evt-runtime-deployment-fallback",
|
|
"event_type":"timeline.updated",
|
|
"deployment_id":"swm-runtime-deployment-only-1",
|
|
"agent_instance_id":"agi-backend-runtime",
|
|
"occurred_at":"2026-05-28T12:30:00Z",
|
|
"payload":{
|
|
"title":"Runtime accepted",
|
|
"summary":"Agent Manager returned a runtime deployment id only",
|
|
"stage":"testing",
|
|
"checkpoint":"runtime_accepted"
|
|
}
|
|
}`
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Request.Header.Set("X-Agent-Service-Token", "callback-token")
|
|
AgentReceiveRuntimeEventCallback(ctx)
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"success":true`)
|
|
require.Contains(t, recorder.Body.String(), `"deployment_id":"`+deploymentID+`"`)
|
|
|
|
var callback model.AgentCallbackEvent
|
|
require.NoError(t, db.Where("event_id = ?", "evt-runtime-deployment-fallback").First(&callback).Error)
|
|
require.Equal(t, deploymentID, callback.DeploymentID)
|
|
require.Equal(t, runtimeID, callback.SwarmID)
|
|
require.Equal(t, "7", callback.UserID)
|
|
|
|
var stored model.AgentDeployment
|
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
|
require.Contains(t, stored.PlanJSON, `"default_model_id":"model-runtime-smoke"`)
|
|
require.Contains(t, stored.PlanJSON, `"allowed_model_ids":["model-runtime-smoke","model-runtime-fallback"]`)
|
|
require.Contains(t, stored.PlanJSON, `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/model-gateway-key"`)
|
|
}
|
|
|
|
func TestAgentCallbackUpdatesDeploymentRuntimeSnapshot(t *testing.T) {
|
|
db := setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_CALLBACK_TOKEN", "callback-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
runtimeID := "swm-runtime-state-1"
|
|
require.NoError(t, db.Model(&model.AgentDeployment{}).
|
|
Where("deployment_id = ?", deploymentID).
|
|
Updates(map[string]any{
|
|
"runtime_deployment_id": runtimeID,
|
|
"runtime_swarm_id": runtimeID,
|
|
}).Error)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
postCallback := func(body string) {
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(body))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Request.Header.Set("X-Agent-Service-Token", "callback-token")
|
|
AgentReceiveRuntimeEventCallback(ctx)
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), `"success":true`)
|
|
}
|
|
|
|
postCallback(`{
|
|
"event_id":"evt-runtime-agent-started",
|
|
"event_type":"agent.started",
|
|
"deployment_id":"swm-runtime-state-1",
|
|
"swarm_id":"swm-runtime-state-1",
|
|
"agent_instance_id":"agi-runtime-builder",
|
|
"occurred_at":"2026-05-28T12:40:00Z",
|
|
"payload":{"agent_role":"builder","status":"running"}
|
|
}`)
|
|
postCallback(`{
|
|
"event_id":"evt-runtime-phase-deploy",
|
|
"event_type":"phase.changed",
|
|
"deployment_id":"swm-runtime-state-1",
|
|
"swarm_id":"swm-runtime-state-1",
|
|
"occurred_at":"2026-05-28T12:41:00Z",
|
|
"payload":{"stage":"deploy","checkpoint":"completed","summary":"Runtime completed"}
|
|
}`)
|
|
postCallback(`{
|
|
"event_id":"evt-runtime-status-completed",
|
|
"event_type":"deployment.status_changed",
|
|
"deployment_id":"swm-runtime-state-1",
|
|
"swarm_id":"swm-runtime-state-1",
|
|
"occurred_at":"2026-05-28T12:42:00Z",
|
|
"payload":{"status":"completed"}
|
|
}`)
|
|
|
|
var stored model.AgentDeployment
|
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
|
require.Equal(t, "completed", stored.Status)
|
|
require.Equal(t, "deploy", stored.Phase)
|
|
require.Equal(t, "completed", stored.RuntimeState)
|
|
require.Equal(t, "2026-05-28T12:42:00Z", stored.UpdatedAtText)
|
|
|
|
var instances []agentAgentInstance
|
|
require.NoError(t, common.UnmarshalJsonStr(stored.AgentInstancesJSON, &instances))
|
|
require.Len(t, instances, 1)
|
|
require.Equal(t, "agi-runtime-builder", instances[0].InstanceID)
|
|
require.Equal(t, "builder", instances[0].Role)
|
|
require.Equal(t, "deploy", instances[0].Phase)
|
|
require.Equal(t, "completed", instances[0].RuntimeState)
|
|
}
|
|
|
|
func TestAgentCallbackRejectsPlaintextSecretsAndMissingToken(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_CALLBACK_TOKEN", "callback-token")
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(`{"event_id":"evt-no-token","event_type":"status.updated"}`))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
AgentReceiveRuntimeEventCallback(ctx)
|
|
var missingToken agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &missingToken))
|
|
require.False(t, missingToken.Success)
|
|
require.Equal(t, "CALLBACK_UNAUTHORIZED", missingToken.Error.Code)
|
|
|
|
secretRecorder := httptest.NewRecorder()
|
|
secretCtx, _ := gin.CreateTestContext(secretRecorder)
|
|
secretCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(`{
|
|
"event_id":"evt-secret",
|
|
"event_type":"status.updated",
|
|
"metadata":{"access_token":"must-not-store"}
|
|
}`))
|
|
secretCtx.Request.Header.Set("Content-Type", "application/json")
|
|
secretCtx.Request.Header.Set("X-Agent-Service-Token", "callback-token")
|
|
AgentReceiveRuntimeEventCallback(secretCtx)
|
|
var secretEnvelope agentCreateTestEnvelope
|
|
require.NoError(t, common.Unmarshal(secretRecorder.Body.Bytes(), &secretEnvelope))
|
|
require.False(t, secretEnvelope.Success)
|
|
require.Equal(t, "CALLBACK_SECRET_REJECTED", secretEnvelope.Error.Code)
|
|
}
|
|
|
|
func TestAgentArtifactsAndTimelineReturnPersistedRelatedRecords(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
t.Setenv("AGENT_CALLBACK_TOKEN", "callback-token")
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
plan.SubMode = "waterfall"
|
|
for idx := range plan.Agents[0].ResourceGrants {
|
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
|
}
|
|
createRecorder, createEnvelope := postAgentCreateUserDeployment(t, 7, plan)
|
|
require.True(t, createEnvelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
callbackBody := fmt.Sprintf(`{
|
|
"event_id":"evt-artifact-timeline",
|
|
"idempotency_key":"idem-artifact-timeline",
|
|
"event_type":"artifact.created",
|
|
"deployment_id":%q,
|
|
"task_id":"task-timeline",
|
|
"correlation_id":"corr-p1-resource-grant",
|
|
"artifact":{"artifact_id":"art-timeline","artifact_type":"summary","title":"Timeline artifact","summary":"Done","uri":"artifact://task-timeline/summary"}
|
|
}`, deploymentID)
|
|
callbackRecorder := httptest.NewRecorder()
|
|
callbackCtx, _ := gin.CreateTestContext(callbackRecorder)
|
|
callbackCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/callbacks/runtime-events", strings.NewReader(callbackBody))
|
|
callbackCtx.Request.Header.Set("Content-Type", "application/json")
|
|
callbackCtx.Request.Header.Set("X-Agent-Service-Token", "callback-token")
|
|
AgentReceiveRuntimeEventCallback(callbackCtx)
|
|
require.Contains(t, callbackRecorder.Body.String(), `"success":true`)
|
|
|
|
snapshotRecorder := httptest.NewRecorder()
|
|
snapshotCtx, _ := gin.CreateTestContext(snapshotRecorder)
|
|
snapshotCtx.Set("id", 7)
|
|
snapshotCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/sk-snapshots/resolve", strings.NewReader(fmt.Sprintf(`{"deployment_id":%q}`, deploymentID)))
|
|
snapshotCtx.Request.Header.Set("Content-Type", "application/json")
|
|
AgentResolveSKSnapshots(snapshotCtx)
|
|
require.Equal(t, http.StatusOK, snapshotRecorder.Code)
|
|
|
|
resetAgentControlPlaneState(t)
|
|
|
|
artifactRecorder := httptest.NewRecorder()
|
|
artifactCtx, _ := gin.CreateTestContext(artifactRecorder)
|
|
artifactCtx.Set("id", 7)
|
|
artifactCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
artifactCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments/"+deploymentID+"/artifacts", nil)
|
|
AgentListUserDeploymentArtifacts(artifactCtx)
|
|
require.Equal(t, http.StatusOK, artifactRecorder.Code)
|
|
require.Contains(t, artifactRecorder.Body.String(), `"art-timeline"`)
|
|
require.Contains(t, artifactRecorder.Body.String(), `"deployment_id":"`+deploymentID+`"`)
|
|
require.Contains(t, artifactRecorder.Body.String(), `"artifacts":[`)
|
|
|
|
snapshotListRecorder := httptest.NewRecorder()
|
|
snapshotListCtx, _ := gin.CreateTestContext(snapshotListRecorder)
|
|
snapshotListCtx.Set("id", 7)
|
|
snapshotListCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
snapshotListCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments/"+deploymentID+"/sk-snapshots", nil)
|
|
AgentListUserSKSnapshots(snapshotListCtx)
|
|
require.Equal(t, http.StatusOK, snapshotListRecorder.Code)
|
|
require.Contains(t, snapshotListRecorder.Body.String(), `"deployment_id":"`+deploymentID+`"`)
|
|
require.Contains(t, snapshotListRecorder.Body.String(), `"sk_snapshots":[`)
|
|
|
|
timelineRecorder := httptest.NewRecorder()
|
|
timelineCtx, _ := gin.CreateTestContext(timelineRecorder)
|
|
timelineCtx.Set("id", 7)
|
|
timelineCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
timelineCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/user/deployments/"+deploymentID+"/timeline", nil)
|
|
AgentGetUserDeploymentTimeline(timelineCtx)
|
|
require.Equal(t, http.StatusOK, timelineRecorder.Code)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"callbacks"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"artifacts"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"sk_snapshots"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"timeline"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"deployment_id":"`+deploymentID+`"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_id":"evt-artifact-timeline"`)
|
|
require.Contains(t, timelineRecorder.Body.String(), `"event_type":"artifact.created"`)
|
|
}
|
|
|
|
func TestAgentStopDeploymentPersistsState(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, baseAgentResourceGrantPlan())
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
stopRecorder := httptest.NewRecorder()
|
|
stopCtx, _ := gin.CreateTestContext(stopRecorder)
|
|
stopCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
stopCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/deployments/"+deploymentID+"/stop", nil)
|
|
AgentStopDeployment(stopCtx)
|
|
require.Equal(t, http.StatusOK, stopRecorder.Code)
|
|
require.Contains(t, stopRecorder.Body.String(), `"status":"stopped"`)
|
|
|
|
resetAgentControlPlaneState(t)
|
|
|
|
getRecorder := httptest.NewRecorder()
|
|
getCtx, _ := gin.CreateTestContext(getRecorder)
|
|
getCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
getCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/deployments/"+deploymentID, nil)
|
|
AgentGetDeployment(getCtx)
|
|
|
|
require.Equal(t, http.StatusOK, getRecorder.Code)
|
|
require.Contains(t, getRecorder.Body.String(), `"status":"stopped"`)
|
|
require.Contains(t, getRecorder.Body.String(), `"runtime_state":"stopped"`)
|
|
}
|
|
|
|
func TestAgentStopDeploymentPropagatesToRuntime(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
stopCalled := false
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/sub-agile/deployments":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"deployment_id":"runtime-dep-stop","status":"pending"}}`))
|
|
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/sub-agile/deployments/runtime-dep-stop/stop":
|
|
stopCalled = true
|
|
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
|
|
body, err := io.ReadAll(r.Body)
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(body), `"reason":"runtime stop smoke"`)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"deployment_id":"runtime-dep-stop","status":"stopped"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
|
|
t.Setenv("AGENT_RUNTIME_ASYNC", "false")
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", server.URL)
|
|
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "service-token")
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, baseAgentResourceGrantPlan())
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
var createBody map[string]any
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
|
|
|
stopRecorder := httptest.NewRecorder()
|
|
stopCtx, _ := gin.CreateTestContext(stopRecorder)
|
|
stopCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
stopCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/deployments/"+deploymentID+"/stop", strings.NewReader(`{"reason":"runtime stop smoke"}`))
|
|
stopCtx.Request.Header.Set("Content-Type", "application/json")
|
|
AgentStopDeployment(stopCtx)
|
|
|
|
require.Equal(t, http.StatusOK, stopRecorder.Code)
|
|
require.Contains(t, stopRecorder.Body.String(), `"success":true`)
|
|
require.Contains(t, stopRecorder.Body.String(), `"runtime_state":"stopped"`)
|
|
require.True(t, stopCalled)
|
|
}
|
|
|
|
func TestManagerOnlyAgentSmokeFlow(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
resetAgentControlPlaneState(t)
|
|
|
|
resourceBody := `{
|
|
"binding_scope":"project-smoke",
|
|
"name":"Smoke project repo",
|
|
"resource_type":"git",
|
|
"provider":"gitee",
|
|
"external_id":"https://example.invalid/acme/smoke.git",
|
|
"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/project-smoke",
|
|
"metadata":{"repo_url":"https://example.invalid/acme/smoke.git","ref":"main"},
|
|
"permission_scope":{"actions":["repo:read"]},
|
|
"constraints":{"allowed_paths":"src/**,docs/**"}
|
|
}`
|
|
resourceRecorder := performResourceRequest(CreateResource, 7, http.MethodPost, "/", resourceBody)
|
|
require.Equal(t, http.StatusOK, resourceRecorder.Code)
|
|
require.Contains(t, resourceRecorder.Body.String(), `"success":true`)
|
|
require.NotContains(t, strings.ToLower(resourceRecorder.Body.String()), "password")
|
|
var resourceEnv map[string]any
|
|
require.NoError(t, common.Unmarshal(resourceRecorder.Body.Bytes(), &resourceEnv))
|
|
resource := resourceEnv["data"].(map[string]any)
|
|
resourceID := int(resource["id"].(float64))
|
|
|
|
grantBody := fmt.Sprintf(`{
|
|
"binding_scope":"project-smoke",
|
|
"resource_id":%d,
|
|
"role":"builder",
|
|
"agent_id":"agent-builder-1",
|
|
"permission_scope":{"actions":["repo:read","repo:write:feature-branches"]},
|
|
"constraints":{"allowed_paths":"src/**,docs/**","ref":"main"}
|
|
}`, resourceID)
|
|
grantRecorder := performResourceRequest(CreateResourceGrant, 7, http.MethodPost, "/", grantBody)
|
|
require.Equal(t, http.StatusOK, grantRecorder.Code)
|
|
require.Contains(t, grantRecorder.Body.String(), `"success":true`)
|
|
var grantEnv map[string]any
|
|
require.NoError(t, common.Unmarshal(grantRecorder.Body.Bytes(), &grantEnv))
|
|
grant := grantEnv["data"].(map[string]any)
|
|
grantID := int(grant["id"].(float64))
|
|
|
|
manifestRecorder := performResourceRequestWithRoute(
|
|
GenerateResourceGrantManifest,
|
|
7,
|
|
http.MethodGet,
|
|
"/manifest",
|
|
"/manifest?binding_scope=project-smoke&role=builder&agent_id=agent-builder-1",
|
|
"",
|
|
)
|
|
require.Equal(t, http.StatusOK, manifestRecorder.Code)
|
|
require.Contains(t, manifestRecorder.Body.String(), `"resource_grants":[`)
|
|
require.Contains(t, manifestRecorder.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/project-smoke"`)
|
|
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = "7"
|
|
plan.UserContext.ChannelID = "smoke-channel"
|
|
plan.BillingContext.NewAPIUserRef = "newapi-user-smoke"
|
|
plan.BillingContext.NewAPIGroup = "vip"
|
|
plan.BillingContext.QuotaRef = "quota-smoke"
|
|
plan.Metadata.CorrelationID = "corr-manager-smoke"
|
|
plan.Agents[0].ResourceGrants = []agentResourceGrant{
|
|
{
|
|
GrantID: fmt.Sprintf("grant-%d", grantID),
|
|
ResourceID: fmt.Sprintf("%d", resourceID),
|
|
ResourceType: agentResourceGit,
|
|
UserID: "7",
|
|
BindingScope: "project-smoke",
|
|
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",
|
|
"resource_ref": "https://example.invalid/acme/smoke.git",
|
|
},
|
|
Status: agentGrantStatusActive,
|
|
SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/project-smoke",
|
|
Audit: map[string]string{"confirmed_by": "7"},
|
|
},
|
|
}
|
|
|
|
deployRecorder, envelope := postAgentCreateDeployment(t, plan)
|
|
require.Equal(t, http.StatusOK, deployRecorder.Code)
|
|
require.True(t, envelope.Success)
|
|
var deployEnv map[string]any
|
|
require.NoError(t, common.Unmarshal(deployRecorder.Body.Bytes(), &deployEnv))
|
|
deploymentID := deployEnv["data"].(map[string]any)["deployment_id"].(string)
|
|
require.NotEmpty(t, deploymentID)
|
|
|
|
logRecorder := httptest.NewRecorder()
|
|
logCtx, _ := gin.CreateTestContext(logRecorder)
|
|
logCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
logCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/deployments/"+deploymentID+"/logs", nil)
|
|
AgentListDeploymentLogs(logCtx)
|
|
require.Equal(t, http.StatusOK, logRecorder.Code)
|
|
require.Contains(t, logRecorder.Body.String(), `"redacted":true`)
|
|
require.NotContains(t, strings.ToLower(logRecorder.Body.String()), "access_token")
|
|
|
|
stopRecorder := httptest.NewRecorder()
|
|
stopCtx, _ := gin.CreateTestContext(stopRecorder)
|
|
stopCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
stopCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agent/deployments/"+deploymentID+"/stop", nil)
|
|
AgentStopDeployment(stopCtx)
|
|
require.Equal(t, http.StatusOK, stopRecorder.Code)
|
|
require.Contains(t, stopRecorder.Body.String(), `"status":"stopped"`)
|
|
|
|
resetAgentControlPlaneState(t)
|
|
getRecorder := httptest.NewRecorder()
|
|
getCtx, _ := gin.CreateTestContext(getRecorder)
|
|
getCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
getCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/deployments/"+deploymentID, nil)
|
|
AgentGetDeployment(getCtx)
|
|
require.Equal(t, http.StatusOK, getRecorder.Code)
|
|
require.Contains(t, getRecorder.Body.String(), `"status":"stopped"`)
|
|
|
|
revokeGrantRecorder := performResourceRequestWithRoute(
|
|
DeleteResourceGrant,
|
|
7,
|
|
http.MethodDelete,
|
|
"/:id",
|
|
fmt.Sprintf("/%d", grantID),
|
|
"",
|
|
)
|
|
require.Equal(t, http.StatusOK, revokeGrantRecorder.Code)
|
|
require.Contains(t, revokeGrantRecorder.Body.String(), `"status":"revoked"`)
|
|
|
|
emptyManifestRecorder := performResourceRequestWithRoute(
|
|
GenerateResourceGrantManifest,
|
|
7,
|
|
http.MethodGet,
|
|
"/manifest",
|
|
"/manifest?binding_scope=project-smoke&role=builder&agent_id=agent-builder-1",
|
|
"",
|
|
)
|
|
require.Equal(t, http.StatusOK, emptyManifestRecorder.Code)
|
|
require.Contains(t, emptyManifestRecorder.Body.String(), `"resource_grants":[]`)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
|
|
recorder, envelope := postAgentCreateDeployment(t, baseAgentResourceGrantPlan())
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.True(t, envelope.Success)
|
|
|
|
agentMu.RLock()
|
|
require.Len(t, agentDeployments, 1)
|
|
var stored agentDeploymentRecord
|
|
for _, record := range agentDeployments {
|
|
stored = record
|
|
}
|
|
agentMu.RUnlock()
|
|
|
|
grants := stored.Plan.Agents[0].ResourceGrants
|
|
require.Len(t, grants, 2)
|
|
require.Equal(t, agentResourceGit, 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, "agent", stored.Plan.AgentRuntime.Platform)
|
|
require.Equal(t, "agent-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 TestAgentPermissionManifestExcludesInactiveResourceGrants(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.Agents[0].ResourceGrants[1].Status = agentGrantStatusRevoked
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
require.True(t, envelope.Success)
|
|
|
|
agentMu.RLock()
|
|
var stored agentDeploymentRecord
|
|
for _, record := range agentDeployments {
|
|
stored = record
|
|
}
|
|
agentMu.RUnlock()
|
|
|
|
require.Len(t, stored.ResourceGrantManifest.ResourceGrants, 1)
|
|
require.Equal(t, "grant-git-builder", stored.ResourceGrantManifest.ResourceGrants[0].GrantID)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsPlaintextResourceGrantCredentialFields(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.Agents[0].ResourceGrants[0].Metadata["access_token"] = "must-not-be-stored"
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "RESOURCE_GRANT_SECRET_REJECTED", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsResourceGrantWithoutSecretRef(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.Agents[0].ResourceGrants[0].SecretRef = ""
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "RESOURCE_GRANT_SECRET_REF_REQUIRED", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsMissingUserContext(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.UserID = ""
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "POLICY_REJECTED", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsResourceGrantUserMismatch(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.Agents[0].ResourceGrants[0].UserID = "user-other"
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "RESOURCE_GRANT_INVALID", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsResourceGrantWithoutBindingScope(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.Agents[0].ResourceGrants[0].BindingScope = ""
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "RESOURCE_GRANT_INVALID", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsNewAPIBillingWithoutUserMapping(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.UserContext.ChannelID = ""
|
|
plan.BillingContext.NewAPIUserRef = ""
|
|
plan.BillingContext.NewAPIGroup = ""
|
|
plan.BillingContext.QuotaRef = ""
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "BILLING_CONTEXT_INVALID", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentCreateDeploymentRejectsRuntimeRoleOutsideAgentPlan(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
plan := baseAgentResourceGrantPlan()
|
|
plan.AgentRuntime.Agents[0].Role = "ops"
|
|
|
|
_, envelope := postAgentCreateDeployment(t, plan)
|
|
|
|
require.False(t, envelope.Success)
|
|
require.Equal(t, "AGENT_RUNTIME_INVALID", envelope.Error.Code)
|
|
require.Empty(t, agentDeployments)
|
|
}
|
|
|
|
func TestAgentDeploymentLogsAndMetricsExposeRedactedReadiness(t *testing.T) {
|
|
resetAgentControlPlaneState(t)
|
|
|
|
_, envelope := postAgentCreateDeployment(t, baseAgentResourceGrantPlan())
|
|
require.True(t, envelope.Success)
|
|
|
|
agentMu.RLock()
|
|
var deploymentID string
|
|
for id := range agentDeployments {
|
|
deploymentID = id
|
|
break
|
|
}
|
|
agentMu.RUnlock()
|
|
require.NotEmpty(t, deploymentID)
|
|
|
|
logRecorder := httptest.NewRecorder()
|
|
logCtx, _ := gin.CreateTestContext(logRecorder)
|
|
logCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agent/deployments/"+deploymentID+"/logs", nil)
|
|
logCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
|
|
AgentListDeploymentLogs(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/agent/deployments/"+deploymentID+"/metrics?window=15m&step=60s", nil)
|
|
metricCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
|
|
|
AgentGetDeploymentMetrics(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":""`)
|
|
}
|
|
|
|
func TestAgentDeploymentDisplayStatus_JudgesDeliverable(t *testing.T) {
|
|
setupAgentControlPlaneTestDB(t)
|
|
rec := agentDeploymentRecord{DeploymentID: "dep_ds_1", Status: "completed"}
|
|
|
|
// completed but no artifacts -> completed_without_deliverable
|
|
require.Equal(t, "completed_without_deliverable", agentDeploymentDisplayStatus(rec))
|
|
|
|
// only a summary/document artifact -> needs_codegen
|
|
require.NoError(t, model.UpsertAgentArtifact(&model.AgentArtifact{
|
|
ArtifactID: "art_ds_doc", DeploymentID: "dep_ds_1", ArtifactType: "document", Title: "plan", CreatedAtMs: 1,
|
|
}))
|
|
require.Equal(t, "needs_codegen", agentDeploymentDisplayStatus(rec))
|
|
|
|
// a real code deliverable -> completed
|
|
require.NoError(t, model.UpsertAgentArtifact(&model.AgentArtifact{
|
|
ArtifactID: "art_ds_code", DeploymentID: "dep_ds_1", ArtifactType: "code_patch", Title: "backend", CreatedAtMs: 2,
|
|
}))
|
|
require.Equal(t, "completed", agentDeploymentDisplayStatus(rec))
|
|
|
|
// synthesized fallback (even if code_patch typed) is not a real deliverable
|
|
require.NoError(t, model.UpsertAgentArtifact(&model.AgentArtifact{
|
|
ArtifactID: "art_ds_synth", DeploymentID: "dep_ds_2", ArtifactType: "code_patch", Title: "fallback",
|
|
MetadataJSON: `{"synthesized":true}`, CreatedAtMs: 1,
|
|
}))
|
|
require.Equal(t, "needs_codegen", agentDeploymentDisplayStatus(agentDeploymentRecord{DeploymentID: "dep_ds_2", Status: "completed"}))
|
|
|
|
// non-completed mirrors the runtime status
|
|
require.Equal(t, "running", agentDeploymentDisplayStatus(agentDeploymentRecord{DeploymentID: "dep_ds_x", Status: "running"}))
|
|
}
|