项目文件夹

文件
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 行
11 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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")
}