server_structured_test.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
  4
  5package mcp
  6
  7import (
  8	"context"
  9	"encoding/json"
 10	"fmt"
 11	"strings"
 12	"testing"
 13
 14	sdk "github.com/modelcontextprotocol/go-sdk/mcp"
 15
 16	"git.secluded.site/cooked-mcp/internal/cooked"
 17)
 18
 19func TestCallToolACIDAPIClientLimits2SummarizesLongShoppingListText(t *testing.T) {
 20	productGroups := make([]cooked.ProductGroup, maxShoppingListTextItems+1)
 21	for index := range productGroups {
 22		productGroups[index] = cooked.ProductGroup{
 23			ID:   fmt.Sprintf("item-%02d", index+1),
 24			Name: fmt.Sprintf("Item %02d", index+1),
 25		}
 26	}
 27
 28	backend := &fakeBackend{shoppingList: cooked.ShoppingList{Aisles: []cooked.Aisle{{
 29		ID:            "pantry",
 30		Name:          "Pantry",
 31		ProductGroups: productGroups,
 32	}}}}
 33	server := NewServer(backend, "test")
 34
 35	result, output, err := server.callShoppingListReadTool(context.Background())
 36	if err != nil {
 37		t.Fatalf("callShoppingListReadTool() error = %v", err)
 38	}
 39
 40	text := requireTextContent(t, result)
 41	if !strings.Contains(text, "Shopping list (showing first 30 of 31 items):") {
 42		t.Fatalf("text output missing shopping-list summary: %q", text)
 43	}
 44	if strings.Contains(text, "Item 31") || strings.Contains(text, "item-31") {
 45		t.Fatalf("text output included truncated item: %q", text)
 46	}
 47	if !strings.Contains(text, "Additional shopping-list items are available in structured_content") {
 48		t.Fatalf("text output missing structured_content hint: %q", text)
 49	}
 50	if len(output.Aisles) != 1 || len(output.Aisles[0].ProductGroups) != maxShoppingListTextItems+1 {
 51		t.Fatalf("structured shopping-list item count = %#v, want all items", output.Aisles)
 52	}
 53	requireStructuredContent(t, result, output)
 54}
 55
 56func TestCallToolACIDAPIClientLimits2PreservesShortShoppingListText(t *testing.T) {
 57	backend := &fakeBackend{shoppingList: cooked.ShoppingList{Aisles: []cooked.Aisle{{
 58		ID:   "pantry",
 59		Name: "Pantry",
 60		ProductGroups: []cooked.ProductGroup{{
 61			ID:       "pasta",
 62			Name:     "Pasta",
 63			Quantity: "200g",
 64		}},
 65	}}}}
 66	server := NewServer(backend, "test")
 67
 68	result, _, err := server.callShoppingListReadTool(context.Background())
 69	if err != nil {
 70		t.Fatalf("callShoppingListReadTool() error = %v", err)
 71	}
 72
 73	text := requireTextContent(t, result)
 74	want := "Shopping list:\n- Pantry:\n  - Pasta — 200g (id: pasta)"
 75	if text != want {
 76		t.Fatalf("short shopping-list text = %q, want %q", text, want)
 77	}
 78	if strings.Contains(text, "structured_content") || strings.Contains(text, "showing first") {
 79		t.Fatalf("short shopping-list text included truncation hint: %q", text)
 80	}
 81}
 82
 83func requireTextContent(t *testing.T, result *sdk.CallToolResult) string {
 84	t.Helper()
 85
 86	if len(result.Content) != 1 {
 87		t.Fatalf("text content length = %d, want 1", len(result.Content))
 88	}
 89	textContent, ok := result.Content[0].(*sdk.TextContent)
 90	if !ok {
 91		t.Fatalf("content type = %T, want *sdk.TextContent", result.Content[0])
 92	}
 93
 94	return textContent.Text
 95}
 96
 97func requireStructuredContent(t *testing.T, result *sdk.CallToolResult, want any) {
 98	t.Helper()
 99	// tools.RESPONSES.5
100
101	if result == nil {
102		t.Fatal("result is nil")
103	}
104	if result.StructuredContent == nil {
105		t.Fatal("structured content is nil")
106	}
107
108	gotJSON, err := json.Marshal(result.StructuredContent)
109	if err != nil {
110		t.Fatalf("marshal structured content: %v", err)
111	}
112	wantJSON, err := json.Marshal(want)
113	if err != nil {
114		t.Fatalf("marshal wanted structured content: %v", err)
115	}
116	if string(gotJSON) != string(wantJSON) {
117		t.Fatalf("structured content = %s, want %s", gotJSON, wantJSON)
118	}
119
120	var object map[string]any
121	if err := json.Unmarshal(gotJSON, &object); err != nil {
122		t.Fatalf("structured content is not a JSON object: %v", err)
123	}
124}