Files
mcp/go/hanzomcp/hanzomcp_test.go
zeekay e9a4d37d1f feat: C FFI + Go cgo bindings for Rust MCP tools, fix TS conformance
Rust FFI (rust/src/ffi.rs):
- Expose all 13 HIP-0300 tools via C-compatible API
- 7 extern "C" functions: init, call_tool, list_tools, last_error,
  free_string, destroy, version
- Persistent multi-threaded tokio runtime behind opaque handle
- panic::catch_unwind on all FFI boundaries
- Wire 5 missing tools (code, git, fetch, workspace, tasks, hanzo)
  into ToolRegistry::execute() match arms
- Build as cdylib + staticlib (libhanzo_mcp.a / .dylib)

Go bindings (go/hanzomcp/):
- Handle type with Init/CallTool/CallToolRaw/ListTools/Close
- JSON in, JSON out across cgo boundary
- runtime.SetFinalizer safety net
- 11 tests including 20-goroutine concurrent stress test
- Race detector clean

TypeScript fixes:
- tasks: handle stale todos.json with wrong schema (todos vs items)
- code: add symbols + summarize actions for Python parity
- fetch: add fetch + head actions for Python parity

Results: Rust 66/66 tests, Go 11/11 tests, TS 22/22 conformance
2026-03-11 11:37:15 -07:00

218 lines
4.3 KiB
Go

package hanzomcp
import (
"encoding/json"
"fmt"
"testing"
)
func TestInitAndClose(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
h.Close()
}
func TestDoubleClose(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
h.Close()
h.Close() // should not panic
}
func TestVersion(t *testing.T) {
v, err := Version()
if err != nil {
t.Fatalf("Version failed: %v", err)
}
if v["name"] != "hanzo-mcp" {
t.Errorf("expected name=hanzo-mcp, got %v", v["name"])
}
}
func TestListTools(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
defer h.Close()
tools, err := h.ListTools()
if err != nil {
t.Fatalf("ListTools failed: %v", err)
}
if len(tools) < 13 {
t.Errorf("expected >= 13 tools, got %d", len(tools))
}
// Check canonical tool names
names := make(map[string]bool)
for _, tool := range tools {
names[tool.Name] = true
}
canonical := []string{"fs", "exec", "code", "git", "fetch", "workspace",
"think", "memory", "plan", "tasks", "mode", "computer"}
for _, name := range canonical {
if !names[name] {
t.Errorf("missing canonical tool: %s", name)
}
}
}
func TestCallToolThink(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
defer h.Close()
result, err := h.CallTool("think", map[string]any{
"action": "think",
"thought": "Go cgo FFI test",
})
if err != nil {
t.Fatalf("CallTool failed: %v", err)
}
if !result.Success {
t.Errorf("expected success, got error: %s", result.Error)
}
}
func TestCallToolFsHelp(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
defer h.Close()
result, err := h.CallTool("fs", map[string]any{
"action": "help",
})
if err != nil {
t.Fatalf("CallTool failed: %v", err)
}
if !result.Success {
t.Errorf("expected success, got error: %s", result.Error)
}
}
func TestCallToolExec(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
defer h.Close()
result, err := h.CallTool("exec", map[string]any{
"action": "exec",
"command": "echo hello-from-go",
})
if err != nil {
t.Fatalf("CallTool failed: %v", err)
}
if !result.Success {
t.Errorf("expected success, got error: %s", result.Error)
}
// Check output contains our string
var content map[string]any
if err := json.Unmarshal(result.Content, &content); err == nil {
if stdout, ok := content["stdout"].(string); ok {
if stdout == "" {
t.Error("expected stdout to contain output")
}
}
}
}
func TestCallToolUnknown(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
defer h.Close()
result, err := h.CallTool("nonexistent_tool", map[string]any{})
if err != nil {
t.Fatalf("CallTool should not error for unknown tool: %v", err)
}
if result.Success {
t.Error("expected failure for unknown tool")
}
}
func TestCallToolRaw(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
defer h.Close()
raw, err := h.CallToolRaw("think", []byte(`{"action":"think","thought":"raw test"}`))
if err != nil {
t.Fatalf("CallToolRaw failed: %v", err)
}
var result ToolResult
if err := json.Unmarshal(raw, &result); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if !result.Success {
t.Errorf("expected success: %s", result.Error)
}
}
func TestCallToolClosedHandle(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
h.Close()
_, err = h.CallTool("think", map[string]any{"action": "help"})
if err == nil {
t.Error("expected error for closed handle")
}
}
func TestConcurrentCalls(t *testing.T) {
h, err := Init(nil)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
defer h.Close()
const n = 20
errs := make(chan error, n)
for i := 0; i < n; i++ {
go func(i int) {
result, err := h.CallTool("think", map[string]any{
"action": "think",
"thought": "concurrent test",
})
if err != nil {
errs <- err
return
}
if !result.Success {
errs <- fmt.Errorf("call %d failed: %s", i, result.Error)
return
}
errs <- nil
}(i)
}
for i := 0; i < n; i++ {
if err := <-errs; err != nil {
t.Errorf("concurrent call failed: %v", err)
}
}
}