From da854277d34a2e591989cbe231c983b580029215 Mon Sep 17 00:00:00 2001 From: chenchen Date: Mon, 8 Jun 2026 16:48:29 +0800 Subject: [PATCH] =?UTF-8?q?test(agent):=20=E5=BC=82=E6=AD=A5=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E7=9A=84=E7=A1=AE=E5=AE=9A=E6=80=A7=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=EF=BC=88=E4=B8=8D=E9=98=BB=E5=A1=9E=20+=20Pending=E2=86=92runn?= =?UTF-8?q?ing=20/=20=E2=86=92failed=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 慢 AM mock 验证 startTemplateAgentAsync 立即返回(不阻塞 30s)。 - Pending 记录在 AM 响应后自动回填 running + runtime_id + subdomain。 - AM 失败时记录标 failed + failure_reason,不卡在 Pending。 Co-Authored-By: Claude Opus 4.8 --- heicode/controller/agent_async_deploy_test.go | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 heicode/controller/agent_async_deploy_test.go diff --git a/heicode/controller/agent_async_deploy_test.go b/heicode/controller/agent_async_deploy_test.go new file mode 100644 index 0000000..15dae80 --- /dev/null +++ b/heicode/controller/agent_async_deploy_test.go @@ -0,0 +1,78 @@ +package controller + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/heicode/manager/model" + "github.com/stretchr/testify/require" +) + +// startTemplateAgentAsync must NOT block the caller on the (slow) AM start, and +// must flip the pre-persisted Pending record to running once AM responds — this +// is the core of the 504 fix (deploy returns immediately; status lands later). +func TestStartTemplateAgentAsync_PendingToRunning(t *testing.T) { + setupResourceControllerTestDB(t) + require.NoError(t, model.DB.AutoMigrate(&model.AgentDeployment{})) + + // Slow AM mock — simulates the ~30s start the handler must not block on. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(150 * time.Millisecond) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"success":true,"data":{"namespace":"rt-async-1","access_info":{"domain":"https://async.agents.example"},"status":"running"}}`)) + })) + defer srv.Close() + t.Setenv("AGENT_RUNTIME_BASE_URL", srv.URL) + + // What HeicodeDeployAgent persists before returning. + require.NoError(t, model.DB.Create(&model.AgentDeployment{ + DeploymentID: "dep_async_run", + UserID: "22", + TemplateID: "architect", + Status: "Pending", + }).Error) + + start := time.Now() + startTemplateAgentAsync("dep_async_run", 0, amStartArgs{ManagerDeploymentID: "dep_async_run", UserID: "22", TemplateKey: "architect"}) + require.Less(t, time.Since(start), 50*time.Millisecond, "must not block on the AM start call") + + var got model.AgentDeployment + require.Eventually(t, func() bool { + model.DB.Where("deployment_id = ?", "dep_async_run").First(&got) + return got.Status == "running" + }, 3*time.Second, 20*time.Millisecond, "Pending should flip to running after async AM start") + require.Equal(t, "rt-async-1", got.RuntimeDeploymentID) + require.Equal(t, "https://async.agents.example", got.Subdomain) +} + +// On AM failure the record must be marked failed (with a reason), not left +// stuck on Pending — so the client surfaces a failure instead of a hang. +func TestStartTemplateAgentAsync_FailureMarksFailed(t *testing.T) { + setupResourceControllerTestDB(t) + require.NoError(t, model.DB.AutoMigrate(&model.AgentDeployment{})) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"success":false,"message":"AM boom"}`)) + })) + defer srv.Close() + t.Setenv("AGENT_RUNTIME_BASE_URL", srv.URL) + + require.NoError(t, model.DB.Create(&model.AgentDeployment{ + DeploymentID: "dep_async_fail", + UserID: "22", + TemplateID: "architect", + Status: "Pending", + }).Error) + + startTemplateAgentAsync("dep_async_fail", 0, amStartArgs{ManagerDeploymentID: "dep_async_fail", UserID: "22", TemplateKey: "architect"}) + + var got model.AgentDeployment + require.Eventually(t, func() bool { + model.DB.Where("deployment_id = ?", "dep_async_fail").First(&got) + return got.Status == "failed" + }, 3*time.Second, 20*time.Millisecond, "AM failure should mark the record failed") + require.NotEmpty(t, got.FailureReason) +}