- Add --debug[=DIR] / --profile[=DIR] flags that write run artefacts
(patterm.log, events.jsonl, per-child raw PTY captures, CPU + heap
+ goroutine pprof) to a dir without polluting stdout/stderr.
- Strengthen vendor-TUI orientation in three places (MCP
initialize.instructions, the spawn_agent tool description, and
help('spawning')) to head off codex's habits of poking the Unix
socket via perl and shelling out to launch peers — both bypass
caller identity and produce orphaned top-level tabs.
- Fix click-and-drag text selection from alt-screen TUIs. Host SGR
mouse reporting now follows the focused child's screen side
instead of being permanently armed; alt-screen TUIs that need
mouse re-enable it themselves and the toggle is forwarded.
- Move drawSidebar() off the per-PTY-chunk hot path. Long claude
session resume was paying a full sidebar rebuild for every
scrolled chunk; the chrome ticker now drains a dirty flag at 60 Hz.
- Gate the per-chunk Title() CGO poll on a containsOSC scan so
codex/ratatui's many SGR-only chunks no longer pay a CGO call each.
152 lines
4.6 KiB
Go
152 lines
4.6 KiB
Go
package mcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestInitializeReturnsCapabilities(t *testing.T) {
|
|
s := &Server{}
|
|
req := []byte(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"claude","version":"1.0"}}}`)
|
|
resp := s.dispatch("", req)
|
|
if resp == nil {
|
|
t.Fatal("expected response for initialize")
|
|
}
|
|
var parsed struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id"`
|
|
Result map[string]interface{} `json:"result"`
|
|
Error *struct {
|
|
Code int `json:"code"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(resp, &parsed); err != nil {
|
|
t.Fatalf("parse: %v\n%s", err, resp)
|
|
}
|
|
if parsed.Error != nil {
|
|
t.Fatalf("initialize returned error: %+v", parsed.Error)
|
|
}
|
|
if parsed.Result["protocolVersion"] == nil {
|
|
t.Fatalf("missing protocolVersion: %+v", parsed.Result)
|
|
}
|
|
caps, ok := parsed.Result["capabilities"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("capabilities not object: %+v", parsed.Result)
|
|
}
|
|
if caps["tools"] == nil {
|
|
t.Fatalf("tools capability missing: %+v", caps)
|
|
}
|
|
// patterm-specific orientation: clients show this to the underlying
|
|
// LLM, so it's our primary hook for steering vendor TUIs (codex in
|
|
// particular) toward the MCP tool surface instead of shell-ing out.
|
|
instructions, ok := parsed.Result["instructions"].(string)
|
|
if !ok || instructions == "" {
|
|
t.Fatalf("instructions missing or wrong type: %+v", parsed.Result)
|
|
}
|
|
}
|
|
|
|
func TestInitializedNotificationSuppressesResponse(t *testing.T) {
|
|
s := &Server{}
|
|
req := []byte(`{"jsonrpc":"2.0","method":"notifications/initialized"}`)
|
|
resp := s.dispatch("", req)
|
|
if resp != nil {
|
|
t.Fatalf("notification produced a response: %s", resp)
|
|
}
|
|
}
|
|
|
|
func TestToolsListReturnsConcreteSchemas(t *testing.T) {
|
|
s := &Server{}
|
|
req := []byte(`{"jsonrpc":"2.0","id":2,"method":"tools/list"}`)
|
|
resp := s.dispatch("", req)
|
|
if resp == nil {
|
|
t.Fatal("expected response for tools/list")
|
|
}
|
|
var parsed struct {
|
|
Result map[string]interface{} `json:"result"`
|
|
Error *struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(resp, &parsed); err != nil {
|
|
t.Fatalf("parse: %v\n%s", err, resp)
|
|
}
|
|
if parsed.Error != nil {
|
|
t.Fatalf("tools/list returned error: %+v", parsed.Error)
|
|
}
|
|
tools, ok := parsed.Result["tools"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("tools not array: %+v", parsed.Result)
|
|
}
|
|
if len(tools) == 0 {
|
|
t.Fatalf("expected at least one tool, got 0")
|
|
}
|
|
// Every tool must have name, description, and inputSchema with
|
|
// `type=object` and a concrete `properties` object — `properties:
|
|
// null` trips up strict MCP clients (claude in particular).
|
|
for i, tool := range tools {
|
|
entry, ok := tool.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("tool %d not object: %#v", i, tool)
|
|
}
|
|
if entry["name"] == "" || entry["name"] == nil {
|
|
t.Fatalf("tool %d missing name: %#v", i, entry)
|
|
}
|
|
if entry["description"] == "" || entry["description"] == nil {
|
|
t.Fatalf("tool %d missing description: %#v", i, entry)
|
|
}
|
|
schema, ok := entry["inputSchema"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("tool %d inputSchema not object: %#v", i, entry)
|
|
}
|
|
if schema["type"] != "object" {
|
|
t.Fatalf("tool %d schema type != object: %#v", i, schema)
|
|
}
|
|
props, ok := schema["properties"]
|
|
if !ok {
|
|
t.Fatalf("tool %s missing properties", entry["name"])
|
|
}
|
|
if _, ok := props.(map[string]interface{}); !ok {
|
|
t.Fatalf("tool %s properties not object (got %T): %#v", entry["name"], props, props)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPingReturnsEmptyObject(t *testing.T) {
|
|
s := &Server{}
|
|
req := []byte(`{"jsonrpc":"2.0","id":3,"method":"ping"}`)
|
|
resp := s.dispatch("", req)
|
|
if resp == nil {
|
|
t.Fatal("expected response for ping")
|
|
}
|
|
var parsed struct {
|
|
Result map[string]interface{} `json:"result"`
|
|
Error *struct{ Code int } `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(resp, &parsed); err != nil {
|
|
t.Fatalf("parse: %v\n%s", err, resp)
|
|
}
|
|
if parsed.Error != nil {
|
|
t.Fatalf("ping returned error: %+v", parsed.Error)
|
|
}
|
|
if parsed.Result == nil {
|
|
t.Fatal("ping result missing")
|
|
}
|
|
}
|
|
|
|
func TestTypedInvalidArgsMapToInvalidParams(t *testing.T) {
|
|
for _, errKind := range []string{ErrorKindInvalidArgs, ErrorKindInvalidKind} {
|
|
_, code, msg, data := mapToolError(Errorf(errKind, "bad args"))
|
|
if code != codeInvalidParams {
|
|
t.Fatalf("%s code = %d, want %d", errKind, code, codeInvalidParams)
|
|
}
|
|
if msg != "bad args" {
|
|
t.Fatalf("%s message = %q", errKind, msg)
|
|
}
|
|
kind, ok := data.(map[string]string)
|
|
if !ok || kind["kind"] != errKind {
|
|
t.Fatalf("%s data = %#v", errKind, data)
|
|
}
|
|
}
|
|
}
|