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
138 lines
4.8 KiB
Go
138 lines
4.8 KiB
Go
package mcp
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/daemon"
|
|
)
|
|
|
|
// TestToolDescriptors_CoversEveryToolOnce asserts the catalog enumerates
|
|
// every registered tool — live and deferred — exactly once, with no
|
|
// duplicates, and that the total matches the raw live+deferred surface.
|
|
func TestToolDescriptors_CoversEveryToolOnce(t *testing.T) {
|
|
srv := newFullTestServer(t)
|
|
|
|
descs := srv.ToolDescriptors()
|
|
|
|
// Expected universe: the raw live MCP surface plus any deferred names.
|
|
expected := make(map[string]bool)
|
|
for name := range srv.mcpServer.ListTools() {
|
|
expected[name] = true
|
|
}
|
|
if srv.lazy != nil {
|
|
for _, name := range srv.lazy.DeferredNames() {
|
|
expected[name] = true
|
|
}
|
|
}
|
|
|
|
if len(descs) != len(expected) {
|
|
t.Errorf("ToolDescriptors returned %d descriptors, want %d (live+deferred)", len(descs), len(expected))
|
|
}
|
|
|
|
seen := make(map[string]int, len(descs))
|
|
for _, d := range descs {
|
|
seen[d.Name]++
|
|
}
|
|
for name, n := range seen {
|
|
if n != 1 {
|
|
t.Errorf("tool %q appears %d times in descriptors (want exactly 1)", name, n)
|
|
}
|
|
}
|
|
for name := range expected {
|
|
if _, ok := seen[name]; !ok {
|
|
t.Errorf("registered tool %q is missing from descriptors", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestToolDescriptors_NonEmptyCategory asserts every descriptor carries a
|
|
// category — toolCategory always classifies (falling back to "other"),
|
|
// so a blank category means the join broke.
|
|
func TestToolDescriptors_NonEmptyCategory(t *testing.T) {
|
|
srv := newFullTestServer(t)
|
|
for _, d := range srv.ToolDescriptors() {
|
|
if d.Category == "" {
|
|
t.Errorf("tool %q has an empty category", d.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestToolDescriptors_MutatingMatchesDaemon asserts each descriptor's
|
|
// Mutating flag agrees with the authoritative daemon.MutatingTools set.
|
|
func TestToolDescriptors_MutatingMatchesDaemon(t *testing.T) {
|
|
srv := newFullTestServer(t)
|
|
for _, d := range srv.ToolDescriptors() {
|
|
if want := daemon.MutatingTools[d.Name]; d.Mutating != want {
|
|
t.Errorf("tool %q Mutating = %v, want %v (daemon.MutatingTools)", d.Name, d.Mutating, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestToolDescriptors_DescriptionNonEmptyAndTrimmed asserts every descriptor
|
|
// with a non-blank raw MCP description carries a matching, whitespace-trimmed
|
|
// Description, and that Summary is never longer than Description (Summary is
|
|
// derived from Description via firstSentence, so it can only truncate it).
|
|
func TestToolDescriptors_DescriptionNonEmptyAndTrimmed(t *testing.T) {
|
|
srv := newFullTestServer(t)
|
|
for _, d := range srv.ToolDescriptors() {
|
|
if d.Description != strings.TrimSpace(d.Description) {
|
|
t.Errorf("tool %q Description is not trimmed: %q", d.Name, d.Description)
|
|
}
|
|
if d.Summary != "" && len(d.Summary) > len(d.Description) {
|
|
t.Errorf("tool %q Summary (%q) is longer than Description (%q)", d.Name, d.Summary, d.Description)
|
|
}
|
|
if d.Description == "" && d.Summary != "" {
|
|
t.Errorf("tool %q has a Summary but an empty Description", d.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFirstSentence_DivergesFromDescriptionOnMultiSentenceInput asserts the
|
|
// Summary/Description split ToolDescriptors relies on: firstSentence
|
|
// truncates at the first ". " boundary, but a period immediately followed by
|
|
// a newline (no space) does not count as a boundary, so the whole
|
|
// (multi-line) description passes through unchanged.
|
|
func TestFirstSentence_DivergesFromDescriptionOnMultiSentenceInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
desc string
|
|
want string
|
|
}{
|
|
{"empty", "", ""},
|
|
{"whitespace only", " \n\t ", ""},
|
|
{"single sentence", "Show who calls a function", "Show who calls a function"},
|
|
{"multi-sentence period-space", "Show who calls a function. Returns the caller subgraph.", "Show who calls a function."},
|
|
{"period-newline is not a boundary", "Show who calls a function.\nReturns the caller subgraph.", "Show who calls a function.\nReturns the caller subgraph."},
|
|
{"leading/trailing whitespace trimmed", " Show who calls a function. ", "Show who calls a function."},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := firstSentence(tt.desc); got != tt.want {
|
|
t.Errorf("firstSentence(%q) = %q, want %q", tt.desc, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestToolDescriptors_CorePresetMembership asserts every tool in the core
|
|
// preset roster carries "core" in its Presets — guarding the
|
|
// presetsContaining join against drift in the preset rosters.
|
|
func TestToolDescriptors_CorePresetMembership(t *testing.T) {
|
|
srv := newFullTestServer(t)
|
|
presets := make(map[string][]string)
|
|
for _, d := range srv.ToolDescriptors() {
|
|
presets[d.Name] = d.Presets
|
|
}
|
|
for _, name := range corePresetTools {
|
|
got, ok := presets[name]
|
|
if !ok {
|
|
t.Errorf("core preset tool %q not present in descriptors", name)
|
|
continue
|
|
}
|
|
if !containsString(got, "core") {
|
|
t.Errorf("core preset tool %q does not list \"core\" in its presets: %v", name, got)
|
|
}
|
|
}
|
|
}
|