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

259 lines
10 KiB
Go

package mcp
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/indexer"
"github.com/zzet/gortex/internal/query"
)
func TestToolPolicy_Presets(t *testing.T) {
cases := []struct {
preset string
tool string
want bool
}{
// full → no restriction.
{"full", "analyze", true},
{"full", "edit_file", true},
{"", "analyze", true},
// agent → the lean coding-agent floor only.
{"agent", "search_symbols", true},
{"agent", "edit_file", true},
{"agent", "verify_change", true},
{"agent", "analyze", false},
{"agent", "store_memory", false}, // memory trio deferred (tail cut)
{"agent", "get_architecture", false},
{"coding-agent", "smart_context", true}, // alias
// core (the shipped default) → curated dev-cycle surface only.
{"core", "smart_context", true},
{"core", "edit_file", true},
{"core", "analyze", true},
{"core", "store_memory", true},
{"core", "review", true},
{"core", "get_architecture", false},
{"core", "taint_paths", false},
{"core", "overlay_push", false},
// "default" / "classic" are aliases of core.
{"default", "smart_context", true},
{"default", "get_architecture", false},
{"classic", "edit_file", true},
// edit → only the headless editing surface.
{"edit", "edit_file", true},
{"edit", "find_files", true},
{"edit", "search_symbols", true},
{"edit", "analyze", false},
{"edit", "get_communities", false},
// nav → read-only navigation; no editors.
{"nav", "search_symbols", true},
{"nav", "find_files", true},
{"nav", "edit_file", false},
{"nav", "write_file", false},
// readonly → everything except mutating tools.
{"readonly", "search_symbols", true},
{"readonly", "analyze", true},
{"readonly", "edit_file", false},
{"readonly", "write_file", false},
{"readonly", "index_repository", false},
}
for _, c := range cases {
p := newToolPolicy(ToolPolicyConfig{Preset: c.preset}, zap.NewNop())
require.Equalf(t, c.want, p.allows(c.tool),
"preset=%q tool=%q", c.preset, c.tool)
}
}
func TestToolPolicy_AlwaysKeptAndDeltas(t *testing.T) {
// tool_profile + tools_search survive any preset.
edit := newToolPolicy(ToolPolicyConfig{Preset: "edit"}, zap.NewNop())
require.True(t, edit.allows("tool_profile"))
require.True(t, edit.allows(LazyToolsSearchName))
// An explicit deny overrides the preset and the always-kept set.
denied := newToolPolicy(ToolPolicyConfig{Preset: "edit", Deny: []string{"edit_file", "tool_profile"}}, zap.NewNop())
require.False(t, denied.allows("edit_file"))
require.False(t, denied.allows("tool_profile"))
// An explicit allow adds a tool on top of the preset.
plus := newToolPolicy(ToolPolicyConfig{Preset: "edit", Allow: []string{"analyze"}}, zap.NewNop())
require.True(t, plus.allows("analyze"))
// Deny-only against the full surface is active and subtractive.
deny := newToolPolicy(ToolPolicyConfig{Deny: []string{"write_file"}}, zap.NewNop())
require.True(t, deny.isActive())
require.False(t, deny.allows("write_file"))
require.True(t, deny.allows("search_symbols"))
}
func TestToolPolicy_ModesAndUnknown(t *testing.T) {
require.True(t, newToolPolicy(ToolPolicyConfig{Preset: "edit"}, zap.NewNop()).hideMode())
require.False(t, newToolPolicy(ToolPolicyConfig{Preset: "edit"}, zap.NewNop()).deferMode())
require.True(t, newToolPolicy(ToolPolicyConfig{Preset: "edit", Mode: "defer"}, zap.NewNop()).deferMode())
// full is inactive regardless of mode.
require.False(t, newToolPolicy(ToolPolicyConfig{Preset: "full", Mode: "defer"}, zap.NewNop()).deferMode())
// core aliases normalise to the canonical "core" label.
require.Equal(t, "core", newToolPolicy(ToolPolicyConfig{Preset: "default"}, zap.NewNop()).preset)
require.Equal(t, "core", newToolPolicy(ToolPolicyConfig{Preset: "classic"}, zap.NewNop()).preset)
// Unknown preset fails open (full surface), never strands the agent.
unknown := newToolPolicy(ToolPolicyConfig{Preset: "bogus"}, zap.NewNop())
require.False(t, unknown.isActive())
require.True(t, unknown.allows("anything"))
}
func TestParseToolSpec(t *testing.T) {
// Preset + deltas.
preset, allow, deny := ParseToolSpec("edit,+find_files,-write_file")
require.Equal(t, "edit", preset)
require.Equal(t, []string{"find_files"}, allow)
require.Equal(t, []string{"write_file"}, deny)
// A known preset followed by a bare tool name → preset + allow delta.
preset, allow, _ = ParseToolSpec("edit,find_files")
require.Equal(t, "edit", preset)
require.Equal(t, []string{"find_files"}, allow)
// No known preset → every bare token is an explicit tool name.
preset, allow, _ = ParseToolSpec("search_symbols,edit_file,find_files")
require.Equal(t, "", preset)
require.Equal(t, []string{"search_symbols", "edit_file", "find_files"}, allow)
}
func TestToolPolicy_ExplicitList(t *testing.T) {
// An explicit comma list (no preset) is exactly that surface.
p := newToolPolicy(ToolPolicyConfig{Allow: []string{"search_symbols", "edit_file"}}, zap.NewNop())
require.True(t, p.isActive())
require.Equal(t, "custom", p.preset)
require.True(t, p.allows("search_symbols"))
require.True(t, p.allows("edit_file"))
require.True(t, p.allows("tool_profile")) // always kept
require.False(t, p.allows("analyze"))
require.False(t, p.allows("write_file"))
// End-to-end through the spec parser + env resolution.
cfg := func(spec string) ToolPolicyConfig {
pr, al, dn := ParseToolSpec(spec)
return ToolPolicyConfig{Preset: pr, Allow: al, Deny: dn}
}
surface := NewToolSurface(cfg("find_files,get_symbol_source"), zap.NewNop())
require.True(t, surface.Active())
require.True(t, surface.Allows("find_files"))
require.True(t, surface.Allows("get_symbol_source"))
require.False(t, surface.Allows("edit_file"))
}
func TestToolPolicy_EnvOverride(t *testing.T) {
t.Setenv(toolPresetEnv, "nav")
t.Setenv(toolPresetModeEnv, "defer")
// Base config says edit/hide; env must win.
p := resolveToolPolicy(ToolPolicyConfig{Preset: "edit"}, zap.NewNop())
require.Equal(t, "nav", p.preset)
require.True(t, p.deferMode())
require.True(t, p.allows("search_symbols"))
require.False(t, p.allows("edit_file"))
}
// setupPresetServer builds a server over a one-file repo with the given
// tool policy so the registration + filtering paths can be exercised.
func setupPresetServer(t *testing.T, cfg ToolPolicyConfig) *Server {
t.Helper()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"),
[]byte("package app\n\nfunc Main() {}\n"), 0o644))
g := graph.New()
reg := testRegistry()
conf := config.Default()
idx := indexer.New(g, reg, conf.Index, zap.NewNop())
_, err := idx.Index(dir)
require.NoError(t, err)
eng := query.NewEngine(g)
return NewServer(eng, g, idx, nil, zap.NewNop(), nil, MultiRepoOptions{ToolPolicy: &cfg})
}
func TestToolPolicy_HideModeServer(t *testing.T) {
srv := setupPresetServer(t, ToolPolicyConfig{Preset: "edit"})
// The edit surface is visible; everything else is filtered out.
require.True(t, srv.IsToolEnabled("edit_file"))
require.True(t, srv.IsToolEnabled("find_files"))
require.True(t, srv.IsToolEnabled("tool_profile"))
require.False(t, srv.IsToolEnabled("analyze"))
require.Equal(t, "blocked", srv.toolStatus("analyze"))
live := srv.liveToolNames()
require.Contains(t, live, "edit_file")
require.NotContains(t, live, "analyze")
// Calls to filtered tools are hard-blocked; allowed tools pass.
require.NotNil(t, srv.checkToolPresetGate(context.Background(), "analyze"))
require.Nil(t, srv.checkToolPresetGate(context.Background(), "edit_file"))
require.Nil(t, srv.checkToolPresetGate(context.Background(), "tool_profile"))
}
func TestToolPolicy_DeferModeServer(t *testing.T) {
srv := setupPresetServer(t, ToolPolicyConfig{Preset: "edit", Mode: "defer"})
// Defer mode parks non-allowed tools behind tools_search instead of
// removing them: allowed tools are live, the rest deferred.
require.True(t, srv.lazy.Enabled())
deferred := srv.lazy.DeferredNames()
require.Contains(t, deferred, "analyze")
require.NotContains(t, deferred, "edit_file")
// A deferred tool is still "enabled" (reachable via tools_search),
// never call-gated in defer mode.
require.True(t, srv.IsToolEnabled("analyze"))
require.Equal(t, "deferred", srv.toolStatus("analyze"))
require.Nil(t, srv.checkToolPresetGate(context.Background(), "analyze"))
}
// TestToolPolicy_CoreDefaultServer exercises the shipped default surface
// (config.Default(): core preset in defer mode) end to end: every core
// tool ships eagerly, specialised tools are deferred behind tools_search
// (never call-gated), and the cold surface stays far below the full
// catalogue.
func TestToolPolicy_CoreDefaultServer(t *testing.T) {
srv := setupPresetServer(t, ToolPolicyConfig{Preset: "core", Mode: "defer"})
require.True(t, srv.lazy.Enabled(), "core/defer must enable the lazy registry")
require.True(t, srv.toolPolicy.deferMode())
require.Equal(t, "core", srv.toolPolicy.preset)
// Every curated core tool is eagerly live in the cold tools/list.
for _, name := range corePresetTools {
require.Equalf(t, "live", srv.toolStatus(name),
"core tool %q must be eagerly live", name)
}
// Introspection + discovery survive every preset.
require.Equal(t, "live", srv.toolStatus("tool_profile"))
require.Equal(t, "live", srv.toolStatus(LazyToolsSearchName))
// Specialised tools are deferred (reachable on demand), not blocked.
for _, name := range []string{"get_architecture", "get_communities", "taint_paths", "flow_between"} {
require.Equalf(t, "deferred", srv.toolStatus(name),
"non-core tool %q must be deferred under core/defer", name)
require.Nilf(t, srv.checkToolPresetGate(context.Background(), name),
"defer mode must never call-gate %q", name)
require.Truef(t, srv.IsToolEnabled(name),
"deferred tool %q is still reachable via tools_search", name)
}
// The eager surface stays lean — a fraction of the full ~180 catalogue.
live := srv.liveToolNames()
t.Logf("core/defer cold surface = %d live tools", len(live))
require.GreaterOrEqual(t, len(live), len(corePresetTools),
"all core tools must be live")
require.Lessf(t, len(live), 60,
"core/defer cold surface should be lean, got %d", len(live))
}