Files
zzet--gortex/internal/mcp/proxy_preset_forward_test.go
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

95 lines
4.4 KiB
Go

package mcp
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
// listToolNamesForSession drives a real tools/list through
// MCPServer.HandleMessage for the given session ID — the same path the
// daemon's per-session dispatcher uses — and returns the visible set.
func listToolNamesForSession(t *testing.T, srv *Server, sessionID string) map[string]bool {
t.Helper()
ctx := WithSessionID(context.Background(), sessionID)
listFrame := []byte(`{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}`)
reply := srv.MCPServer().HandleMessage(ctx, listFrame)
require.NotNil(t, reply)
out, _ := json.Marshal(reply)
var parsed struct {
Result struct {
Tools []struct {
Name string `json:"name"`
} `json:"tools"`
} `json:"result"`
}
require.NoError(t, json.Unmarshal(out, &parsed))
names := make(map[string]bool, len(parsed.Result.Tools))
for _, e := range parsed.Result.Tools {
names[e.Name] = true
}
return names
}
// TestSessionPresetForward_ProxyPath is the W1 regression: a tool preset
// forwarded by a `gortex mcp` proxy (GORTEX_TOOLS / --tools relayed through
// the daemon handshake and recorded via NoteSessionToolPolicy) is honoured
// authoritatively at tools/list — the daemon serves the shared graph, but
// each session sees exactly the surface it asked for, both narrower AND
// wider than the server's global `core`/defer default. Before this, a
// client preset that widened the surface was silently a no-op because the
// proxy's byte-pump filter can only subtract from the daemon's list.
func TestSessionPresetForward_ProxyPath(t *testing.T) {
srv := setupPresetServer(t, ToolPolicyConfig{Preset: "core", Mode: "defer"})
// Baseline: a session that forwards nothing gets the server's global
// core default — a specialised tool stays deferred (absent).
base := listToolNamesForSession(t, srv, "sess_default")
require.True(t, base["smart_context"], "core tool must be visible by default")
require.False(t, base["find_overrides"], "non-core find_overrides is deferred by default")
require.False(t, base["get_architecture"], "non-core get_architecture is deferred by default")
// A session forwards `nav`: the surface NARROWS to read-only navigation
// (edit tools drop) AND WIDENS to include nav-preset tools the daemon
// held deferred under core (find_overrides). This is the case the proxy
// filter alone could never satisfy.
srv.NoteSessionToolPolicy("sess_nav", "nav", "")
nav := listToolNamesForSession(t, srv, "sess_nav")
require.True(t, nav["search_symbols"], "nav keeps navigation tools")
require.True(t, nav["find_overrides"],
"forwarded nav must WIDEN the surface to a tool the daemon deferred under core")
require.False(t, nav["edit_file"], "forwarded nav must NARROW out editing tools")
// A session forwards `full`: the whole catalogue is materialised for it,
// including tools deferred under the server's core default.
srv.NoteSessionToolPolicy("sess_full", "full", "")
full := listToolNamesForSession(t, srv, "sess_full")
require.True(t, full["get_architecture"],
"forwarded full must widen to the deferred catalogue")
require.True(t, full["edit_file"], "full includes editing tools")
require.Greater(t, len(full), len(base),
"full surface must be strictly larger than the core default")
// Sessions are isolated: the default session's surface is unchanged by
// another session forwarding a wider preset.
baseAfter := listToolNamesForSession(t, srv, "sess_default")
require.False(t, baseAfter["get_architecture"],
"one session forwarding full must not leak into another session's surface")
}
// TestSessionPresetForward_ExplicitList checks an explicit allow-list spec
// (no preset — exactly these tools) forwarded by a client scopes the
// session down to precisely that set plus the always-kept discovery tools.
func TestSessionPresetForward_ExplicitList(t *testing.T) {
srv := setupPresetServer(t, ToolPolicyConfig{Preset: "core", Mode: "defer"})
srv.NoteSessionToolPolicy("sess_x", "search_symbols,edit_file", "")
names := listToolNamesForSession(t, srv, "sess_x")
require.True(t, names["search_symbols"])
require.True(t, names["edit_file"])
require.True(t, names[LazyToolsSearchName], "discovery tool is always kept")
require.False(t, names["smart_context"], "a tool outside the explicit list is hidden")
require.False(t, names["analyze"], "a tool outside the explicit list is hidden")
}