Files
vikingowl ec9433d783 chore(lint): clear remaining errcheck and staticcheck findings
Brings the project to a clean `make lint` baseline (0 issues).

Mechanical:
- Wrap deferred resp.Body.Close() in closures (router/discovery.go,
  router/probe.go) so the unchecked return surfaces as `_ = ...`.
- Apply `_ = ...` (single or multi-return blank) to test-file calls
  that intentionally ignore errors: os.MkdirAll / os.WriteFile / os.Chdir
  in setup paths, Close / Shutdown in teardown, Submit / Spawn / Send /
  LoadDir in tests that assert on side effects.

Structural:
- engine.handleRequestTooLarge drops the unused req parameter and
  rebuilds the request from compacted history (SA4009 — argument was
  overwritten before first use).
- provider.ClassifyHTTPStatus and google.applyCapabilityOverrides switch
  to tagged switches over the discriminator (QF1002).
- tui.app.go MouseWheel + inputMode and cmd/gnoma main slm-status use
  tagged switches in place of equality chains (QF1003).
- cmd/gnoma main.go merges a var decl with its immediate assignment
  (S1021).
- Three empty-branch sites (dispatcher_test, loader_test,
  coordinator_test) become real assertions or get the dead `if` removed
  (SA9003).
2026-05-19 17:53:42 +02:00

278 lines
6.9 KiB
Go

package mcp
import (
"context"
"encoding/json"
"log/slog"
"os"
"path/filepath"
"testing"
"time"
)
// writeMockServer creates a bash script that reads a JSON-RPC request from stdin
// and writes a canned response to stdout. Returns the script path.
func writeMockServer(t *testing.T, responseJSON string) string {
t.Helper()
dir := t.TempDir()
script := filepath.Join(dir, "mock-server.sh")
content := `#!/bin/bash
read -r line
echo '` + responseJSON + `'
`
if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
t.Fatalf("write mock server: %v", err)
}
return script
}
// writeMockServerMulti creates a script that responds to each line of input
// with a corresponding line of output from the provided responses.
func writeMockServerMulti(t *testing.T, responses []string) string {
t.Helper()
dir := t.TempDir()
script := filepath.Join(dir, "mock-server.sh")
var body string
for _, r := range responses {
body += `read -r line
echo '` + r + `'
`
}
content := "#!/bin/bash\n" + body
if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
t.Fatalf("write mock server: %v", err)
}
return script
}
func testLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
}
func TestTransport_StartAndClose(t *testing.T) {
script := writeMockServer(t, `{"jsonrpc":"2.0","id":1,"result":{}}`)
tr := NewTransport("bash", []string{script}, nil, testLogger())
ctx := context.Background()
if err := tr.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
if err := tr.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
}
func TestTransport_Call_Success(t *testing.T) {
resp := `{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}`
script := writeMockServer(t, resp)
tr := NewTransport("bash", []string{script}, nil, testLogger())
ctx := context.Background()
if err := tr.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
defer func() { _ = tr.Close() }()
result, err := tr.Call(ctx, "tools/list", nil)
if err != nil {
t.Fatalf("Call: %v", err)
}
var parsed struct {
Tools []json.RawMessage `json:"tools"`
}
if err := json.Unmarshal(result, &parsed); err != nil {
t.Fatalf("unmarshal result: %v", err)
}
if len(parsed.Tools) != 0 {
t.Errorf("expected empty tools, got %d", len(parsed.Tools))
}
}
func TestTransport_Call_RPCError(t *testing.T) {
resp := `{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found"}}`
script := writeMockServer(t, resp)
tr := NewTransport("bash", []string{script}, nil, testLogger())
ctx := context.Background()
if err := tr.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
defer func() { _ = tr.Close() }()
_, err := tr.Call(ctx, "nonexistent", nil)
if err == nil {
t.Fatal("expected error for RPC error response")
}
var rpcErr *RPCError
if !errorAs(err, &rpcErr) {
t.Fatalf("expected *RPCError, got %T: %v", err, err)
}
if rpcErr.Code != -32601 {
t.Errorf("error code = %d, want -32601", rpcErr.Code)
}
}
func TestTransport_Call_Timeout(t *testing.T) {
// Script that hangs forever.
dir := t.TempDir()
script := filepath.Join(dir, "hang.sh")
if err := os.WriteFile(script, []byte("#!/bin/bash\nsleep 60\n"), 0o755); err != nil {
t.Fatalf("write: %v", err)
}
tr := NewTransport("bash", []string{script}, nil, testLogger())
ctx := context.Background()
if err := tr.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
defer func() { _ = tr.Close() }()
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
_, err := tr.Call(ctx, "tools/list", nil)
if err == nil {
t.Fatal("expected timeout error")
}
}
func TestTransport_Call_EnvPassed(t *testing.T) {
// Script that echoes an env var as the result.
dir := t.TempDir()
script := filepath.Join(dir, "env-echo.sh")
content := `#!/bin/bash
read -r line
echo "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"val\":\"$TEST_MCP_VAR\"}}"
`
if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
t.Fatalf("write: %v", err)
}
env := map[string]string{"TEST_MCP_VAR": "hello_mcp"}
tr := NewTransport("bash", []string{script}, env, testLogger())
ctx := context.Background()
if err := tr.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
defer func() { _ = tr.Close() }()
result, err := tr.Call(ctx, "test", nil)
if err != nil {
t.Fatalf("Call: %v", err)
}
var parsed struct {
Val string `json:"val"`
}
if err := json.Unmarshal(result, &parsed); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if parsed.Val != "hello_mcp" {
t.Errorf("val = %q, want %q", parsed.Val, "hello_mcp")
}
}
func TestTransport_Notify(t *testing.T) {
// Notification doesn't expect a response, so the script just reads and exits.
dir := t.TempDir()
script := filepath.Join(dir, "notify.sh")
content := `#!/bin/bash
read -r line
# Write the received line to a file so we can verify it was sent.
echo "$line" > "` + filepath.Join(dir, "received.json") + `"
`
if err := os.WriteFile(script, []byte(content), 0o755); err != nil {
t.Fatalf("write: %v", err)
}
tr := NewTransport("bash", []string{script}, nil, testLogger())
ctx := context.Background()
if err := tr.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
if err := tr.Notify(ctx, "initialized", nil); err != nil {
t.Fatalf("Notify: %v", err)
}
// Give the script a moment to write the file.
time.Sleep(50 * time.Millisecond)
_ = tr.Close()
data, err := os.ReadFile(filepath.Join(dir, "received.json"))
if err != nil {
t.Fatalf("read received: %v", err)
}
var notif Notification
if err := json.Unmarshal(data, &notif); err != nil {
t.Fatalf("unmarshal notification: %v", err)
}
if notif.Method != "initialized" {
t.Errorf("method = %q, want %q", notif.Method, "initialized")
}
}
func TestTransport_MultipleCalls(t *testing.T) {
responses := []string{
`{"jsonrpc":"2.0","id":1,"result":{"step":"first"}}`,
`{"jsonrpc":"2.0","id":2,"result":{"step":"second"}}`,
}
script := writeMockServerMulti(t, responses)
tr := NewTransport("bash", []string{script}, nil, testLogger())
ctx := context.Background()
if err := tr.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
defer func() { _ = tr.Close() }()
// First call.
r1, err := tr.Call(ctx, "first", nil)
if err != nil {
t.Fatalf("Call 1: %v", err)
}
var p1 struct{ Step string }
_ = json.Unmarshal(r1, &p1)
if p1.Step != "first" {
t.Errorf("call 1 step = %q, want %q", p1.Step, "first")
}
// Second call.
r2, err := tr.Call(ctx, "second", nil)
if err != nil {
t.Fatalf("Call 2: %v", err)
}
var p2 struct{ Step string }
_ = json.Unmarshal(r2, &p2)
if p2.Step != "second" {
t.Errorf("call 2 step = %q, want %q", p2.Step, "second")
}
}
// errorAs is a local helper since errors.As requires a pointer to interface.
func errorAs[T error](err error, target *T) bool {
for err != nil {
if t, ok := err.(T); ok {
*target = t
return true
}
if u, ok := err.(interface{ Unwrap() error }); ok {
err = u.Unwrap()
} else {
return false
}
}
return false
}