mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 23:41:12 +08:00
### Description
This PR ports the `GET /api/v1/agents/prompts` endpoint from the Python
backend to the Go backend.
### Changes Made
- **Handler**: Added `GetPrompts` method to `internal/handler/agent.go`.
- **Router**: Registered the `agents.GET("/prompts")` route in
`internal/router/router.go`.
- **Logic**: Leveraged the existing `service.LoadPrompt` utility to read
`analyze_task_system`, `analyze_task_user`, `next_step`, `reflect`, and
`citation_prompt` templates directly from the `rag/prompts` directory.
- **Unit Test**: Added `TestGetPrompts_Success` to
`internal/handler/agent_test.go` to mock the HTTP context and validate
the JSON response structure.
### Motivation
This is part of the ongoing effort to port the Agent API surface to Go.
Since this specific endpoint only serves static prompt templates and
does not require the complex DAG/Canvas execution engine, it can be
seamlessly and safely handled by the Go backend.
### Testing
- [x] Added automated unit test `TestGetPrompts_Success` (verified
passing).
- [x] Tested locally via `curl` against the Go server (port 9380) and
Python server (port 9384).
- [x] Verified that the Go JSON response structure and loaded prompt
text are logically 100% identical to the Python implementation.
807 lines
25 KiB
Go
807 lines
25 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"ragflow/internal/common"
|
|
"ragflow/internal/dao"
|
|
"ragflow/internal/entity"
|
|
"ragflow/internal/service"
|
|
)
|
|
|
|
// setupHandlerAgentsTestDB sets up SQLite in-memory DB with tables needed for agent handler tests.
|
|
func setupHandlerAgentsTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
TranslateError: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to open sqlite: %v", err)
|
|
}
|
|
|
|
if err := db.AutoMigrate(
|
|
&entity.User{},
|
|
&entity.UserCanvas{},
|
|
&entity.UserCanvasVersion{},
|
|
&entity.API4Conversation{},
|
|
); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
|
|
return db
|
|
}
|
|
|
|
// setupGinContextWithUserAndDB creates a gin context with pre-authenticated user
|
|
// and swaps dao.DB to the test database. Returns cleanup function.
|
|
func setupGinContextWithUserAndDB(t *testing.T, method, path string) (*gin.Context, *httptest.ResponseRecorder, *gorm.DB) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(method, path, nil)
|
|
c.Set("user", &entity.User{ID: "user-1"})
|
|
c.Set("user_id", "user-1")
|
|
|
|
db := setupHandlerAgentsTestDB(t)
|
|
orig := dao.DB
|
|
dao.DB = db
|
|
t.Cleanup(func() { dao.DB = orig })
|
|
|
|
return c, w, db
|
|
}
|
|
|
|
// draw a box with a slot for the old TestListAgents test.
|
|
// TestListAgents_Success verifies the ListAgents handler returns a valid response.
|
|
|
|
// TestListAgentVersionsHandler_Success verifies the happy path with real DB.
|
|
func TestListAgentVersionsHandler_Success(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, "GET", "/api/v1/agents/canvas-1/versions")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}}
|
|
|
|
// Insert canvas owned by user-1
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
|
|
// Insert 2 versions with staggered timestamps
|
|
now := time.Now()
|
|
db.Create(&entity.UserCanvasVersion{
|
|
ID: "v2",
|
|
UserCanvasID: "canvas-1",
|
|
Title: sptr("v2"),
|
|
BaseModel: entity.BaseModel{
|
|
UpdateTime: ptr(now.UnixMilli()),
|
|
},
|
|
})
|
|
db.Create(&entity.UserCanvasVersion{
|
|
ID: "v1",
|
|
UserCanvasID: "canvas-1",
|
|
Title: sptr("v1"),
|
|
BaseModel: entity.BaseModel{
|
|
UpdateTime: ptr(now.Add(-time.Hour).UnixMilli()),
|
|
},
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.ListAgentVersions(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
code, _ := resp["code"].(float64)
|
|
if code != float64(common.CodeSuccess) {
|
|
t.Fatalf("expected code 0, got %v: %v", code, resp["message"])
|
|
}
|
|
|
|
data, ok := resp["data"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected data array, got %T", resp["data"])
|
|
}
|
|
if len(data) != 2 {
|
|
t.Fatalf("expected 2 versions, got %d", len(data))
|
|
}
|
|
|
|
v2 := data[0].(map[string]interface{})
|
|
if v2["title"] != "v2" {
|
|
t.Errorf("expected v2 first, got %s", v2["title"])
|
|
}
|
|
}
|
|
|
|
// TestListAgentVersionsHandler_NoPermission verifies cross-user access is denied.
|
|
func TestListAgentVersionsHandler_NoPermission(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db := setupHandlerAgentsTestDB(t)
|
|
orig := dao.DB
|
|
dao.DB = db
|
|
t.Cleanup(func() { dao.DB = orig })
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("GET", "/api/v1/agents/canvas-b/versions", nil)
|
|
c.Set("user", &entity.User{ID: "user-a"})
|
|
c.Set("user_id", "user-a")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-b"}}
|
|
|
|
// Canvas owned by user-b
|
|
db.Create(&entity.UserCanvas{ID: "canvas-b", UserID: "user-b", Title: sptr("Not Yours")})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.ListAgentVersions(c)
|
|
|
|
var resp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
code, _ := resp["code"].(float64)
|
|
if code != float64(common.CodeOperatingError) {
|
|
t.Errorf("expected operating error code %d, got %v", common.CodeOperatingError, code)
|
|
}
|
|
}
|
|
|
|
// TestListAgentVersionsHandler_CanvasNotFound verifies behavior for non-existent canvas.
|
|
func TestListAgentVersionsHandler_CanvasNotFound(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db := setupHandlerAgentsTestDB(t)
|
|
orig := dao.DB
|
|
dao.DB = db
|
|
t.Cleanup(func() { dao.DB = orig })
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest("GET", "/api/v1/agents/non-existent/versions", nil)
|
|
c.Set("user", &entity.User{ID: "user-1"})
|
|
c.Set("user_id", "user-1")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "non-existent"}}
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.ListAgentVersions(c)
|
|
|
|
var resp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
code, _ := resp["code"].(float64)
|
|
if code != float64(common.CodeOperatingError) {
|
|
t.Errorf("expected operating error code %d, got %v", common.CodeOperatingError, code)
|
|
}
|
|
}
|
|
|
|
// TestGetAgentVersionHandler_Success verifies getting a specific version.
|
|
func TestGetAgentVersionHandler_Success(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, "GET", "/api/v1/agents/canvas-1/versions/v1")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "version_id", Value: "v1"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
db.Create(&entity.UserCanvasVersion{
|
|
ID: "v1",
|
|
UserCanvasID: "canvas-1",
|
|
Title: sptr("version-1"),
|
|
DSL: entity.JSONMap{"key": "value"},
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.GetAgentVersion(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
code, _ := resp["code"].(float64)
|
|
if code != float64(common.CodeSuccess) {
|
|
t.Fatalf("expected code 0, got %v: %v", code, resp["message"])
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected data object, got %T", resp["data"])
|
|
}
|
|
if data["title"] != "version-1" {
|
|
t.Errorf("expected title 'version-1', got %v", data["title"])
|
|
}
|
|
if _, ok := data["dsl"]; !ok {
|
|
t.Errorf("expected dsl field in version detail response")
|
|
}
|
|
}
|
|
|
|
// TestGetAgentVersionHandler_VersionNotFound verifies 404 for missing version.
|
|
func TestGetAgentVersionHandler_VersionNotFound(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, "GET", "/api/v1/agents/canvas-1/versions/non-existent")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "version_id", Value: "non-existent"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.GetAgentVersion(c)
|
|
|
|
var resp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
code, _ := resp["code"].(float64)
|
|
if code != float64(common.CodeNotFound) {
|
|
t.Errorf("expected not found code %d, got %v", common.CodeNotFound, code)
|
|
}
|
|
}
|
|
|
|
func TestListAgentSessionsHandlerSuccess(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, http.MethodGet, "/api/v1/agents/canvas-1/sessions")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
db.Create(&entity.API4Conversation{
|
|
ID: "session-1",
|
|
DialogID: "canvas-1",
|
|
UserID: "user-1",
|
|
Message: json.RawMessage(`[{"role":"assistant","content":"hello","prompt":"hidden"}]`),
|
|
Reference: json.RawMessage(`[]`),
|
|
BaseModel: entity.BaseModel{
|
|
UpdateTime: ptr(time.Now().UnixMilli()),
|
|
},
|
|
})
|
|
db.Create(&entity.API4Conversation{
|
|
ID: "session-2",
|
|
DialogID: "canvas-1",
|
|
UserID: "user-1",
|
|
Message: json.RawMessage(`[{"role":"user","content":"question"}]`),
|
|
Reference: json.RawMessage(`[]`),
|
|
BaseModel: entity.BaseModel{
|
|
UpdateTime: ptr(time.Now().Add(-time.Hour).UnixMilli()),
|
|
},
|
|
})
|
|
db.Create(&entity.API4Conversation{
|
|
ID: "session-other-agent",
|
|
DialogID: "canvas-other",
|
|
UserID: "user-1",
|
|
Message: json.RawMessage(`[{"role":"assistant","content":"other"}]`),
|
|
Reference: json.RawMessage(`[]`),
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.ListAgentSessions(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
if resp["code"] != float64(common.CodeSuccess) {
|
|
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, resp["code"], resp["message"])
|
|
}
|
|
if resp["total"] != float64(2) {
|
|
t.Fatalf("expected total 2, got %v", resp["total"])
|
|
}
|
|
|
|
data, ok := resp["data"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected data array, got %T", resp["data"])
|
|
}
|
|
if len(data) != 2 {
|
|
t.Fatalf("expected 2 sessions, got %d", len(data))
|
|
}
|
|
|
|
first := data[0].(map[string]interface{})
|
|
if first["agent_id"] != "canvas-1" {
|
|
t.Fatalf("expected agent_id canvas-1, got %v", first["agent_id"])
|
|
}
|
|
messages := first["message"].([]interface{})
|
|
message := messages[0].(map[string]interface{})
|
|
if _, ok := message["prompt"]; ok {
|
|
t.Fatalf("expected prompt to be stripped from list response")
|
|
}
|
|
}
|
|
|
|
func TestGetAgentSessionHandlerSuccess(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, http.MethodGet, "/api/v1/agents/canvas-1/sessions/session-1")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "session_id", Value: "session-1"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
db.Create(&entity.API4Conversation{
|
|
ID: "session-1",
|
|
DialogID: "canvas-1",
|
|
UserID: "user-1",
|
|
Message: json.RawMessage(`[{"role":"assistant","content":"hello"}]`),
|
|
Reference: json.RawMessage(`[]`),
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.GetAgentSession(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
if resp["code"] != float64(common.CodeSuccess) {
|
|
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, resp["code"], resp["message"])
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected data object, got %T", resp["data"])
|
|
}
|
|
if data["id"] != "session-1" {
|
|
t.Fatalf("expected session-1, got %v", data["id"])
|
|
}
|
|
if data["dialog_id"] != "canvas-1" {
|
|
t.Fatalf("expected dialog_id canvas-1, got %v", data["dialog_id"])
|
|
}
|
|
}
|
|
|
|
func TestGetAgentSessionHandlerRejectsSessionFromAnotherAgent(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, http.MethodGet, "/api/v1/agents/canvas-1/sessions/session-other")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "session_id", Value: "session-other"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
db.Create(&entity.API4Conversation{
|
|
ID: "session-other",
|
|
DialogID: "canvas-other",
|
|
UserID: "user-1",
|
|
Message: json.RawMessage(`[{"role":"assistant","content":"other"}]`),
|
|
Reference: json.RawMessage(`[]`),
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.GetAgentSession(c)
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
if resp["code"] == float64(common.CodeSuccess) {
|
|
t.Fatalf("expected non-success for cross-agent session, got response %v", resp)
|
|
}
|
|
if resp["data"] != nil {
|
|
t.Fatalf("expected nil data for cross-agent session, got %v", resp["data"])
|
|
}
|
|
}
|
|
|
|
func TestDeleteAgentSessionItemHandlerDeletesOnlyMatchingAgent(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, http.MethodDelete, "/api/v1/agents/canvas-1/sessions/session-1")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "session_id", Value: "session-1"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
db.Create(&entity.API4Conversation{
|
|
ID: "session-1",
|
|
DialogID: "canvas-1",
|
|
UserID: "user-1",
|
|
Message: json.RawMessage(`[]`),
|
|
Reference: json.RawMessage(`[]`),
|
|
})
|
|
db.Create(&entity.API4Conversation{
|
|
ID: "session-other",
|
|
DialogID: "canvas-other",
|
|
UserID: "user-1",
|
|
Message: json.RawMessage(`[]`),
|
|
Reference: json.RawMessage(`[]`),
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.DeleteAgentSessionItem(c)
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
if resp["code"] != float64(common.CodeSuccess) {
|
|
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, resp["code"], resp["message"])
|
|
}
|
|
if resp["data"] != true {
|
|
t.Fatalf("expected data true, got %v", resp["data"])
|
|
}
|
|
|
|
var deletedCount int64
|
|
if err := db.Model(&entity.API4Conversation{}).Where("id = ?", "session-1").Count(&deletedCount).Error; err != nil {
|
|
t.Fatalf("failed to count deleted session: %v", err)
|
|
}
|
|
if deletedCount != 0 {
|
|
t.Fatalf("expected session-1 to be deleted, count=%d", deletedCount)
|
|
}
|
|
|
|
var otherCount int64
|
|
if err := db.Model(&entity.API4Conversation{}).Where("id = ?", "session-other").Count(&otherCount).Error; err != nil {
|
|
t.Fatalf("failed to count other session: %v", err)
|
|
}
|
|
if otherCount != 1 {
|
|
t.Fatalf("expected session-other to remain, count=%d", otherCount)
|
|
}
|
|
}
|
|
|
|
func TestDeleteAgentSessionItemHandlerIgnoresSessionFromAnotherAgent(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, http.MethodDelete, "/api/v1/agents/canvas-1/sessions/session-other")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "session_id", Value: "session-other"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
db.Create(&entity.API4Conversation{
|
|
ID: "session-other",
|
|
DialogID: "canvas-other",
|
|
UserID: "user-1",
|
|
Message: json.RawMessage(`[]`),
|
|
Reference: json.RawMessage(`[]`),
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.DeleteAgentSessionItem(c)
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
if resp["code"] != float64(common.CodeSuccess) {
|
|
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, resp["code"], resp["message"])
|
|
}
|
|
if resp["data"] != false {
|
|
t.Fatalf("expected data false, got %v", resp["data"])
|
|
}
|
|
|
|
var count int64
|
|
if err := db.Model(&entity.API4Conversation{}).Where("id = ?", "session-other").Count(&count).Error; err != nil {
|
|
t.Fatalf("failed to count other session: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("expected cross-agent session to remain, count=%d", count)
|
|
}
|
|
}
|
|
|
|
func TestUpdateAgentTagsHandlerSuccess(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, http.MethodPut, "/api/v1/agents/canvas-1/tags")
|
|
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/agents/canvas-1/tags", strings.NewReader(`{"tags":["alpha","beta","alpha"]}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-1",
|
|
UserID: "user-1",
|
|
Title: sptr("Test Agent"),
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.UpdateAgentTags(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
code, _ := resp["code"].(float64)
|
|
if code != float64(common.CodeSuccess) {
|
|
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, code, resp["message"])
|
|
}
|
|
if resp["data"] != true {
|
|
t.Fatalf("expected data true, got %v", resp["data"])
|
|
}
|
|
|
|
var canvas entity.UserCanvas
|
|
if err := db.Where("id = ?", "canvas-1").First(&canvas).Error; err != nil {
|
|
t.Fatalf("failed to reload canvas: %v", err)
|
|
}
|
|
if canvas.Tags != "alpha,beta" {
|
|
t.Fatalf("expected normalized tags alpha,beta, got %q", canvas.Tags)
|
|
}
|
|
}
|
|
|
|
func TestUpdateAgentTagsHandlerNoPermission(t *testing.T) {
|
|
c, w, db := setupGinContextWithUserAndDB(t, http.MethodPut, "/api/v1/agents/canvas-b/tags")
|
|
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/agents/canvas-b/tags", strings.NewReader(`{"tags":["alpha"]}`))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-b"}}
|
|
|
|
db.Create(&entity.UserCanvas{
|
|
ID: "canvas-b",
|
|
UserID: "user-b",
|
|
Title: sptr("Private Agent"),
|
|
Permission: "me",
|
|
})
|
|
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.UpdateAgentTags(c)
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
code, _ := resp["code"].(float64)
|
|
if code != float64(common.CodeOperatingError) {
|
|
t.Fatalf("expected code %d, got %v: %v", common.CodeOperatingError, code, resp["message"])
|
|
}
|
|
if resp["data"] != false {
|
|
t.Fatalf("expected data false, got %v", resp["data"])
|
|
}
|
|
if resp["message"] != "Agent not found or no permission." {
|
|
t.Fatalf("unexpected message: %v", resp["message"])
|
|
}
|
|
}
|
|
|
|
// sptr returns a pointer to the given string.
|
|
// ptr returns a pointer to the given int64.
|
|
func ptr(v int64) *int64 { return &v }
|
|
|
|
// fakeAgentService satisfies the subset of AgentService used by the handler.
|
|
// It is injected via a wrapper to avoid importing the real DAO (which requires a DB).
|
|
type fakeAgentService struct {
|
|
result *service.ListAgentsResponse
|
|
code common.ErrorCode
|
|
err error
|
|
templates []*entity.CanvasTemplate
|
|
templatesErr error
|
|
}
|
|
|
|
// agentServiceIface is the minimum interface the handler depends on.
|
|
type agentServiceIface interface {
|
|
ListAgents(userID, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string, canvasCategory string) (*service.ListAgentsResponse, common.ErrorCode, error)
|
|
ListTemplates() ([]*entity.CanvasTemplate, error)
|
|
}
|
|
|
|
// agentHandlerTestable is a version of AgentHandler that accepts the interface.
|
|
type agentHandlerTestable struct {
|
|
svc agentServiceIface
|
|
}
|
|
|
|
func (h *agentHandlerTestable) listAgents(c *gin.Context) {
|
|
user, errorCode, errorMessage := GetUser(c)
|
|
if errorCode != common.CodeSuccess {
|
|
jsonError(c, errorCode, errorMessage)
|
|
return
|
|
}
|
|
result, code, err := h.svc.ListAgents(user.ID, "", 0, 0, "create_time", true, nil, "")
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"code": code, "data": false, "message": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": result, "message": "success"})
|
|
}
|
|
|
|
func (h *agentHandlerTestable) listTemplates(c *gin.Context) {
|
|
if _, errorCode, errorMessage := GetUser(c); errorCode != common.CodeSuccess {
|
|
jsonError(c, errorCode, errorMessage)
|
|
return
|
|
}
|
|
templates, err := h.svc.ListTemplates()
|
|
if err != nil {
|
|
jsonError(c, common.CodeServerError, err.Error())
|
|
return
|
|
}
|
|
if templates == nil {
|
|
templates = []*entity.CanvasTemplate{}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": templates, "message": "success"})
|
|
}
|
|
|
|
func (f *fakeAgentService) ListAgents(userID, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string, canvasCategory string) (*service.ListAgentsResponse, common.ErrorCode, error) {
|
|
return f.result, f.code, f.err
|
|
}
|
|
|
|
func (f *fakeAgentService) ListTemplates() ([]*entity.CanvasTemplate, error) {
|
|
return f.templates, f.templatesErr
|
|
}
|
|
|
|
func setupAgentRouter(svc agentServiceIface) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
h := &agentHandlerTestable{svc: svc}
|
|
r.GET("/api/v1/agents", func(c *gin.Context) {
|
|
c.Set("user", &entity.User{ID: "user-abc"})
|
|
h.listAgents(c)
|
|
})
|
|
r.GET("/api/v1/agents/templates", func(c *gin.Context) {
|
|
c.Set("user", &entity.User{ID: "user-abc"})
|
|
h.listTemplates(c)
|
|
})
|
|
r.GET("/api/v1/agents/templates_anon", func(c *gin.Context) {
|
|
// no user set → unauthenticated probe
|
|
h.listTemplates(c)
|
|
})
|
|
return r
|
|
}
|
|
|
|
func TestListAgents_Success(t *testing.T) {
|
|
title := "My Agent"
|
|
svc := &fakeAgentService{
|
|
result: &service.ListAgentsResponse{
|
|
Canvas: []*service.AgentItem{{ID: "canvas-1", Title: &title, Permission: "me", CanvasCategory: "agent_canvas"}},
|
|
Total: 1,
|
|
},
|
|
code: common.CodeSuccess,
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/agents", nil)
|
|
setupAgentRouter(svc).ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["code"] != float64(common.CodeSuccess) {
|
|
t.Errorf("expected code %d, got %v", common.CodeSuccess, body["code"])
|
|
}
|
|
data, ok := body["data"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("data is not a map: %v", body["data"])
|
|
}
|
|
if data["total"] != float64(1) {
|
|
t.Errorf("expected total=1, got %v", data["total"])
|
|
}
|
|
}
|
|
|
|
func TestListAgentTemplates_Success(t *testing.T) {
|
|
cnvType := "agent"
|
|
svc := &fakeAgentService{
|
|
templates: []*entity.CanvasTemplate{
|
|
{
|
|
ID: "template-1",
|
|
CanvasType: &cnvType,
|
|
CanvasCategory: "agent_canvas",
|
|
Title: entity.JSONMap{"en": "Sample"},
|
|
Description: entity.JSONMap{"en": "Sample desc"},
|
|
},
|
|
},
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/agents/templates", nil)
|
|
setupAgentRouter(svc).ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("unmarshal: %v body=%s", err, w.Body.String())
|
|
}
|
|
if body["code"] != float64(common.CodeSuccess) {
|
|
t.Errorf("code=%v want %d", body["code"], common.CodeSuccess)
|
|
}
|
|
data, ok := body["data"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("data is not an array: %v", body["data"])
|
|
}
|
|
if len(data) != 1 {
|
|
t.Fatalf("expected 1 template, got %d", len(data))
|
|
}
|
|
first := data[0].(map[string]interface{})
|
|
if first["id"] != "template-1" {
|
|
t.Errorf("id=%v want template-1", first["id"])
|
|
}
|
|
if first["canvas_category"] != "agent_canvas" {
|
|
t.Errorf("canvas_category=%v want agent_canvas", first["canvas_category"])
|
|
}
|
|
}
|
|
|
|
func TestListAgentTemplates_EmptyIsArrayNotNull(t *testing.T) {
|
|
svc := &fakeAgentService{templates: nil}
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/agents/templates", nil)
|
|
setupAgentRouter(svc).ServeHTTP(w, req)
|
|
|
|
var body map[string]interface{}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &body)
|
|
// JSON shape contract: never null - frontends do .map() on it.
|
|
if _, ok := body["data"].([]interface{}); !ok {
|
|
t.Fatalf("data is not an array when templates empty: %v (raw=%s)", body["data"], w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestListAgentTemplates_RequiresAuth(t *testing.T) {
|
|
svc := &fakeAgentService{templates: []*entity.CanvasTemplate{}}
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/agents/templates_anon", nil)
|
|
setupAgentRouter(svc).ServeHTTP(w, req)
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if code, _ := body["code"].(float64); int(code) == int(common.CodeSuccess) {
|
|
t.Errorf("expected non-success without auth, got body=%v", body)
|
|
}
|
|
}
|
|
|
|
func TestGetPrompts_Success(t *testing.T) {
|
|
c, w, _ := setupGinContextWithUserAndDB(t, http.MethodGet, "/api/v1/agents/prompts")
|
|
|
|
// Create handler with fake or real service.
|
|
h := NewAgentHandler(service.NewAgentService(), nil)
|
|
h.GetPrompts(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
code, _ := resp["code"].(float64)
|
|
if code != float64(common.CodeSuccess) {
|
|
t.Fatalf("expected code 0, got %v: %v", code, resp["message"])
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
// Check if keys exist
|
|
expectedKeys := []string{"task_analysis", "plan_generation", "reflection", "citation_guidelines"}
|
|
for _, key := range expectedKeys {
|
|
if _, ok := data[key]; !ok {
|
|
t.Errorf("expected key %s in data", key)
|
|
}
|
|
}
|
|
}
|