package handler import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "ai-agent-scaffold-go/internal/model" "ai-agent-scaffold-go/internal/service" "ai-agent-scaffold-go/pkg/types" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) // ============================================================ // Stub Runner for handler tests // ============================================================ type stubRunner struct { sessionID string runResult []string runErr error } func (r *stubRunner) CreateSession(userID string) (string, error) { if r.sessionID != "" { return r.sessionID, nil } return "sess:" + userID + ":1", nil } func (r *stubRunner) Run(userID, sessionID string, content model.ChatContent) ([]string, error) { return r.runResult, r.runErr } func (r *stubRunner) Stream(userID, sessionID string, content model.ChatContent) (<-chan string, <-chan error) { outputs := make(chan string, 4) errs := make(chan error, 1) go func() { defer close(outputs) defer close(errs) if r.runErr != nil { errs <- r.runErr return } for _, s := range r.runResult { outputs <- s } }() return outputs, errs } // ============================================================ // 辅助函数 // ============================================================ func setupRouter() (*gin.Engine, *model.InMemoryAgentRegistry) { gin.SetMode(gin.TestMode) registry := model.NewInMemoryAgentRegistry() sessions := model.NewInMemorySessionStore() svc := service.NewChatService(registry, sessions) router := gin.New() RegisterRoutes(router, svc) return router, registry } func doRequest(router http.Handler, method, path, body string) *httptest.ResponseRecorder { var req *http.Request if body != "" { req = httptest.NewRequest(method, path, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, path, nil) } w := httptest.NewRecorder() router.ServeHTTP(w, req) return w } func parseEnvelope(t *testing.T, w *httptest.ResponseRecorder) Envelope { t.Helper() var resp Envelope err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(t, err) return resp } // ============================================================ // healthz 测试 // ============================================================ func TestHealthz_Returns200(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) w := doRequest(router, "GET", "/healthz", "") assert.Equal(t, 200, w.Code) } // ============================================================ // queryAgentConfigList 测试 // ============================================================ func TestQueryAgentConfigList_ReturnsList(t *testing.T) { router, registry := setupRouter() registry.Register(model.RegisteredAgent{AgentID: "1", AgentName: "a", AgentDesc: "desc a"}) registry.Register(model.RegisteredAgent{AgentID: "2", AgentName: "b", AgentDesc: "desc b"}) w := doRequest(router, "GET", "/api/v1/query_ai_agent_config_list", "") assert.Equal(t, 200, w.Code) resp := parseEnvelope(t, w) assert.Equal(t, types.CodeSuccess, resp.Code) data, _ := json.Marshal(resp.Data) var agents []AiAgentConfigResponse json.Unmarshal(data, &agents) assert.Len(t, agents, 2) } func TestQueryAgentConfigList_Empty_ReturnsEmptyArray(t *testing.T) { router, _ := setupRouter() w := doRequest(router, "GET", "/api/v1/query_ai_agent_config_list", "") assert.Equal(t, 200, w.Code) resp := parseEnvelope(t, w) assert.Equal(t, types.CodeSuccess, resp.Code) } // ============================================================ // createSession 测试 // ============================================================ func TestCreateSession_Success(t *testing.T) { router, registry := setupRouter() registry.Register(model.RegisteredAgent{ AgentID: "1", Runner: &stubRunner{sessionID: "sess:1:1"}, }) body := `{"agentId":"1","userId":"user1"}` w := doRequest(router, "POST", "/api/v1/create_session", body) assert.Equal(t, 200, w.Code) resp := parseEnvelope(t, w) assert.Equal(t, types.CodeSuccess, resp.Code) data, _ := json.Marshal(resp.Data) var sessResp CreateSessionResponse json.Unmarshal(data, &sessResp) assert.Equal(t, "sess:1:1", sessResp.SessionID) } func TestCreateSession_AgentNotFound_Returns0003(t *testing.T) { router, _ := setupRouter() body := `{"agentId":"nonexistent","userId":"user1"}` w := doRequest(router, "POST", "/api/v1/create_session", body) resp := parseEnvelope(t, w) assert.Equal(t, types.CodeAgentNotFound, resp.Code) } func TestCreateSession_MissingParams_Returns0002(t *testing.T) { router, _ := setupRouter() // 空 body w := doRequest(router, "POST", "/api/v1/create_session", "") resp := parseEnvelope(t, w) assert.Equal(t, types.CodeIllegalParameter, resp.Code) } func TestCreateSession_Query_Success(t *testing.T) { router, registry := setupRouter() registry.Register(model.RegisteredAgent{ AgentID: "1", Runner: &stubRunner{sessionID: "sess:u1:1"}, }) w := doRequest(router, "GET", "/api/v1/create_session?agentId=1&userId=user1", "") assert.Equal(t, 200, w.Code) resp := parseEnvelope(t, w) assert.Equal(t, types.CodeSuccess, resp.Code) } func TestCreateSession_Query_AgentNotFound_Returns0003(t *testing.T) { router, _ := setupRouter() w := doRequest(router, "GET", "/api/v1/create_session?agentId=x&userId=u", "") resp := parseEnvelope(t, w) assert.Equal(t, types.CodeAgentNotFound, resp.Code) } // ============================================================ // chat 测试 // ============================================================ func TestChat_Success(t *testing.T) { router, registry := setupRouter() registry.Register(model.RegisteredAgent{ AgentID: "1", Runner: &stubRunner{sessionID: "s1", runResult: []string{"hello"}}, }) body := `{"agentId":"1","userId":"u1","sessionId":"s1","message":"hi"}` w := doRequest(router, "POST", "/api/v1/chat", body) assert.Equal(t, 200, w.Code) resp := parseEnvelope(t, w) assert.Equal(t, types.CodeSuccess, resp.Code) data, _ := json.Marshal(resp.Data) var chatResp ChatResponse json.Unmarshal(data, &chatResp) assert.Equal(t, "hello", chatResp.Content) } func TestChat_AgentNotFound_Returns0003(t *testing.T) { router, _ := setupRouter() body := `{"agentId":"nonexistent","userId":"u1","message":"hi"}` w := doRequest(router, "POST", "/api/v1/chat", body) resp := parseEnvelope(t, w) assert.Equal(t, types.CodeAgentNotFound, resp.Code) } func TestChat_MissingBody_Returns0002(t *testing.T) { router, _ := setupRouter() w := doRequest(router, "POST", "/api/v1/chat", "") resp := parseEnvelope(t, w) assert.Equal(t, types.CodeIllegalParameter, resp.Code) } func TestChat_RunnerError_ReturnsUnknownError(t *testing.T) { router, registry := setupRouter() registry.Register(model.RegisteredAgent{ AgentID: "1", Runner: &stubRunner{sessionID: "s1", runErr: assert.AnError}, }) body := `{"agentId":"1","userId":"u1","sessionId":"s1","message":"hi"}` w := doRequest(router, "POST", "/api/v1/chat", body) resp := parseEnvelope(t, w) assert.Equal(t, types.CodeUnknownError, resp.Code) } // ============================================================ // chatStream 测试 // ============================================================ func TestChatStream_SSEHeaders(t *testing.T) { router, registry := setupRouter() registry.Register(model.RegisteredAgent{ AgentID: "1", Runner: &stubRunner{sessionID: "s1", runResult: []string{"chunk1", "chunk2"}}, }) body := `{"agentId":"1","userId":"u1","sessionId":"s1","message":"hi"}` w := doRequest(router, "POST", "/api/v1/chat_stream", body) assert.Equal(t, 200, w.Code) assert.Contains(t, w.Header().Get("Content-Type"), "text/event-stream") assert.Equal(t, "no-cache", w.Header().Get("Cache-Control")) } func TestChatStream_AgentNotFound_ReturnsError(t *testing.T) { router, _ := setupRouter() body := `{"agentId":"nonexistent","userId":"u1","message":"hi"}` w := doRequest(router, "POST", "/api/v1/chat_stream", body) // SSE 流式中错误通过 event 发送,HTTP 状态码仍为 200 assert.Equal(t, 200, w.Code) } // ============================================================ // writeError 测试 // ============================================================ func TestWriteError_AppError(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) writeError(c, types.NewAppError(types.CodeAgentNotFound, "not found")) var resp Envelope json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, types.CodeAgentNotFound, resp.Code) } func TestWriteError_UnknownError(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) writeError(c, assert.AnError) var resp Envelope json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, types.CodeUnknownError, resp.Code) } // ============================================================ // success 测试 // ============================================================ func TestSuccess_ReturnsEnvelope(t *testing.T) { resp := success(map[string]string{"key": "val"}) assert.Equal(t, types.CodeSuccess, resp.Code) assert.Equal(t, types.InfoSuccess, resp.Info) assert.NotNil(t, resp.Data) }