From a1f2b999350acbf2ff6cbfacf8e4096c24944714 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 7 Jun 2026 13:32:32 +0100 Subject: [PATCH] test(e2e): solve_grid over real MCP stdio with fake LLM --- test/e2e/fakellm_test.go | 23 ++++++++++++ test/e2e/main_test.go | 18 ++++++++++ test/e2e/server_test.go | 78 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 test/e2e/fakellm_test.go create mode 100644 test/e2e/main_test.go create mode 100644 test/e2e/server_test.go diff --git a/test/e2e/fakellm_test.go b/test/e2e/fakellm_test.go new file mode 100644 index 0000000..9196704 --- /dev/null +++ b/test/e2e/fakellm_test.go @@ -0,0 +1,23 @@ +//go:build e2e + +package e2e + +import ( + "io" + "net/http" + "net/http/httptest" +) + +// startFakeLLM returns an OpenAI-compatible endpoint that always selects tiles 0,1,2. +func startFakeLLM() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v1/chat/completions": + _, _ = io.WriteString(w, `{"choices":[{"message":{"content":"{\"tiles\":[0,1,2],\"confidence\":0.95,\"recheck_recommended\":false}"}}]}`) + case "/v1/audio/transcriptions": + _, _ = io.WriteString(w, `{"text":"fortytwo"}`) + default: + http.NotFound(w, r) + } + })) +} diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go new file mode 100644 index 0000000..2e7877a --- /dev/null +++ b/test/e2e/main_test.go @@ -0,0 +1,18 @@ +//go:build e2e + +package e2e + +import ( + "os" + "os/exec" + "testing" +) + +func TestMain(m *testing.M) { + build := exec.Command("go", "build", "-o", "../../bin/captcha-solver-mcp", "../../cmd/captcha-solver-mcp") + build.Stdout, build.Stderr = os.Stdout, os.Stderr + if err := build.Run(); err != nil { + os.Exit(1) + } + os.Exit(m.Run()) +} diff --git a/test/e2e/server_test.go b/test/e2e/server_test.go new file mode 100644 index 0000000..2a90964 --- /dev/null +++ b/test/e2e/server_test.go @@ -0,0 +1,78 @@ +//go:build e2e + +package e2e + +import ( + "context" + "encoding/json" + "os/exec" + "testing" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func TestSolveGridOverStdio(t *testing.T) { + llmSrv := startFakeLLM() + defer llmSrv.Close() + + cmd := exec.Command("../../bin/captcha-solver-mcp") + cmd.Env = append(cmd.Environ(), + "CAPTCHA_LLM_PROVIDER=openai-compatible", + "CAPTCHA_LLM_BASE_URL="+llmSrv.URL, + "CAPTCHA_LLM_API_KEY=test", + "CAPTCHA_LLM_MODEL=fake", + ) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client := mcp.NewClient(&mcp.Implementation{Name: "e2e", Version: "v0"}, nil) + session, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil) + if err != nil { + t.Fatalf("connect: %v", err) + } + defer func() { _ = session.Close() }() + + res, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "solve_grid_captcha", + Arguments: map[string]any{ + "screenshot": "iVBORw0KGgo=", + "instruction": "select all buses", + "rows": 3, + "cols": 3, + "image_width": 300, + "image_height": 300, + }, + }) + if err != nil { + t.Fatalf("call: %v", err) + } + assertTiles(t, res, []int{0, 1, 2}) +} + +// assertTiles unmarshals the structured tool result and compares tiles. +func assertTiles(t *testing.T, res *mcp.CallToolResult, want []int) { + t.Helper() + if res.IsError { + t.Fatalf("tool returned error: %+v", res.Content) + } + raw, err := json.Marshal(res.StructuredContent) + if err != nil { + t.Fatalf("marshal structured: %v", err) + } + var out struct { + Tiles []int `json:"tiles"` + } + if err := json.Unmarshal(raw, &out); err != nil { + t.Fatalf("unmarshal %q: %v", raw, err) + } + if len(out.Tiles) != len(want) { + t.Fatalf("tiles=%v want %v", out.Tiles, want) + } + for i := range want { + if out.Tiles[i] != want[i] { + t.Fatalf("tiles=%v want %v", out.Tiles, want) + } + } +}