Files
heicode-mananger/heicode/controller/agent_template_test.go
T
chenchenandClaude Opus 4.8 19fd4f3326 refactor(agent): built-in per-type env mapping; fix start timeout, orphan, guards
Replace the user-facing env_map mistake with a built-in env convention keyed by
resource type + provider — users never see/edit env names; they only fill plain
resource fields. Supports git (gitea/github/gitlab), vm, database
(mysql/pg/redis/mongo, with alias normalisation), storage (azure blob / bucket).
Lenient: missing optional fields are skipped; only unsupported type or a KV read
failure errors.

Other issues found in review and fixed:
- start timeout: template-agent start now uses a longer timeout (default 60s,
  AGENT_RUNTIME_START_TIMEOUT_SECONDS) since AM provisions synchronously — 5s
  would time out. amTemplateDo takes a per-call timeout.
- orphan agent: if AM start succeeds but the Manager record fails to persist, the
  orphan is rolled back (best-effort amDeleteTemplateAgent).
- findUserTemplateAgent now guards template_id<>'' so the new endpoints can't
  touch a legacy task deployment.
- binding_ids defaults to [] (not null).
- removed ResourceBinding.EnvMap field entirely.

Tests rewritten for the built-in convention (blob metadata-only, db provider
prefixes incl pg/mg aliases, git provider-agnostic names, ownership, unsupported
type, empty); adapter round-trip + router tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 22:48:29 +08:00

161 lines
5.5 KiB
Go

package controller
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/heicode/manager/model"
"github.com/stretchr/testify/require"
)
func TestBuildAgentEnvFromBindings_BuiltinBlobMetadataOnly(t *testing.T) {
setupResourceControllerTestDB(t)
// No secret_ref -> secret fields (BLOB_KEY) are skipped, not an error.
b := model.ResourceBinding{
UserId: 7,
Name: "blob1",
ResourceType: "blob",
Provider: "blob",
Metadata: `{"account":"myacct","container":"uploads"}`,
}
require.NoError(t, model.DB.Create(&b).Error)
env, err := buildAgentEnvFromBindings(7, []int{b.Id})
require.NoError(t, err)
require.Equal(t, "myacct", env["BLOB_ACCOUNT"])
require.Equal(t, "uploads", env["BLOB_CONTAINER"])
_, hasKey := env["BLOB_KEY"]
require.False(t, hasKey) // no secret bound
}
func TestBuildAgentEnvFromBindings_DatabaseProviderPrefix(t *testing.T) {
setupResourceControllerTestDB(t)
cases := []struct {
provider string
wantHost string // env var that should carry the host
}{
{"mysql", "MYSQL_HOST"},
{"pg", "PG_HOST"},
{"postgresql", "PG_HOST"}, // alias normalises to pg
{"redis", "REDIS_HOST"},
{"mg", "MONGO_HOST"}, // alias normalises to mongo
}
for _, tc := range cases {
b := model.ResourceBinding{
UserId: 7,
Name: "db-" + tc.provider,
ResourceType: "database",
Provider: tc.provider,
Metadata: `{"host":"db.example","port":"5432","database":"app","user":"u"}`,
}
require.NoError(t, model.DB.Create(&b).Error)
env, err := buildAgentEnvFromBindings(7, []int{b.Id})
require.NoError(t, err, tc.provider)
require.Equal(t, "db.example", env[tc.wantHost], "provider %s -> %s", tc.provider, tc.wantHost)
}
}
func TestBuildAgentEnvFromBindings_GitNamesProviderAgnostic(t *testing.T) {
setupResourceControllerTestDB(t)
for _, provider := range []string{"github", "gitea", "gitlab"} {
b := model.ResourceBinding{
UserId: 7,
Name: "git-" + provider,
ResourceType: "git",
Provider: provider,
Metadata: `{"provider":"` + provider + `","repo_url":"https://x/owner/repo","default_branch":"main"}`,
}
require.NoError(t, model.DB.Create(&b).Error)
env, err := buildAgentEnvFromBindings(7, []int{b.Id})
require.NoError(t, err)
require.Equal(t, "https://x/owner/repo", env["GIT_REPO_URL"])
require.Equal(t, "main", env["GIT_BRANCH"])
require.Equal(t, provider, env["GIT_PROVIDER"])
}
}
func TestBuildAgentEnvFromBindings_OwnershipEnforced(t *testing.T) {
setupResourceControllerTestDB(t)
b := model.ResourceBinding{UserId: 7, Name: "x", ResourceType: "blob", Provider: "blob", Metadata: `{}`}
require.NoError(t, model.DB.Create(&b).Error)
_, err := buildAgentEnvFromBindings(99, []int{b.Id}) // different user
require.Error(t, err)
}
func TestBuildAgentEnvFromBindings_UnsupportedType(t *testing.T) {
setupResourceControllerTestDB(t)
b := model.ResourceBinding{UserId: 7, Name: "x", ResourceType: "quantum", Provider: "weird", Metadata: `{}`}
require.NoError(t, model.DB.Create(&b).Error)
_, err := buildAgentEnvFromBindings(7, []int{b.Id})
require.Error(t, err)
}
func TestBuildAgentEnvFromBindings_Empty(t *testing.T) {
setupResourceControllerTestDB(t)
env, err := buildAgentEnvFromBindings(7, nil)
require.NoError(t, err)
require.Empty(t, env)
}
func TestAgentTemplatePathSubstitution(t *testing.T) {
require.Contains(t, agentTemplateStartPath("tpl1"), "/templates/tpl1/start")
require.Contains(t, agentTemplateAgentStopPath("rt-9"), "/agents/rt-9/stop")
require.Contains(t, agentTemplateAgentPath("rt-9"), "/agents/rt-9")
}
func TestAMStartTemplateAgent_RoundTrip(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/api/agent/templates/tpl1/start", r.URL.Path)
body, _ := io.ReadAll(r.Body)
require.Contains(t, string(body), "manager_deployment_id")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"runtime_id":"rt-9","subdomain":"https://x.example","access_token":"tok","status":"running"}}`))
}))
defer srv.Close()
t.Setenv("AGENT_RUNTIME_BASE_URL", srv.URL)
res, err := amStartTemplateAgent(context.Background(), "tpl1", "dep_1", map[string]string{"K": "V"}, "https://cb")
require.NoError(t, err)
require.Equal(t, "rt-9", res.RuntimeID)
require.Equal(t, "https://x.example", res.Subdomain)
require.Equal(t, "tok", res.AccessToken)
require.Equal(t, "running", res.Status)
}
func TestAMGetAgentStatus_RoundTrip(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/api/agent/agents/rt-1", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"status":"running"}}`))
}))
defer srv.Close()
t.Setenv("AGENT_RUNTIME_BASE_URL", srv.URL)
status, err := amGetAgentStatus(context.Background(), "rt-1")
require.NoError(t, err)
require.Equal(t, "running", status)
}
func TestTemplateAgentResponse(t *testing.T) {
row := model.AgentDeployment{
DeploymentID: "dep_abc",
TemplateID: "tpl1",
Subdomain: "https://abc.agents.example",
AccessToken: "tok",
BindingIDsJSON: "[1,2]",
Status: "running",
}
resp := templateAgentResponse(row)
require.Equal(t, "dep_abc", resp["agent_id"])
require.Equal(t, "tpl1", resp["template_id"])
require.Equal(t, []int{1, 2}, resp["binding_ids"])
require.Equal(t, "running", resp["status"])
}