generated from VLADIMIR/template
fix
This commit is contained in:
@@ -15,10 +15,6 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
// Интеграционный тест HTTP-«клея»: MCP-сервер монтируется как
|
||||
// streamable HTTP (эндпоинт /api/mcp) и вызывается через HTTP-клиент mcp-go.
|
||||
// Проверяются initialize, tools/list (точное множество) и tools/call
|
||||
// connect — как это будет делать внешний MCP-клиент.
|
||||
func TestStreamableHTTPEndToEnd(t *testing.T) {
|
||||
svc := NewMCPService(&fakeGamePlayer{story: defaultStory(), game: defaultGame()}, &fakeAnswerSubmitter{})
|
||||
|
||||
@@ -29,8 +25,6 @@ func TestStreamableHTTPEndToEnd(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
// ВАЖНО: клиент POST-ит строго на переданный baseURL без добавления
|
||||
// пути — эндпоинт /api/mcp передаём явно.
|
||||
mcpClient, err := client.NewStreamableHttpClient(srv.URL + "/api/mcp")
|
||||
if err != nil {
|
||||
t.Fatalf("NewStreamableHttpClient() error = %v", err)
|
||||
@@ -77,8 +71,6 @@ func TestStreamableHTTPEndToEnd(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка, что структуры моков сериализуемы (защита от изменения типов
|
||||
// storytelling/game_service без обновления моков).
|
||||
func TestMockTypesUsable(_ *testing.T) {
|
||||
_ = defaultStory()
|
||||
_ = defaultGame()
|
||||
|
||||
@@ -19,27 +19,17 @@ import (
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
// defaultCallTimeout — таймаут на выполнение одного инструмента:
|
||||
// защита от зависших запросов к БД (10s — как у прежнего HTTP-прокси).
|
||||
const defaultCallTimeout = 10 * time.Second
|
||||
|
||||
// GamePlayer — граница доступа к игровым сервисам, реализуемая
|
||||
// *gameplay_service.GameplayService; интерфейс позволяет тестировать сервис
|
||||
// без БД.
|
||||
type GamePlayer interface {
|
||||
GetTeamActions(ctx context.Context, teamID int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error)
|
||||
AddTeamAction(ctx context.Context, teamID int, password, actionCode string, viaMCP bool) error
|
||||
}
|
||||
|
||||
// AnswerSubmitter сохраняет ответы команды независимо от прохождения истории.
|
||||
type AnswerSubmitter interface {
|
||||
SubmitTeamAnswers(ctx context.Context, teamID int, password string, answers []answer_service.Answer) error
|
||||
}
|
||||
|
||||
// MCPService собирает MCP-сервер с игровыми инструментами. Сервис не
|
||||
// хранит состояние между вызовами: каждый вызов получает id команды и
|
||||
// пароль явно — либо из ссылки на игру через инструмент connect, либо
|
||||
// напрямую.
|
||||
type MCPService struct {
|
||||
game GamePlayer
|
||||
answers AnswerSubmitter
|
||||
@@ -124,13 +114,6 @@ type connectArgs struct {
|
||||
password string
|
||||
}
|
||||
|
||||
// parseConnectURL разбирает ссылку на игру вида /team-story/{id}?password=...
|
||||
// и возвращает id команды и пароль.
|
||||
//
|
||||
// Ссылка используется ТОЛЬКО как носитель id и пароля: MCP-сервер не
|
||||
// выполняет HTTP-запросов по ней, поэтому схема и хост не проверяются
|
||||
// (протокол-относительная ссылка //host/team-story/{id}?password=...
|
||||
// принимается, «mailto:...» отклоняется на проверке пути).
|
||||
func parseConnectURL(rawURL string) (connectArgs, error) {
|
||||
if strings.TrimSpace(rawURL) == "" {
|
||||
return connectArgs{}, fmt.Errorf("требуется url")
|
||||
@@ -280,10 +263,6 @@ type submitAnswersArgs struct {
|
||||
answers []answer_service.Answer
|
||||
}
|
||||
|
||||
// parseSubmitAnswersArgs разбирает аргументы инструмента submit_answers:
|
||||
// answers — массив объектов {questionCode, answer} (JSON-массив приходит
|
||||
// как []any с map[string]any элементами). Ключ questionCode совпадает с
|
||||
// REST JSON (camelCase) — единый формат для клиентов.
|
||||
func parseSubmitAnswersArgs(args map[string]any) (submitAnswersArgs, error) {
|
||||
teamID, ok := argInt64(args["team_id"])
|
||||
if !ok {
|
||||
@@ -345,8 +324,6 @@ func (s *MCPService) handleSubmitAnswers(ctx context.Context, req mcp.CallToolRe
|
||||
return mcp.NewToolResultText(jsonBody), nil
|
||||
}
|
||||
|
||||
// argInt64 достаёт int64 из аргумента инструмента: числа приходят как
|
||||
// float64 (JSON), строки — как string.
|
||||
func argInt64(v any) (int64, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
|
||||
@@ -16,8 +16,6 @@ import (
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// fakeGamePlayer — мок GamePlayer с предзаданными историей, игрой и
|
||||
// ошибками (реализует интерфейс GamePlayer, БД не нужна).
|
||||
type fakeGamePlayer struct {
|
||||
story *storytelling.Story
|
||||
game *game_model.Game
|
||||
@@ -82,8 +80,6 @@ func defaultGame() *game_model.Game {
|
||||
}
|
||||
}
|
||||
|
||||
// mcpFixture поднимает встроенный MCP-клиент над сервисом с моком
|
||||
// GamePlayer. Каждый кейс создаёт СВЕЖИЙ сервис.
|
||||
type mcpFixture struct {
|
||||
client *client.Client
|
||||
answers *fakeAnswerSubmitter
|
||||
@@ -118,7 +114,6 @@ func newMCPFixture(t *testing.T, player *fakeGamePlayer) *mcpFixture {
|
||||
return &mcpFixture{client: mcpClient, answers: answers}
|
||||
}
|
||||
|
||||
// call вызывает инструмент с аргументами и возвращает текст результата.
|
||||
func (f *mcpFixture) call(t *testing.T, name string, args map[string]any) string {
|
||||
t.Helper()
|
||||
request := mcp.CallToolRequest{}
|
||||
@@ -141,7 +136,6 @@ func (f *mcpFixture) call(t *testing.T, name string, args map[string]any) string
|
||||
return text.String()
|
||||
}
|
||||
|
||||
// toolNames возвращает имена инструментов сервера.
|
||||
func (f *mcpFixture) toolNames(t *testing.T) []string {
|
||||
t.Helper()
|
||||
toolsResult, err := f.client.ListTools(context.Background(), mcp.ListToolsRequest{})
|
||||
@@ -199,8 +193,6 @@ func TestConnectRelativeURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnectProtocolRelativeURL: протокол-относительная ссылка принимается
|
||||
// (запросы по ней не выполняются — из ссылки берутся только id и пароль).
|
||||
func TestConnectProtocolRelativeURL(t *testing.T) {
|
||||
f := newMCPFixture(t, &fakeGamePlayer{story: defaultStory(), game: defaultGame()})
|
||||
|
||||
@@ -313,7 +305,7 @@ func TestMakeMove(t *testing.T) {
|
||||
f := newMCPFixture(t, player)
|
||||
|
||||
got := f.call(t, "make_move", map[string]any{
|
||||
"team_id": "10", // строка тоже принимается
|
||||
"team_id": "10",
|
||||
"password": "team-pass-1",
|
||||
"code": "hall",
|
||||
})
|
||||
@@ -408,7 +400,6 @@ func TestParseConnectURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewMCPService — конструктор не требует БД и возвращает сервис.
|
||||
func TestNewMCPService(t *testing.T) {
|
||||
svc := NewMCPService(&fakeGamePlayer{}, &fakeAnswerSubmitter{})
|
||||
if svc == nil {
|
||||
@@ -419,7 +410,6 @@ func TestNewMCPService(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultCallTimeout — защита от случайного изменения таймаута.
|
||||
func TestDefaultCallTimeout(t *testing.T) {
|
||||
if defaultCallTimeout != 10*time.Second {
|
||||
t.Fatalf("defaultCallTimeout = %v; want 10s", defaultCallTimeout)
|
||||
|
||||
Reference in New Issue
Block a user