1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
4
5package mcp
6
7import (
8 "context"
9 "testing"
10
11 "git.secluded.site/cooked-mcp/internal/cooked"
12)
13
14func TestCallToolACIDToolsSurface1ReadsShoppingList(t *testing.T) {
15 backend := &fakeBackend{shoppingList: cooked.ShoppingList{
16 Aisles: []cooked.Aisle{{
17 ID: "pantry",
18 Name: "Pantry",
19 ProductGroups: []cooked.ProductGroup{{
20 ID: "pasta",
21 Name: "Pasta",
22 Quantity: "200g",
23 }},
24 }},
25 }}
26 server := NewServer(backend, "test")
27
28 output, err := server.CallReadTool(context.Background(), ReadArguments{Target: "shopping_list"})
29 if err != nil {
30 t.Fatalf("CallReadTool() error = %v", err)
31 }
32
33 if len(output.Aisles) != 1 {
34 t.Fatalf("structured aisles = %d, want 1", len(output.Aisles))
35 }
36 if output.Aisles[0].ProductGroups[0].Name != "Pasta" {
37 t.Fatalf("first item = %q, want Pasta", output.Aisles[0].ProductGroups[0].Name)
38 }
39}
40
41func TestCallToolACIDRecipesRead1ListsRecipes(t *testing.T) {
42 backend := &fakeBackend{recipes: []cooked.RecipeCard{{ID: "recipe-1", Title: "Pasta", ThumbnailURL: "https://example.invalid/thumb.jpg"}}}
43 server := NewServer(backend, "test")
44
45 output, err := server.CallReadTool(context.Background(), ReadArguments{Target: "recipes", Page: 2, Limit: 5})
46 if err != nil {
47 t.Fatalf("CallReadTool() error = %v", err)
48 }
49
50 if backend.page != 2 {
51 t.Fatalf("backend page = %d, want 2", backend.page)
52 }
53 if backend.limit != 5 {
54 t.Fatalf("backend limit = %d, want 5", backend.limit)
55 }
56 if len(output.Recipes) != 1 {
57 t.Fatalf("structured recipes = %d, want 1", len(output.Recipes))
58 }
59 if output.Recipes[0].ID != "recipe-1" || output.Recipes[0].Title != "Pasta" {
60 t.Fatalf("first recipe = %#v, want recipe-1 Pasta", output.Recipes[0])
61 }
62}
63
64func TestCallToolACIDRecipesPagination4DefaultsAndCapsRecipeLimit(t *testing.T) {
65 backend := &fakeBackend{}
66 server := NewServer(backend, "test")
67
68 _, err := server.CallReadTool(context.Background(), ReadArguments{Target: "recipes", Limit: 99})
69 if err != nil {
70 t.Fatalf("CallReadTool() error = %v", err)
71 }
72
73 if backend.page != 1 {
74 t.Fatalf("backend page = %d, want default page 1", backend.page)
75 }
76 if backend.limit != 30 {
77 t.Fatalf("backend limit = %d, want capped limit 30", backend.limit)
78 }
79}
80
81func TestCallToolACIDRecipesRead2SearchesRecipes(t *testing.T) {
82 backend := &fakeBackend{searchRecipes: []cooked.RecipeCard{
83 {ID: "recipe-1", Title: "Pasta", ThumbnailURL: "https://example.invalid/thumb.jpg"},
84 {ID: "recipe-2", Title: "Tomato Pasta", ThumbnailURL: "https://example.invalid/thumb2.jpg"},
85 }}
86 server := NewServer(backend, "test")
87
88 output, err := server.CallReadTool(context.Background(), ReadArguments{
89 Target: "recipes",
90 Query: " pasta ",
91 Page: 2,
92 Limit: 1,
93 })
94 if err != nil {
95 t.Fatalf("CallReadTool() error = %v", err)
96 }
97
98 if backend.searchQuery != "pasta" {
99 t.Fatalf("backend search query = %q, want pasta", backend.searchQuery)
100 }
101 if backend.searchPage != 2 {
102 t.Fatalf("backend search page = %d, want 2", backend.searchPage)
103 }
104 if len(output.Recipes) != 1 {
105 t.Fatalf("structured recipes = %d, want truncated result length 1", len(output.Recipes))
106 }
107 if output.Recipes[0].ID != "recipe-1" || output.Recipes[0].Title != "Pasta" {
108 t.Fatalf("first recipe = %#v, want recipe-1 Pasta", output.Recipes[0])
109 }
110}
111
112type fakeBackend struct {
113 shoppingList cooked.ShoppingList
114 recipes []cooked.RecipeCard
115 searchRecipes []cooked.RecipeCard
116 page int
117 limit int
118 searchQuery string
119 searchPage int
120}
121
122func (f *fakeBackend) ReadShoppingList(context.Context) (cooked.ShoppingList, error) {
123 return f.shoppingList, nil
124}
125
126func (f *fakeBackend) ListRecipes(_ context.Context, page, limit int) ([]cooked.RecipeCard, error) {
127 f.page = page
128 f.limit = limit
129
130 return f.recipes, nil
131}
132
133func (f *fakeBackend) SearchRecipes(_ context.Context, query string, page int) ([]cooked.RecipeCard, error) {
134 f.searchQuery = query
135 f.searchPage = page
136
137 return f.searchRecipes, nil
138}