promptHandler logs a failing Render's error with the prompt name and answers the fixed JSON-RPC internal error "pier: the prompt could not be rendered", as dispatch does for tool calls (A-WR-08), so database or provider text never reaches the agent or its transcript. TestPromptRenderError renders a prompt that fails with a database error naming its host and asserts, on both paths, that the client sees only the fixed text and the cause is in the log.
102 lines
3.7 KiB
Go
102 lines
3.7 KiB
Go
package pier_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// promptNames lists the prompt names a session sees, sorted.
|
|
func promptNames(t *testing.T, ctx context.Context, cs *mcp.ClientSession) []string {
|
|
t.Helper()
|
|
list, err := cs.ListPrompts(ctx, nil)
|
|
if err != nil {
|
|
t.Fatalf("prompts/list: %v", err)
|
|
}
|
|
var out []string
|
|
for _, p := range list.Prompts {
|
|
out = append(out, p.Name)
|
|
}
|
|
slices.Sort(out)
|
|
return out
|
|
}
|
|
|
|
// TestPromptWriteCeiling pins plan 06's prompt ceiling (C-IN-01, P-28): a
|
|
// prompt declaring Writes is listed for a read and write grant and hidden
|
|
// from a read-only grant (and cannot be fetched by it), while a prompt
|
|
// without Writes is listed for both.
|
|
func TestPromptWriteCeiling(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
draft := summarizePrompt()
|
|
draft.Name = "draft"
|
|
draft.Writes = true
|
|
in := startInstance(t, newApp(t, appSpec{guards: []pact.MCPGuard{&acmeGuard{}}}),
|
|
acmePlugin{tools: []pact.MCPTool{echoTool()}, prompts: []pact.MCPPrompt{summarizePrompt(), draft}})
|
|
t.Cleanup(in.pier().StopReaper)
|
|
|
|
for _, c := range []struct {
|
|
name string
|
|
token string
|
|
want []string
|
|
}{
|
|
{"read and write grant", "oas_at_test", []string{"acme_draft", "acme_summarize"}},
|
|
{"read-only grant", "oas_at_read", []string{"acme_summarize"}},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
for _, version := range []string{"", "newest"} {
|
|
cs := sdkClient(t, ctx, in.url(), c.token, clientOpts{version: version})
|
|
if got := promptNames(t, ctx, cs); !slices.Equal(got, c.want) {
|
|
t.Fatalf("version %q: prompts = %v, want %v", version, got, c.want)
|
|
}
|
|
_, err := cs.GetPrompt(ctx, &mcp.GetPromptParams{Name: "acme_draft", Arguments: map[string]string{"topic": "x"}})
|
|
if allowed := slices.Contains(c.want, "acme_draft"); (err == nil) != allowed {
|
|
t.Fatalf("version %q: get acme_draft err = %v, allowed %v", version, err, allowed)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPromptRenderError pins RA-WR-05: a prompt whose Render fails answers a
|
|
// fixed JSON-RPC error on both paths; the cause (here a database error
|
|
// naming its host and user) is logged server-side and never reaches the
|
|
// client.
|
|
func TestPromptRenderError(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
const cause = `failed to connect to host=db.internal user=summer database=app: dial tcp 10.0.0.5:5432`
|
|
broken := summarizePrompt()
|
|
broken.Name = "broken"
|
|
broken.Render = func(context.Context, map[string]string) ([]pact.MCPPromptMessage, error) {
|
|
return nil, errors.New(cause)
|
|
}
|
|
in := startInstance(t, newApp(t, appSpec{guards: []pact.MCPGuard{&acmeGuard{}}}),
|
|
acmePlugin{tools: []pact.MCPTool{echoTool()}, prompts: []pact.MCPPrompt{broken}})
|
|
t.Cleanup(in.pier().StopReaper)
|
|
logs := captureLogs(t)
|
|
|
|
for _, version := range []string{"", "newest"} {
|
|
cs := sdkClient(t, ctx, in.url(), "oas_at_test", clientOpts{version: version})
|
|
_, err := cs.GetPrompt(ctx, &mcp.GetPromptParams{Name: "acme_broken", Arguments: map[string]string{"topic": "x"}})
|
|
if err == nil {
|
|
t.Fatalf("version %q: a failing Render answered no error", version)
|
|
}
|
|
if strings.Contains(err.Error(), "db.internal") || strings.Contains(err.Error(), "10.0.0.5") {
|
|
t.Fatalf("version %q: the Render cause reached the client: %v", version, err)
|
|
}
|
|
if !strings.Contains(err.Error(), "pier: the prompt could not be rendered") {
|
|
t.Fatalf("version %q: error = %v, want the fixed text", version, err)
|
|
}
|
|
}
|
|
if !strings.Contains(logs.String(), "db.internal") {
|
|
t.Fatalf("the Render cause was not logged: %s", logs.String())
|
|
}
|
|
}
|