Files
wehub-resource-sync a06f331eb8
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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

203 lines
6.2 KiB
Go

package mcp
import (
"context"
"encoding/json"
"sync"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// recordedNotification captures a single SendNotificationToAllClients call.
type recordedNotification struct {
method string
params map[string]any
}
// recordingNotifier implements resourcesUpdatedNotifier for tests.
type recordingNotifier struct {
mu sync.Mutex
calls []recordedNotification
}
func (r *recordingNotifier) SendNotificationToAllClients(method string, params map[string]any) {
r.mu.Lock()
defer r.mu.Unlock()
r.calls = append(r.calls, recordedNotification{method: method, params: params})
}
func (r *recordingNotifier) snapshot() []recordedNotification {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]recordedNotification, len(r.calls))
copy(out, r.calls)
return out
}
func readResource(t *testing.T, handler func(context.Context, mcplib.ReadResourceRequest) ([]mcplib.ResourceContents, error), uri string) map[string]any {
t.Helper()
req := mcplib.ReadResourceRequest{}
req.Params.URI = uri
contents, err := handler(context.Background(), req)
require.NoError(t, err)
require.Len(t, contents, 1)
tr, ok := contents[0].(mcplib.TextResourceContents)
require.True(t, ok, "expected TextResourceContents")
require.Equal(t, "application/json", tr.MIMEType)
require.Equal(t, uri, tr.URI)
var payload map[string]any
require.NoError(t, json.Unmarshal([]byte(tr.Text), &payload))
return payload
}
func TestResourceStats_MatchesGraphStatsTool(t *testing.T) {
srv, _ := setupTestServer(t)
resourcePayload := readResource(t, srv.handleResourceStats, "gortex://stats")
// The resource handler must call buildGraphStatsPayload — same
// keys the tool surfaces. Spot-check the stable keys.
for _, key := range []string{"total_nodes", "total_edges", "by_kind", "by_language", "token_savings"} {
assert.Contains(t, resourcePayload, key, "stats resource missing %q", key)
}
assert.Greater(t, int(resourcePayload["total_nodes"].(float64)), 0)
}
func TestResourceIndexHealth_HasExpectedShape(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceIndexHealth, "gortex://index-health")
for _, key := range []string{"health_score", "node_count", "edge_count", "language_coverage"} {
assert.Contains(t, payload, key, "index-health resource missing %q", key)
}
}
func TestResourceWorkspace_DegradesUnbound(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceWorkspace, "gortex://workspace")
// setupTestServer doesn't bind a workspace — expect the unbound
// degenerate shape.
assert.Equal(t, "unbound", payload["mode"])
assert.NotNil(t, payload["repos"])
}
func TestResourceRepos_DegradesUnbound(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceRepos, "gortex://repos")
assert.Equal(t, "unbound", payload["mode"])
repos, ok := payload["repos"].([]any)
require.True(t, ok)
assert.Empty(t, repos)
}
func TestResourceActiveProject_NoConfigManager(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceActiveProject, "gortex://active-project")
assert.Equal(t, "", payload["project"])
}
func TestResourceReport_HasOrientationKeys(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceReport, "gortex://report")
for _, key := range []string{"total_nodes", "total_edges", "top_languages", "top_kinds", "communities", "processes", "hotspots", "dead_code", "open_todos"} {
assert.Contains(t, payload, key, "report resource missing %q", key)
}
}
func TestResourceGodNodes_TruncatesAt20(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceGodNodes, "gortex://god-nodes")
// Either we get a list, or the small-codebase guard message.
if msg, ok := payload["message"]; ok {
assert.Contains(t, msg, "too small")
return
}
gn, ok := payload["god_nodes"].([]any)
require.True(t, ok)
assert.LessOrEqual(t, len(gn), 20)
}
func TestResourceSurprises_HasExpectedKeys(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceSurprises, "gortex://surprises")
for _, key := range []string{"cycles", "dead_code", "cross_community_hubs"} {
assert.Contains(t, payload, key, "surprises resource missing %q", key)
}
}
func TestResourceAudit_HandlesEmptyConfigDir(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceAudit, "gortex://audit")
// setupTestServer's tmp dir has no CLAUDE.md / AGENTS.md /
// Cursor rules — expect the "no agent config files" message.
if v, ok := payload["files_scanned"]; ok {
assert.EqualValues(t, 0, v)
}
}
func TestResourceQuestions_HasAggregateKeys(t *testing.T) {
srv, _ := setupTestServer(t)
payload := readResource(t, srv.handleResourceQuestions, "gortex://questions")
for _, key := range []string{"questions", "total", "by_tag", "with_assignee"} {
assert.Contains(t, payload, key, "questions resource missing %q", key)
}
}
func TestRunAnalysis_PushesBootstrapResourceUpdates(t *testing.T) {
srv, _ := setupTestServer(t)
rec := &recordingNotifier{}
srv.resourcesNotifier = rec
srv.RunAnalysis()
calls := rec.snapshot()
require.Equal(t, len(bootstrapResourceURIs()), len(calls), "one notification per bootstrap URI")
// Every call must be the right method with a `uri` param matching
// one of the bootstrap URIs.
want := make(map[string]bool, len(bootstrapResourceURIs()))
for _, uri := range bootstrapResourceURIs() {
want[uri] = false
}
for _, c := range calls {
assert.Equal(t, "notifications/resources/updated", c.method)
uri, ok := c.params["uri"].(string)
require.True(t, ok, "uri param missing or wrong type")
_, expected := want[uri]
assert.True(t, expected, "unexpected uri %q", uri)
want[uri] = true
}
for uri, fired := range want {
assert.True(t, fired, "bootstrap uri %q never fired", uri)
}
}
func TestNotifyBootstrapResourcesUpdated_NoOpWithoutNotifier(t *testing.T) {
srv, _ := setupTestServer(t)
// Replace the live mcpServer reference with a notifier-less
// stand-in by clearing it. The notifier path must not panic.
srv.mcpServer = nil
srv.notifyBootstrapResourcesUpdated()
}