test(agent): 异步部署的确定性测试(不阻塞 + Pending→running / →failed)
- 慢 AM mock 验证 startTemplateAgentAsync 立即返回(不阻塞 30s)。 - Pending 记录在 AM 响应后自动回填 running + runtime_id + subdomain。 - AM 失败时记录标 failed + failure_reason,不卡在 Pending。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user