package main import ( "bufio" "encoding/json" "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) // TestIntegrationBinarySubprocess builds the binary (or uses an existing // bin/devicemesh-mcp) and exercises a full initialize -> tools/list -> // tools/call sequence over a real OS pipe. This validates that the same // code path that claude will invoke (subprocess + stdio) works end-to-end. // // Skipped when the binary cannot be built or located, so the rest of the // unit tests still run cleanly on minimal sandboxes. func TestIntegrationBinarySubprocess(t *testing.T) { if testing.Short() { t.Skip("integration test skipped in -short mode") } binPath := buildOrLocateBinary(t) if binPath == "" { t.Skip("cannot build/locate devicemesh-mcp binary") } mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body := map[string]any{} _ = json.NewDecoder(r.Body).Decode(&body) _ = json.NewEncoder(w).Encode(map[string]any{ "request_id": body["request_id"], "ok": true, "duration_ms": 7, "result": map[string]any{ "stdout": "subprocess hi", "stderr": "", "exit_code": 0, }, }) })) defer mock.Close() cmd := exec.Command(binPath, "--device-agent", mock.URL, "--mode", "user", "--server-name", "devicemesh", ) stdin, err := cmd.StdinPipe() if err != nil { t.Fatalf("stdin pipe: %v", err) } stdout, err := cmd.StdoutPipe() if err != nil { t.Fatalf("stdout pipe: %v", err) } cmd.Stderr = io.Discard if err := cmd.Start(); err != nil { t.Fatalf("start: %v", err) } defer func() { _ = stdin.Close() _ = cmd.Process.Kill() _ = cmd.Wait() }() // Real MCP clients send `notifications/initialized` after the // initialize response is received before sending any other requests. // We mirror the same sequence — without it the server may queue // follow-up frames behind the not-yet-initialized session. frames := []string{ initFrame(1), notifInitializedFrame(), toolsListFrame(2), toolsCallFrame(3, "exec", map[string]any{"argv": []any{"echo", "subprocess"}}), } for _, f := range frames { if !strings.HasSuffix(f, "\n") { f += "\n" } if _, err := stdin.Write([]byte(f)); err != nil { t.Fatalf("write frame: %v", err) } } // Read responses (up to 3 with timeout). reader := bufio.NewReader(stdout) deadline := time.After(5 * time.Second) responses := make([]map[string]any, 0, 3) readCh := make(chan map[string]any, 4) go func() { defer close(readCh) dec := json.NewDecoder(reader) for { var msg map[string]any if err := dec.Decode(&msg); err != nil { return } readCh <- msg } }() readLoop: for { select { case msg, ok := <-readCh: if !ok { break readLoop } responses = append(responses, msg) if len(responses) >= 3 { break readLoop } case <-deadline: break readLoop } } if len(responses) < 3 { t.Fatalf("expected 3 responses, got %d: %v", len(responses), responses) } // Validate the tools/call (id=3) response. r := responses[2] if r["id"] != float64(3) { t.Errorf("expected id=3, got %v", r["id"]) } result, _ := r["result"].(map[string]any) contents, _ := result["content"].([]any) if len(contents) == 0 { t.Fatalf("missing content in tools/call response: %v", r) } first, _ := contents[0].(map[string]any) text, _ := first["text"].(string) if !strings.Contains(text, "subprocess hi") { t.Errorf("expected text to contain 'subprocess hi', got %q", text) } } // buildOrLocateBinary returns the absolute path to bin/devicemesh-mcp, // building it under a temp dir if it is missing. Returns "" if neither // option works (the test then skips). func buildOrLocateBinary(t *testing.T) string { t.Helper() // First, try ../../bin/devicemesh-mcp relative to this file (CWD when // `go test ./cmd/devicemesh-mcp/` is the cmd dir itself). candidates := []string{ filepath.Join("..", "..", "bin", "devicemesh-mcp"), filepath.Join("bin", "devicemesh-mcp"), } for _, c := range candidates { if abs, err := filepath.Abs(c); err == nil { if st, err := os.Stat(abs); err == nil && !st.IsDir() { return abs } } } // Build into a tmpdir. tmpDir := t.TempDir() out := filepath.Join(tmpDir, "devicemesh-mcp") cmd := exec.Command("/usr/local/go/bin/go", "build", "-tags", "goolm", "-o", out, ".") cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { t.Logf("build failed: %v", err) return "" } return out }