generated from VLADIMIR/template
add AnswerService
This commit is contained in:
@@ -20,7 +20,7 @@ import (
|
||||
// Проверяются initialize, tools/list (точное множество) и tools/call
|
||||
// connect — как это будет делать внешний MCP-клиент.
|
||||
func TestStreamableHTTPEndToEnd(t *testing.T) {
|
||||
svc := NewMCPService(&fakeGamePlayer{story: defaultStory(), game: defaultGame()})
|
||||
svc := NewMCPService(&fakeGamePlayer{story: defaultStory(), game: defaultGame()}, &fakeAnswerSubmitter{})
|
||||
|
||||
mcpHTTP := server.NewStreamableHTTPServer(svc.Server())
|
||||
srv := httptest.NewServer(mcpHTTP)
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
"evening_detective_server/internal/modules/storytelling"
|
||||
"evening_detective_server/internal/repos/actions_repo"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/answer_service"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
@@ -33,7 +33,11 @@ const defaultCallTimeout = 10 * time.Second
|
||||
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
|
||||
SubmitTeamAnswers(ctx context.Context, teamId int, password string, answers []game_service.Answer) error
|
||||
}
|
||||
|
||||
// AnswerSubmitter сохраняет ответы команды независимо от прохождения истории.
|
||||
type AnswerSubmitter interface {
|
||||
SubmitTeamAnswers(ctx context.Context, teamId int, password string, answers []answer_service.Answer) error
|
||||
}
|
||||
|
||||
// MCPService собирает MCP-сервер с игровыми инструментами. Сервис не
|
||||
@@ -41,12 +45,13 @@ type GamePlayer interface {
|
||||
// пароль явно — либо из ссылки на игру через инструмент connect, либо
|
||||
// напрямую.
|
||||
type MCPService struct {
|
||||
game GamePlayer
|
||||
game GamePlayer
|
||||
answers AnswerSubmitter
|
||||
}
|
||||
|
||||
// NewMCPService создаёт сервис поверх игровых сервисов.
|
||||
func NewMCPService(game GamePlayer) *MCPService {
|
||||
return &MCPService{game: game}
|
||||
func NewMCPService(game GamePlayer, answers AnswerSubmitter) *MCPService {
|
||||
return &MCPService{game: game, answers: answers}
|
||||
}
|
||||
|
||||
// Server собирает и возвращает MCP-сервер с зарегистрированными
|
||||
@@ -283,7 +288,7 @@ func (s *MCPService) handleMakeMove(ctx context.Context, req mcp.CallToolRequest
|
||||
type submitAnswersArgs struct {
|
||||
teamID int64
|
||||
password string
|
||||
answers []game_service.Answer
|
||||
answers []answer_service.Answer
|
||||
}
|
||||
|
||||
// parseSubmitAnswersArgs разбирает аргументы инструмента submit_answers:
|
||||
@@ -303,7 +308,7 @@ func parseSubmitAnswersArgs(args map[string]any) (submitAnswersArgs, error) {
|
||||
if !ok {
|
||||
return submitAnswersArgs{}, fmt.Errorf("требуется answers (массив ответов)")
|
||||
}
|
||||
answers := make([]game_service.Answer, 0, len(rawAnswers))
|
||||
answers := make([]answer_service.Answer, 0, len(rawAnswers))
|
||||
for i, raw := range rawAnswers {
|
||||
item, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
@@ -314,7 +319,7 @@ func parseSubmitAnswersArgs(args map[string]any) (submitAnswersArgs, error) {
|
||||
if questionCode == "" {
|
||||
return submitAnswersArgs{}, fmt.Errorf("answers[%d]: требуется questionCode", i)
|
||||
}
|
||||
answers = append(answers, game_service.Answer{
|
||||
answers = append(answers, answer_service.Answer{
|
||||
QuestionCode: questionCode,
|
||||
Text: answerText,
|
||||
})
|
||||
@@ -335,7 +340,7 @@ func (s *MCPService) handleSubmitAnswers(ctx context.Context, req mcp.CallToolRe
|
||||
ctx, cancel := context.WithTimeout(ctx, defaultCallTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := s.game.SubmitTeamAnswers(ctx, int(args.teamID), args.password, args.answers); err != nil {
|
||||
if err := s.answers.SubmitTeamAnswers(ctx, int(args.teamID), args.password, args.answers); err != nil {
|
||||
return mcp.NewToolResultError(fmt.Sprintf("submit_answers: %v", err)), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"evening_detective_server/internal/modules/storytelling"
|
||||
"evening_detective_server/internal/repos/actions_repo"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/answer_service"
|
||||
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -40,7 +40,15 @@ func (f *fakeGamePlayer) AddTeamAction(_ context.Context, teamID int, password,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeGamePlayer) SubmitTeamAnswers(_ context.Context, teamID int, password string, answers []game_service.Answer) error {
|
||||
type fakeAnswerSubmitter struct {
|
||||
err error
|
||||
teamID int
|
||||
password string
|
||||
answers []answer_service.Answer
|
||||
}
|
||||
|
||||
func (f *fakeAnswerSubmitter) SubmitTeamAnswers(_ context.Context, teamID int, password string, answers []answer_service.Answer) error {
|
||||
f.teamID, f.password, f.answers = teamID, password, answers
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
@@ -77,13 +85,15 @@ func defaultGame() *game_model.Game {
|
||||
// mcpFixture поднимает in-process MCP-клиент над сервисом с моком
|
||||
// GamePlayer. Каждый кейс создаёт СВЕЖИЙ сервис.
|
||||
type mcpFixture struct {
|
||||
client *client.Client
|
||||
client *client.Client
|
||||
answers *fakeAnswerSubmitter
|
||||
}
|
||||
|
||||
func newMCPFixture(t *testing.T, player GamePlayer) *mcpFixture {
|
||||
func newMCPFixture(t *testing.T, player *fakeGamePlayer) *mcpFixture {
|
||||
t.Helper()
|
||||
|
||||
svc := NewMCPService(player)
|
||||
answers := &fakeAnswerSubmitter{err: player.err}
|
||||
svc := NewMCPService(player, answers)
|
||||
|
||||
mcpClient, err := client.NewInProcessClient(svc.Server())
|
||||
if err != nil {
|
||||
@@ -105,7 +115,7 @@ func newMCPFixture(t *testing.T, player GamePlayer) *mcpFixture {
|
||||
t.Fatalf("client.Initialize() error = %v", err)
|
||||
}
|
||||
|
||||
return &mcpFixture{client: mcpClient}
|
||||
return &mcpFixture{client: mcpClient, answers: answers}
|
||||
}
|
||||
|
||||
// call вызывает инструмент с аргументами и возвращает текст результата.
|
||||
@@ -400,12 +410,12 @@ func TestParseConnectURL(t *testing.T) {
|
||||
|
||||
// TestNewMCPService — конструктор не требует БД и возвращает сервис.
|
||||
func TestNewMCPService(t *testing.T) {
|
||||
svc := NewMCPService(&fakeGamePlayer{})
|
||||
svc := NewMCPService(&fakeGamePlayer{}, &fakeAnswerSubmitter{})
|
||||
if svc == nil {
|
||||
t.Fatal("NewMCPService() = nil")
|
||||
}
|
||||
if svc.game == nil {
|
||||
t.Fatal("NewMCPService(): game == nil")
|
||||
if svc.game == nil || svc.answers == nil {
|
||||
t.Fatal("NewMCPService(): missing game or answers dependency")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +440,11 @@ func TestSubmitAnswers(t *testing.T) {
|
||||
if !strings.Contains(got, "entrance") {
|
||||
t.Fatalf("submit_answers: %q; want story JSON в ответе", got)
|
||||
}
|
||||
if f.answers.teamID != 10 || f.answers.password != "team-pass-1" || len(f.answers.answers) != 2 ||
|
||||
f.answers.answers[0] != (answer_service.Answer{QuestionCode: "q1", Text: "Дворецкий"}) ||
|
||||
f.answers.answers[1] != (answer_service.Answer{QuestionCode: "q2", Text: ""}) {
|
||||
t.Fatalf("answers not forwarded to AnswerSubmitter: %+v", f.answers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitAnswersValidation(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user