chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpserver "github.com/mark3labs/mcp-go/server"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/zzet/gortex/internal/graph"
|
||||
)
|
||||
|
||||
// TestCallToolStrict_Success returns the text content with a nil error
|
||||
// when the tool returns a normal text result.
|
||||
func TestCallToolStrict_Success(t *testing.T) {
|
||||
h := newTestHandler(t)
|
||||
text, err := h.CallToolStrict(context.Background(), "echo", map[string]any{"message": "hello"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello", text)
|
||||
}
|
||||
|
||||
// TestCallToolStrict_MissingTool returns an error when the tool name is not
|
||||
// registered, instead of silently returning an empty string.
|
||||
func TestCallToolStrict_MissingTool(t *testing.T) {
|
||||
h := newTestHandler(t)
|
||||
_, err := h.CallToolStrict(context.Background(), "no-such-tool", nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no-such-tool")
|
||||
assert.Contains(t, err.Error(), "not registered")
|
||||
}
|
||||
|
||||
// TestCallToolStrict_ToolErrorResult promotes an MCP IsError=true result to
|
||||
// a Go error. This is the contract that handleContracts depends on to surface
|
||||
// 5xx instead of pretending the call succeeded with empty content.
|
||||
func TestCallToolStrict_ToolErrorResult(t *testing.T) {
|
||||
g := graph.New()
|
||||
srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
|
||||
mcpserver.WithToolCapabilities(false),
|
||||
)
|
||||
srv.AddTool(
|
||||
mcp.NewTool("erroring_tool", mcp.WithDescription("returns IsError=true")),
|
||||
func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return mcp.NewToolResultError("project not found: \"gortex\""), nil
|
||||
},
|
||||
)
|
||||
h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())
|
||||
|
||||
text, err := h.CallToolStrict(context.Background(), "erroring_tool", nil)
|
||||
require.Error(t, err, "IsError=true must be promoted to a Go error")
|
||||
assert.Contains(t, err.Error(), "project not found")
|
||||
// The text content is preserved so callers can put it in the response body.
|
||||
assert.Contains(t, text, "project not found")
|
||||
}
|
||||
|
||||
// TestCallToolStrict_HandlerError surfaces a Go-level handler error.
|
||||
func TestCallToolStrict_HandlerError(t *testing.T) {
|
||||
g := graph.New()
|
||||
srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
|
||||
mcpserver.WithToolCapabilities(false),
|
||||
)
|
||||
srv.AddTool(
|
||||
mcp.NewTool("failing_tool", mcp.WithDescription("returns Go-level error")),
|
||||
func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return nil, errors.New("upstream blew up")
|
||||
},
|
||||
)
|
||||
h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())
|
||||
|
||||
_, err := h.CallToolStrict(context.Background(), "failing_tool", nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "upstream blew up")
|
||||
}
|
||||
|
||||
// TestCallTool_BackwardsCompatible — the legacy best-effort wrapper still
|
||||
// returns "" on error and never panics. Eval/http_handler.go and similar
|
||||
// callers depend on this contract.
|
||||
func TestCallTool_BackwardsCompatible(t *testing.T) {
|
||||
g := graph.New()
|
||||
srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
|
||||
mcpserver.WithToolCapabilities(false),
|
||||
)
|
||||
srv.AddTool(
|
||||
mcp.NewTool("erroring_tool", mcp.WithDescription("")),
|
||||
func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return mcp.NewToolResultError("nope"), nil
|
||||
},
|
||||
)
|
||||
h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())
|
||||
|
||||
// IsError=true: legacy CallTool returns the text (best-effort) and
|
||||
// callers cannot tell it was an error. This is the contract callers
|
||||
// like eval/http_handler.go rely on; the ones that need to know now
|
||||
// use CallToolStrict.
|
||||
got := h.CallTool(context.Background(), "erroring_tool", nil)
|
||||
assert.Equal(t, "nope", got)
|
||||
|
||||
// Missing tool: still returns "".
|
||||
got = h.CallTool(context.Background(), "no-such-tool", nil)
|
||||
assert.Empty(t, got)
|
||||
}
|
||||
|
||||
// TestHandleContracts_ToolError_500 verifies the user-visible bug fix: when
|
||||
// the underlying contracts tool returns an error result (e.g. project not
|
||||
// found), /v1/contracts must return HTTP 500 with the error in the body, not
|
||||
// a fake empty 200.
|
||||
func TestHandleContracts_ToolError_500(t *testing.T) {
|
||||
g := graph.New()
|
||||
srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
|
||||
mcpserver.WithToolCapabilities(false),
|
||||
)
|
||||
srv.AddTool(
|
||||
mcp.NewTool("contracts", mcp.WithDescription("contracts stub")),
|
||||
func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return mcp.NewToolResultError(`project not found: "gortex" (available: )`), nil
|
||||
},
|
||||
)
|
||||
h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/contracts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rec.Code,
|
||||
"tool error must surface as 5xx, not 200 with empty body")
|
||||
|
||||
var body map[string]any
|
||||
require.NoError(t, json.NewDecoder(rec.Body).Decode(&body))
|
||||
msg, _ := body["message"].(string)
|
||||
assert.True(t, strings.Contains(msg, "project not found"),
|
||||
"error body must include the underlying tool message; got: %q", msg)
|
||||
}
|
||||
|
||||
// TestHandleContracts_Success_200 verifies the happy path still works:
|
||||
// contracts tool returns a valid JSON payload → handler 200s with the
|
||||
// flattened list.
|
||||
func TestHandleContracts_Success_200(t *testing.T) {
|
||||
g := graph.New()
|
||||
srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
|
||||
mcpserver.WithToolCapabilities(false),
|
||||
)
|
||||
srv.AddTool(
|
||||
mcp.NewTool("contracts", mcp.WithDescription("contracts stub")),
|
||||
func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
payload := `{"by_repo":{"alpha":{"contracts":{"http":[{"id":"GET /foo","type":"http","role":"provider","symbol_id":"alpha/x.go::H","file_path":"alpha/x.go","line":10,"repo_prefix":"alpha"}]},"total":1}}}`
|
||||
return mcp.NewToolResultText(payload), nil
|
||||
},
|
||||
)
|
||||
h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/contracts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var body struct {
|
||||
Contracts []map[string]any `json:"contracts"`
|
||||
}
|
||||
require.NoError(t, json.NewDecoder(rec.Body).Decode(&body))
|
||||
require.Len(t, body.Contracts, 1)
|
||||
assert.Equal(t, "GET /foo", body.Contracts[0]["id"])
|
||||
}
|
||||
Reference in New Issue
Block a user