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}