From 50a3b27825d208b7453e527949ce5647a649e634 Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Mon, 4 May 2026 20:46:07 +0200 Subject: [PATCH] test(mcp): pin session concurrency, document error codes, assert id round-trip Co-Authored-By: Claude Sonnet 4.6 --- internal/mcp/jsonrpc.go | 24 ++++++++++++++++++++---- internal/mcp/jsonrpc_test.go | 3 +++ internal/mcp/session_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/mcp/jsonrpc.go b/internal/mcp/jsonrpc.go index e25f191..058e1b1 100644 --- a/internal/mcp/jsonrpc.go +++ b/internal/mcp/jsonrpc.go @@ -2,12 +2,28 @@ package mcp import "encoding/json" +// JSON-RPC application-defined error codes (range -32000 to -32099 per spec). +// Tool handlers return one of these from tools/call to signal a typed failure. const ( + // CodePermissionDenied: caller authenticated but lacks permission for this + // resource (e.g. owner not in the allowlist). CodePermissionDenied = -32001 - CodeNotFound = -32002 - CodeConflict = -32003 - CodeValidation = -32004 - CodeUpstreamGitea = -32005 + + // CodeNotFound: target repo, file, branch, PR, issue, or workflow run does + // not exist. + CodeNotFound = -32002 + + // CodeConflict: write attempted on stale state (branch already exists, + // non-fast-forward push, file modified concurrently). + CodeConflict = -32003 + + // CodeValidation: arguments failed input validation (bad regex, oversized + // payload, missing required field). + CodeValidation = -32004 + + // CodeUpstreamGitea: Gitea API returned an error this server could not map + // to one of the codes above. The original status is in error.data. + CodeUpstreamGitea = -32005 ) type Request struct { diff --git a/internal/mcp/jsonrpc_test.go b/internal/mcp/jsonrpc_test.go index 6242f7c..ca0a68f 100644 --- a/internal/mcp/jsonrpc_test.go +++ b/internal/mcp/jsonrpc_test.go @@ -15,6 +15,9 @@ func TestRequestUnmarshal(t *testing.T) { require.NoError(t, json.Unmarshal(raw, &req)) assert.Equal(t, "2.0", req.JSONRPC) assert.Equal(t, "initialize", req.Method) + // ID is opaque; encoding/json decodes JSON numbers into float64 by default. + // We don't type-assert in the server — we echo it back unchanged. + assert.Equal(t, float64(1), req.ID) } func TestErrorResponseShape(t *testing.T) { diff --git a/internal/mcp/session_test.go b/internal/mcp/session_test.go index 9880e44..af86757 100644 --- a/internal/mcp/session_test.go +++ b/internal/mcp/session_test.go @@ -1,6 +1,7 @@ package mcp_test import ( + "sync" "testing" "gitea.d-ma.be/mathias/gitea-mcp/internal/mcp" @@ -20,3 +21,26 @@ func TestSessionStoreIssueAndCheck(t *testing.T) { s.Drop(id) assert.False(t, s.Valid(id)) } + +func TestSessionStoreConcurrency(t *testing.T) { + s := mcp.NewSessionStore() + + const goroutines = 32 + const perGoroutine = 100 + + var wg sync.WaitGroup + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + for j := 0; j < perGoroutine; j++ { + id := s.Issue() + if !s.Valid(id) { + t.Errorf("issued id %s reported invalid", id) + } + s.Drop(id) + } + }() + } + wg.Wait() +}