This commit is contained in:
2026-10-03 13:22:11 +07:00
parent 05090722a0
commit 2caa2c1a88
74 changed files with 44 additions and 676 deletions
@@ -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()
-23
View File
@@ -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:
+1 -11
View File
@@ -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)