323 lines
9.2 KiB
Go
323 lines
9.2 KiB
Go
|
|
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)
|
|||
|
|
}
|