Files
zzet--gortex/internal/mcp/format_negotiation_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

296 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mcp
import (
"context"
"strings"
"testing"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestDefaultFormatForClient verifies the GCX-capable client allowlist.
// The list is the contract: clients in it get gcx by default; everything
// else falls back to JSON. Drift in this list silently changes the wire
// format every shipping client receives, so the test pins it explicitly.
func TestDefaultFormatForClient(t *testing.T) {
cases := []struct {
client string
want string
}{
// GCX-capable: every client whose plugin/CLI ships a GCX1 decoder.
{"claude-code", "gcx"},
{"Claude-Code", "gcx"}, // case-insensitive
{" claude-code ", "gcx"}, // trimmed
{"cursor", "gcx"},
{"vscode", "gcx"},
{"zed", "gcx"},
{"aider", "gcx"},
{"kilocode", "gcx"},
{"opencode", "gcx"},
{"openclaw", "gcx"},
{"codex", "gcx"},
{"omp-coding-agent", "gcx"},
// Unknown / unset → JSON fallback.
{"", ""},
{"some-other-client", ""},
{"unknown", ""},
}
for _, tc := range cases {
assert.Equal(t, tc.want, defaultFormatForClient(tc.client),
"defaultFormatForClient(%q)", tc.client)
}
}
// TestResolveSessionFormat_NoSession returns "" for a bare context — no
// session means no client identity, which means no default format.
func TestResolveSessionFormat_NoSession(t *testing.T) {
srv, _ := setupTestServer(t)
assert.Equal(t, "", srv.resolveSessionFormat(context.Background()))
}
// TestResolveSessionFormat_KnownClient verifies the full per-session
// path: NoteSessionClient stores the client name, and resolveSessionFormat
// reads it back through sessionFor + defaultFormatForClient.
func TestResolveSessionFormat_KnownClient(t *testing.T) {
srv, _ := setupTestServer(t)
srv.NoteSessionClient("session_X", "claude-code", "1.0.42")
ctx := WithSessionID(context.Background(), "session_X")
assert.Equal(t, "gcx", srv.resolveSessionFormat(ctx))
}
func TestResolveSessionFormat_UnknownClient(t *testing.T) {
srv, _ := setupTestServer(t)
srv.NoteSessionClient("session_X", "some-bespoke-client", "0.1")
ctx := WithSessionID(context.Background(), "session_X")
assert.Equal(t, "", srv.resolveSessionFormat(ctx),
"unknown client must fall back to JSON (empty string)")
}
// TestNoteSessionClient_NilSafe ensures NoteSessionClient never panics
// when called on a nil *Server or with empty inputs — both are normal
// during boot races / embedded-mode tests.
func TestNoteSessionClient_NilSafe(t *testing.T) {
var srv *Server
srv.NoteSessionClient("sess", "claude-code", "1.0")
srv2, _ := setupTestServer(t)
srv2.NoteSessionClient("", "claude-code", "1.0") // empty session id → no-op
srv2.NoteSessionClient("sess", "", "1.0") // empty client → no-op
}
// TestNoteSessionClient_IsolatedPerSession verifies two sessions get
// independent client-name state. This is the core invariant that lets
// the daemon serve multiple proxies through one shared *Server.
func TestNoteSessionClient_IsolatedPerSession(t *testing.T) {
srv, _ := setupTestServer(t)
srv.NoteSessionClient("sess_A", "claude-code", "1.0")
srv.NoteSessionClient("sess_B", "some-bespoke-client", "0.1")
ctxA := WithSessionID(context.Background(), "sess_A")
ctxB := WithSessionID(context.Background(), "sess_B")
assert.Equal(t, "gcx", srv.resolveSessionFormat(ctxA))
assert.Equal(t, "", srv.resolveSessionFormat(ctxB))
}
// TestIsGCX_ExplicitFormatWins verifies that an explicit `format` arg
// overrides the per-session default in either direction.
func TestIsGCX_ExplicitFormatWins(t *testing.T) {
srv, _ := setupTestServer(t)
srv.NoteSessionClient("sess_A", "claude-code", "1.0") // session default = gcx
ctx := WithSessionID(context.Background(), "sess_A")
// Explicit "json" must override the session default.
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{"format": "json"}
assert.False(t, srv.isGCX(ctx, req),
"explicit format=json must defeat session-default gcx")
// Explicit "gcx" stays gcx.
req2 := mcp.CallToolRequest{}
req2.Params.Arguments = map[string]any{"format": "gcx"}
assert.True(t, srv.isGCX(ctx, req2))
}
// TestIsGCX_SessionDefaultApplies verifies that a session whose client
// is GCX-capable picks gcx when the request omits `format`.
func TestIsGCX_SessionDefaultApplies(t *testing.T) {
srv, _ := setupTestServer(t)
srv.NoteSessionClient("sess_A", "claude-code", "1.0")
ctx := WithSessionID(context.Background(), "sess_A")
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{} // no format
assert.True(t, srv.isGCX(ctx, req),
"claude-code session with no explicit format must default to gcx")
}
// TestIsGCX_NoSession_NoFormat returns false — the legacy default is
// JSON, and absent both an explicit format and a known client we must
// preserve that.
func TestIsGCX_NoSession_NoFormat(t *testing.T) {
srv, _ := setupTestServer(t)
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{}
assert.False(t, srv.isGCX(context.Background(), req))
}
// TestIsTOON_ExplicitFormatWins verifies that an explicit `format=toon`
// trips isTOON regardless of session default.
func TestIsTOON_ExplicitFormatWins(t *testing.T) {
srv, _ := setupTestServer(t)
srv.NoteSessionClient("sess_A", "claude-code", "1.0") // gcx by default
ctx := WithSessionID(context.Background(), "sess_A")
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{"format": "toon"}
assert.True(t, srv.isTOON(ctx, req))
assert.False(t, srv.isGCX(ctx, req),
"format=toon must not also trigger gcx")
}
// TestRespondJSONOrTOON_RoutesByFormat pins the helper that 14
// list-shaped tools share. With explicit format=toon the payload comes
// back as TOON-marshalled text; with no format and an unknown client
// it falls back to JSON. This is the single decision point most tools
// route through, so a regression here would silently flip every
// downstream consumer to the wrong format.
func TestRespondJSONOrTOON_RoutesByFormat(t *testing.T) {
srv, _ := setupTestServer(t)
payload := map[string]any{"x": 1, "y": "two"}
// format=toon → TOON text result.
reqTOON := mcp.CallToolRequest{}
reqTOON.Params.Arguments = map[string]any{"format": "toon"}
res, err := srv.respondJSONOrTOON(context.Background(), reqTOON, payload)
assert.NoError(t, err)
assert.False(t, res.IsError)
tc, ok := res.Content[0].(mcp.TextContent)
assert.True(t, ok, "expected TextContent for TOON result")
// TOON encodes scalar map values with `key: value` lines; an empty
// or JSON-shaped payload would not contain that exact prefix.
assert.Contains(t, tc.Text, "x: 1")
assert.NotContains(t, tc.Text, "{")
// no format, unknown client → JSON fallback.
reqJSON := mcp.CallToolRequest{}
reqJSON.Params.Arguments = map[string]any{}
res, err = srv.respondJSONOrTOON(context.Background(), reqJSON, payload)
assert.NoError(t, err)
tc, ok = res.Content[0].(mcp.TextContent)
assert.True(t, ok)
assert.Contains(t, tc.Text, "{") // JSON object braces
// format=json overrides session default → JSON.
srv.NoteSessionClient("sess_T", "claude-code", "1.0") // session default would be gcx, not toon
ctx := WithSessionID(context.Background(), "sess_T")
reqExplicitJSON := mcp.CallToolRequest{}
reqExplicitJSON.Params.Arguments = map[string]any{"format": "json"}
res, err = srv.respondJSONOrTOON(ctx, reqExplicitJSON, payload)
assert.NoError(t, err)
tc, ok = res.Content[0].(mcp.TextContent)
assert.True(t, ok)
assert.Contains(t, tc.Text, "{")
}
// TestRespondJSONOrTOON_MaxTokensTrimAndDecorate is the end-to-end
// pin for the new max_tokens parameter: a payload that exceeds the
// derived byte cap must come back trimmed AND carry the
// _truncated_by_tokens marker so the agent can see the tokens
// budget was the constraint that fired. Pairs with the unit tests
// in budget_test.go — this one rides the actual respondJSONOrTOON
// path that get_file_summary / find_usages / get_call_chain /
// get_editing_context / prefetch_context all share.
func TestRespondJSONOrTOON_MaxTokensTrimAndDecorate(t *testing.T) {
srv, _ := setupTestServer(t)
// Build a payload large enough that a tight max_tokens cap must
// trim. 200 rows × ~80 bytes per row ≈ 16 KB raw payload.
rows := make([]any, 200)
for i := range rows {
rows[i] = map[string]any{
"id": "row-" + strings.Repeat("x", 40),
"line": i,
"meta": strings.Repeat("padding-", 10),
}
}
payload := map[string]any{
"results": rows,
"total": 200,
}
// max_tokens=200 ≈ 700 bytes — clearly too small for the raw
// payload, so trim should fire.
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{
"format": "json",
"max_tokens": float64(200),
}
res, err := srv.respondJSONOrTOON(context.Background(), req, payload)
require.NoError(t, err)
require.False(t, res.IsError)
tc, ok := res.Content[0].(mcp.TextContent)
require.True(t, ok)
// The response must carry both the generic truncation flag AND
// the tokens-specific marker, plus the original max_tokens value
// for caller diagnostics.
assert.Contains(t, tc.Text, "_truncated_by_budget")
assert.Contains(t, tc.Text, "_truncated_by_tokens")
assert.Contains(t, tc.Text, "_max_tokens")
// Same payload with a generous max_tokens cap → no trim, no markers.
req.Params.Arguments = map[string]any{
"format": "json",
"max_tokens": float64(100000),
}
res, err = srv.respondJSONOrTOON(context.Background(), req, payload)
require.NoError(t, err)
tc, ok = res.Content[0].(mcp.TextContent)
require.True(t, ok)
assert.NotContains(t, tc.Text, "_truncated_by_budget")
assert.NotContains(t, tc.Text, "_truncated_by_tokens")
}
// TestGCXResponseWithBudget_MaxTokensTrimAndDecorate covers the GCX
// path's max_tokens behaviour: a GCX payload exceeding the
// token-derived byte cap gets row-trimmed by trimGCXBytes and then
// stamped with a `# max_tokens=N truncated_by_tokens=true` comment by
// decorateTokenBudgetGCX. The header stays intact so decoders still
// see a valid GCX1 envelope.
func TestGCXResponseWithBudget_MaxTokensTrimAndDecorate(t *testing.T) {
srv, _ := setupTestServer(t)
// Build a synthetic GCX payload that is comfortably over a
// 200-token (~700 byte) cap.
var sb strings.Builder
sb.WriteString("GCX1 tool=test fields=id,kind,name\n")
for i := 0; i < 50; i++ {
sb.WriteString("internal/foo.go::Bar")
sb.WriteString(strings.Repeat("x", 20))
sb.WriteString("\tfunction\tBar\n")
}
payload := []byte(sb.String())
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]any{
"format": "gcx",
"max_tokens": float64(200),
}
res, err := srv.gcxResponseWithBudget(req)(payload, nil)
require.NoError(t, err)
require.False(t, res.IsError)
tc, ok := res.Content[0].(mcp.TextContent)
require.True(t, ok)
assert.True(t, strings.HasPrefix(tc.Text, "GCX1 tool=test"))
// Both the byte-level and token-level markers must appear.
assert.Contains(t, tc.Text, "# truncated_by_budget=true")
assert.Contains(t, tc.Text, "max_tokens=200")
assert.Contains(t, tc.Text, "truncated_by_tokens=true")
}