Skip to content

Commit dc07359

Browse files
authored
[test-improver] Improve tests for registerPromptsFromBackend (#8307)
## Summary Adds `internal/server/register_prompts_test.go` with four new tests targeting `registerPromptsFromBackend` in `internal/server/tool_registry.go`. ### File analyzed **`internal/server/tool_registry.go`** — `registerPromptsFromBackend` (lines 382–460) Before this PR, the function had **9.4% coverage** because the only covered path was the early-return guard (`!conn.BackendHasPromptsCapability()`). All other paths required a streamable HTTP backend that declares prompts capability, which existing tests did not set up. ### Improvements made New file: `internal/server/register_prompts_test.go` | Test | Path covered | |------|-------------| | `TestRegisterPromptsFromBackend_NoCapability` | Early-return when backend has no prompts capability (plain JSON-RPC connection) | | `TestRegisterPromptsFromBackend_RequestError` | HTTP 500 from backend → `handleRequestError` callback → graceful nil return | | `TestRegisterPromptsFromBackend_EmptyPromptsList` | Streamable backend returns `{"prompts": []}` → empty-list path → nil return | | `TestRegisterPromptsFromBackend_RegistersPrompts` | Streamable backend returns 2 prompts → prompts are registered → nil return | Three reusable helpers were introduced: - **`newStreamableBackendWithPromptsCapability`** — `httptest.Server` that speaks streamable HTTP MCP protocol (`Mcp-Session-Id` header in initialize, `capabilities: {"prompts": {}}`, 202 for notifications/initialized, delegate for `prompts/list`) - **`connectStreamableBackend`** — establishes a `launcher.Connection` backed by the test server and asserts `BackendHasPromptsCapability() == true` - **`minimalPromptsTestServer`** — minimal `UnifiedServer` with only the SDK server field populated (sufficient for the registration phase; `launcher` is not needed until a prompt is actually invoked) ### Coverage | Metric | Before | After | |--------|--------|-------| | `registerPromptsFromBackend` | 9.4% | **68.8%** | | `internal/server` package total | 91.8% | **93.1%** | ### Test output ``` === RUN TestRegisterPromptsFromBackend_NoCapability --- PASS: TestRegisterPromptsFromBackend_NoCapability (0.00s) === RUN TestRegisterPromptsFromBackend_RequestError --- PASS: TestRegisterPromptsFromBackend_RequestError (0.00s) === RUN TestRegisterPromptsFromBackend_EmptyPromptsList --- PASS: TestRegisterPromptsFromBackend_EmptyPromptsList (0.00s) === RUN TestRegisterPromptsFromBackend_RegistersPrompts --- PASS: TestRegisterPromptsFromBackend_RegistersPrompts (0.00s) PASS ok github.com/github/gh-aw-mcpg/internal/server 0.017s ``` All existing tests continue to pass (`make agent-finished` green for all Go packages and integration tests). > [!WARNING] > <details> > <summary>Firewall blocked 1 domain</summary> > > The following domain was blocked by the firewall during workflow execution: > > - `index.crates.io` >> To allow these domains, add them to the `network.allowed` list in your workflow frontmatter: > > ```yaml > network: > allowed: > - defaults > - "index.crates.io" > ``` > > See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information. > > </details> > Generated by [Test Improver](https://github.com/github/gh-aw-mcpg/actions/runs/28410847582) · 787.4 AIC · ⊞ 5.6K · [◷](https://github.com/search?q=repo%3Agithub%2Fgh-aw-mcpg+%22gh-aw-workflow-id%3A+test-improver%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Test Improver, engine: copilot, version: 1.0.65, model: claude-sonnet-4.6, id: 28410847582, workflow_id: test-improver, run: https://github.com/github/gh-aw-mcpg/actions/runs/28410847582 --> <!-- gh-aw-workflow-id: test-improver --> <!-- gh-aw-workflow-call-id: github/gh-aw-mcpg/test-improver -->
2 parents a47eb6d + d791574 commit dc07359

1 file changed

Lines changed: 224 additions & 0 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/github/gh-aw-mcpg/internal/config"
12+
"github.com/github/gh-aw-mcpg/internal/launcher"
13+
"github.com/github/gh-aw-mcpg/internal/mcp"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// newStreamableBackendWithPromptsCapability creates an httptest.Server that speaks the
19+
// streamable HTTP MCP protocol and declares prompts capability in its initialize response.
20+
// The onPromptsList callback is invoked for each prompts/list request and receives the
21+
// http.ResponseWriter and the JSON-RPC request-ID so the caller can write the desired
22+
// response.
23+
func newStreamableBackendWithPromptsCapability(
24+
t *testing.T,
25+
onPromptsList func(w http.ResponseWriter, reqID interface{}),
26+
) *httptest.Server {
27+
t.Helper()
28+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
29+
if r.Method != http.MethodPost {
30+
w.WriteHeader(http.StatusMethodNotAllowed)
31+
return
32+
}
33+
34+
body, err := io.ReadAll(r.Body)
35+
if err != nil || len(body) == 0 {
36+
w.WriteHeader(http.StatusBadRequest)
37+
return
38+
}
39+
40+
var req map[string]interface{}
41+
if err := json.Unmarshal(body, &req); err != nil {
42+
w.WriteHeader(http.StatusBadRequest)
43+
return
44+
}
45+
46+
method, _ := req["method"].(string)
47+
switch method {
48+
case "initialize":
49+
w.Header().Set("Content-Type", "application/json")
50+
w.Header().Set("Mcp-Session-Id", "test-prompts-session")
51+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
52+
"jsonrpc": "2.0",
53+
"id": req["id"],
54+
"result": map[string]interface{}{
55+
"protocolVersion": "2024-11-05",
56+
"capabilities": map[string]interface{}{
57+
"prompts": map[string]interface{}{},
58+
},
59+
"serverInfo": map[string]interface{}{
60+
"name": "prompts-capable-backend",
61+
"version": "1.0.0",
62+
},
63+
},
64+
})
65+
66+
case "notifications/initialized":
67+
w.WriteHeader(http.StatusAccepted)
68+
69+
case "prompts/list":
70+
onPromptsList(w, req["id"])
71+
72+
default:
73+
w.WriteHeader(http.StatusNotFound)
74+
}
75+
}))
76+
}
77+
78+
// connectStreamableBackend establishes a launcher connection to the given server
79+
// and verifies that the resulting connection declares prompts capability.
80+
func connectStreamableBackend(t *testing.T, srv *httptest.Server) *mcp.Connection {
81+
t.Helper()
82+
83+
cfg := &config.Config{
84+
Servers: map[string]*config.ServerConfig{
85+
"prompts-server": {Type: "http", URL: srv.URL},
86+
},
87+
}
88+
89+
l := launcher.New(context.Background(), cfg)
90+
t.Cleanup(func() { l.Close() })
91+
92+
conn, err := launcher.GetOrLaunch(l, "prompts-server")
93+
require.NoError(t, err, "GetOrLaunch should succeed for streamable backend")
94+
require.NotNil(t, conn, "connection should not be nil")
95+
require.True(t, conn.BackendHasPromptsCapability(),
96+
"connection to a backend that returned capabilities.prompts should report prompts capability")
97+
return conn
98+
}
99+
100+
// minimalPromptsTestServer constructs just enough of a UnifiedServer for tests that call
101+
// registerPromptsFromBackend directly. Only the sdk.Server field is required during
102+
// prompt registration; the launcher and other fields are only needed when the registered
103+
// prompt handler is actually invoked.
104+
func minimalPromptsTestServer(t *testing.T) *UnifiedServer {
105+
t.Helper()
106+
return &UnifiedServer{
107+
server: newSDKServer("test-prompts", logUnified),
108+
}
109+
}
110+
111+
// TestRegisterPromptsFromBackend_NoCapability verifies that a backend without prompts
112+
// capability causes registerPromptsFromBackend to return immediately without error.
113+
// This exercises the !BackendHasPromptsCapability() early-return branch.
114+
func TestRegisterPromptsFromBackend_NoCapability(t *testing.T) {
115+
// Plain-JSON-RPC backend: no Mcp-Session-Id header → no SDK session → BackendHasPromptsCapability() == false
116+
plainBackend := newMockBackend(t, "no-prompts-backend", []string{"some_tool"})
117+
defer plainBackend.Close()
118+
119+
cfg := &config.Config{
120+
Servers: map[string]*config.ServerConfig{
121+
"no-prompts-server": {Type: "http", URL: plainBackend.URL},
122+
},
123+
}
124+
l := launcher.New(context.Background(), cfg)
125+
defer l.Close()
126+
127+
conn, err := launcher.GetOrLaunch(l, "no-prompts-server")
128+
require.NoError(t, err)
129+
assert.False(t, conn.BackendHasPromptsCapability(),
130+
"plain-JSON-RPC connection should not declare prompts capability")
131+
132+
us := minimalPromptsTestServer(t)
133+
err = us.registerPromptsFromBackend(context.Background(), "no-prompts-server", conn)
134+
assert.NoError(t, err, "should return nil when backend has no prompts capability")
135+
}
136+
137+
// TestRegisterPromptsFromBackend_RequestError verifies graceful handling when the backend
138+
// returns an HTTP error for prompts/list. The function should swallow the error (non-fatal)
139+
// and return nil.
140+
func TestRegisterPromptsFromBackend_RequestError(t *testing.T) {
141+
promptsListCalled := make(chan struct{}, 10)
142+
t.Cleanup(func() {
143+
assert.Equal(t, 1, len(promptsListCalled), "expected prompts/list to be called exactly once")
144+
})
145+
146+
srv := newStreamableBackendWithPromptsCapability(t, func(w http.ResponseWriter, _ interface{}) {
147+
promptsListCalled <- struct{}{}
148+
// Return HTTP 500 → SDK treats this as a request-level failure
149+
w.WriteHeader(http.StatusInternalServerError)
150+
})
151+
152+
conn := connectStreamableBackend(t, srv)
153+
us := minimalPromptsTestServer(t)
154+
155+
err := us.registerPromptsFromBackend(context.Background(), "prompts-server", conn)
156+
assert.NoError(t, err, "request error should be treated as a graceful skip, not a fatal error")
157+
}
158+
159+
// TestRegisterPromptsFromBackend_EmptyPromptsList verifies that a backend with prompts
160+
// capability that returns an empty prompts/list causes registerPromptsFromBackend to
161+
// return nil without registering anything.
162+
func TestRegisterPromptsFromBackend_EmptyPromptsList(t *testing.T) {
163+
promptsListCalled := make(chan struct{}, 10)
164+
t.Cleanup(func() {
165+
assert.Equal(t, 1, len(promptsListCalled), "expected prompts/list to be called exactly once")
166+
})
167+
168+
srv := newStreamableBackendWithPromptsCapability(t, func(w http.ResponseWriter, reqID interface{}) {
169+
promptsListCalled <- struct{}{}
170+
w.Header().Set("Content-Type", "application/json")
171+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
172+
"jsonrpc": "2.0",
173+
"id": reqID,
174+
"result": map[string]interface{}{
175+
"prompts": []interface{}{},
176+
},
177+
})
178+
})
179+
defer srv.Close()
180+
181+
conn := connectStreamableBackend(t, srv)
182+
us := minimalPromptsTestServer(t)
183+
184+
err := us.registerPromptsFromBackend(context.Background(), "prompts-server", conn)
185+
assert.NoError(t, err, "empty prompts list should return nil without registering anything")
186+
}
187+
188+
// TestRegisterPromptsFromBackend_RegistersPrompts verifies that prompts returned by the
189+
// backend are registered on the unified server. The function should return nil and the
190+
// SDK server should have the prompt added.
191+
func TestRegisterPromptsFromBackend_RegistersPrompts(t *testing.T) {
192+
promptsListCalled := make(chan struct{}, 10)
193+
t.Cleanup(func() {
194+
assert.Equal(t, 1, len(promptsListCalled), "expected prompts/list to be called exactly once")
195+
})
196+
197+
srv := newStreamableBackendWithPromptsCapability(t, func(w http.ResponseWriter, reqID interface{}) {
198+
promptsListCalled <- struct{}{}
199+
w.Header().Set("Content-Type", "application/json")
200+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
201+
"jsonrpc": "2.0",
202+
"id": reqID,
203+
"result": map[string]interface{}{
204+
"prompts": []map[string]interface{}{
205+
{
206+
"name": "summarize",
207+
"description": "Summarizes the given text",
208+
},
209+
{
210+
"name": "translate",
211+
"description": "Translates text to another language",
212+
},
213+
},
214+
},
215+
})
216+
})
217+
defer srv.Close()
218+
219+
conn := connectStreamableBackend(t, srv)
220+
us := minimalPromptsTestServer(t)
221+
222+
err := us.registerPromptsFromBackend(context.Background(), "prompts-server", conn)
223+
assert.NoError(t, err, "should return nil when prompts are successfully registered")
224+
}

0 commit comments

Comments
 (0)