mirror of
https://github.com/lukaszraczylo/mcp-captcharesolver.git
synced 2026-10-12 00:45:28 +00:00
- Add Samples to TextOptions/GridOptions; N>1 runs N solves at temp 0.5 and majority-votes (text: most frequent; grid: tiles in strict majority). N<=1 keeps the single deterministic call path unchanged. - Add NoTargets to GridResult so callers know to click Verify instead of looping; prompt instructs empty tiles + no_targets:true when nothing matches. - Grid prompt: numbered cells 0..R*C-1, select any partial overlap. - Text prompt: disambiguate similar glyphs (0/O 1/l/I 5/S 2/Z 8/B). - Rotation prompt: reason step-by-step then emit final JSON.
98 lines
2.7 KiB
Go
98 lines
2.7 KiB
Go
package captcha
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"testing"
|
|
|
|
"github.com/lukaszraczylo/captcha-solver-mcp/internal/llm"
|
|
)
|
|
|
|
// smallPNG returns a tiny valid PNG for exercising preprocessing paths.
|
|
func smallPNG(t *testing.T) []byte {
|
|
t.Helper()
|
|
img := image.NewRGBA(image.Rect(0, 0, 6, 6))
|
|
for y := 0; y < 6; y++ {
|
|
for x := 0; x < 6; x++ {
|
|
img.Set(x, y, color.RGBA{R: uint8(x * 40), G: uint8(y * 40), B: 80, A: 255})
|
|
}
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, img); err != nil {
|
|
t.Fatalf("encode png: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func TestSolveGrid(t *testing.T) {
|
|
f := &llm.Fake{VisionResp: `{"tiles":[0,4,8],"confidence":0.8,"recheck_recommended":true}`}
|
|
res, err := SolveGrid(context.Background(), f, []byte{1}, "image/png", GridOptions{
|
|
Instruction: "select all buses", Rows: 3, Cols: 3, ImageWidth: 300, ImageHeight: 300,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(res.Tiles) != 3 || res.Tiles[0] != 0 {
|
|
t.Fatalf("tiles %+v", res.Tiles)
|
|
}
|
|
if len(res.Centroids) != 3 || res.Centroids[1].X != 150 { // tile 4 centre
|
|
t.Fatalf("centroids %+v", res.Centroids)
|
|
}
|
|
if !res.RecheckRecommended {
|
|
t.Fatal("recheck flag lost")
|
|
}
|
|
}
|
|
|
|
func TestSolveGridVoting(t *testing.T) {
|
|
f := &llm.Fake{VisionResponses: []string{
|
|
`{"tiles":[0,1],"confidence":0.9}`,
|
|
`{"tiles":[0,2],"confidence":0.9}`,
|
|
`{"tiles":[0,1],"confidence":0.9}`,
|
|
}}
|
|
res, err := SolveGrid(context.Background(), f, []byte{1}, "image/png", GridOptions{
|
|
Instruction: "select all buses", Rows: 3, Cols: 3, Samples: 3,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 0 in 3/3, 1 in 2/3 (>half), 2 in 1/3 (dropped).
|
|
if len(res.Tiles) != 2 || res.Tiles[0] != 0 || res.Tiles[1] != 1 {
|
|
t.Fatalf("voted tiles %+v, want [0 1]", res.Tiles)
|
|
}
|
|
if len(f.VisionCalls) != 3 {
|
|
t.Fatalf("expected 3 vision calls, got %d", len(f.VisionCalls))
|
|
}
|
|
}
|
|
|
|
func TestSolveGridNoTargets(t *testing.T) {
|
|
f := &llm.Fake{VisionResp: `{"tiles":[],"confidence":0.9,"no_targets":true}`}
|
|
res, err := SolveGrid(context.Background(), f, []byte{1}, "image/png", GridOptions{
|
|
Instruction: "select all buses", Rows: 3, Cols: 3,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !res.NoTargets {
|
|
t.Fatal("no_targets flag lost")
|
|
}
|
|
if len(res.Tiles) != 0 {
|
|
t.Fatalf("expected empty tiles, got %+v", res.Tiles)
|
|
}
|
|
}
|
|
|
|
func TestSolveGridPreprocessing(t *testing.T) {
|
|
f := &llm.Fake{VisionResp: `{"tiles":[0,4],"confidence":0.8}`}
|
|
res, err := SolveGrid(context.Background(), f, smallPNG(t), "image/png", GridOptions{
|
|
Instruction: "select all buses", Rows: 3, Cols: 3, Annotate: true, Upscale: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(res.Tiles) != 2 {
|
|
t.Fatalf("expected 2 tiles, got %+v", res.Tiles)
|
|
}
|
|
}
|