Files
summercms/modules/pier/stateless_smoke_test.go
Jakub Zych 806e87c510 feat(19.7-02): Redis sessions, event fan-out, stateless protocol path and resource metadata
- store_redis.go: records with PX TTLs, per-binding sets, pub/sub events,
  opened lazily from redis.url under cooler's prefix (nothing dials at mount)
- events.go: pact.MCPSubjectChanged republished through the store; every
  instance recomputes the subject's sessions and stateless servers
  (list_changed) or ends them on revocation; the link cache is dropped
- stateless.go: the SDK's stateless handler over per-identity servers for
  2026-07-28 clients, routed by the protocol header or server/discover
- prm.go: RFC 9728 metadata at both well-known paths; long GET streams
  re-verify their bearer every minute
- TestPierSessionsSmoke, TestPierFanOutSmoke, TestPierStatelessSmoke on
  miniredis; README features, configuration and operational notes
2026-10-22 18:23:43 +02:00

81 lines
2.7 KiB
Go

package pier_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// Smoke test of phase 19.7 plan 02 Task 3: the hybrid protocol (D-32). A
// client at the SDK's newest protocol negotiates 2026-07-28 statelessly
// (no Mcp-Session-Id) and calls the tool; a client pinned to 2025-11-25
// gets a session on the stateful path; both see the same per-identity list.
func TestPierStatelessSmoke(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
app := pierApp(t, "enabled: false\n")
host, err := mountPier(t, app, []party.Plugin{acmePlugin{tools: []pact.MCPTool{echoTool()}}})
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(host)
defer srv.Close()
transport := func() *mcp.StreamableClientTransport {
return &mcp.StreamableClientTransport{
Endpoint: srv.URL + "/mcp",
HTTPClient: &http.Client{Transport: bearerTransport{token: "oas_at_test"}},
}
}
newest := mcp.NewClient(&mcp.Implementation{Name: "newest", Version: "1"}, nil)
stateless, err := newest.Connect(ctx, transport(), nil)
if err != nil {
t.Fatalf("stateless connect: %v", err)
}
defer stateless.Close()
if stateless.ID() != "" {
t.Fatalf("the newest client got a session id %q, want none", stateless.ID())
}
if v := stateless.InitializeResult().ProtocolVersion; v != "2026-07-28" {
t.Fatalf("negotiated %q, want 2026-07-28", v)
}
res, err := stateless.CallTool(ctx, &mcp.CallToolParams{Name: "acme_echo", Arguments: map[string]any{"id": 3, "text": "yo"}})
if err != nil || res.IsError {
t.Fatalf("stateless tools/call = %+v, %v", res, err)
}
if structured, _ := res.StructuredContent.(map[string]any); structured["principal"] != 7.0 || structured["id"] != 3.0 {
t.Fatalf("stateless structuredContent = %+v", res.StructuredContent)
}
legacy := mcp.NewClient(&mcp.Implementation{Name: "legacy", Version: "1"}, nil)
stateful, err := legacy.Connect(ctx, transport(), &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
if err != nil {
t.Fatalf("stateful connect: %v", err)
}
defer stateful.Close()
if stateful.ID() == "" {
t.Fatal("the 2025-11-25 client got no session id")
}
if v := stateful.InitializeResult().ProtocolVersion; v != "2025-11-25" {
t.Fatalf("negotiated %q, want 2025-11-25", v)
}
a, err := stateless.ListTools(ctx, nil)
if err != nil {
t.Fatal(err)
}
b, err := stateful.ListTools(ctx, nil)
if err != nil {
t.Fatal(err)
}
if len(a.Tools) != 1 || len(b.Tools) != 1 || a.Tools[0].Name != b.Tools[0].Name || a.Tools[0].Name != "acme_echo" {
t.Fatalf("lists differ: stateless %+v, stateful %+v", a.Tools, b.Tools)
}
}