server_wording_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	"reflect"
 10	"slices"
 11	"strings"
 12	"testing"
 13)
 14
 15// tools.READ_TOOL.1 tools.SCHEMA.4 tools.RESPONSES.6
 16func TestCallToolACIDToolsReadTool1RejectsUnsupportedTargetBeforeCallingCooked(t *testing.T) {
 17	backend := &fakeBackend{}
 18	server := NewServer(backend, "test")
 19
 20	_, err := server.CallReadTool(context.Background(), ReadArguments{Target: "meal_plan"})
 21	if err == nil {
 22		t.Fatal("CallReadTool() error = nil, want unsupported target error")
 23	}
 24	if err.Error() != `unsupported read target "meal_plan" (supported: shopping_list, recipes, recipe, example_recipes)` {
 25		t.Fatalf("CallReadTool() error = %q", err.Error())
 26	}
 27	if backend.readShoppingListCalls != 0 || backend.metadataCalls != 0 || backend.contentCalls != 0 {
 28		t.Fatalf(
 29			"backend calls = shopping list %d metadata %d content %d, want none",
 30			backend.readShoppingListCalls,
 31			backend.metadataCalls,
 32			backend.contentCalls,
 33		)
 34	}
 35}
 36
 37// recipes.SAVE.10 tools.SCHEMA.4 tools.RESPONSES.6
 38func TestCallToolACIDRecipesSave10RejectsUnsupportedSourceBeforeCallingCooked(t *testing.T) {
 39	backend := &fakeBackend{}
 40	server := NewServer(backend, "test")
 41
 42	_, _, err := server.callSaveRecipeTool(context.Background(), nil, SaveRecipeArguments{Source: "unsupported"})
 43	if err == nil {
 44		t.Fatal("callSaveRecipeTool() error = nil, want unsupported source error")
 45	}
 46	if err.Error() != `unsupported save_recipe source "unsupported" (supported: raw_text, prepared, url, draft, existing)` {
 47		t.Fatalf("callSaveRecipeTool() error = %q", err.Error())
 48	}
 49	if backend.saveCalls != 0 || backend.draftSaveCalls != 0 {
 50		t.Fatalf("save calls = prepared %d draft %d, want none", backend.saveCalls, backend.draftSaveCalls)
 51	}
 52}
 53
 54// tools.SCHEMA.3 tools.SCHEMA.5 tools.SAVE_RECIPE_TOOL.1 tools.CHANGE_SHOPPING_LIST_TOOL.1
 55func TestToolDescriptionsACIDToolsSchema3And5DescribeCurrentLimits(t *testing.T) {
 56	read := readTool()
 57	for _, want := range []string{
 58		"shopping_list",
 59		"recipes",
 60		"recipe with recipe_id",
 61		"example_recipes",
 62		"You MUST check the examples before saving anything",
 63		"particular Markdown dialect",
 64		"Recipe results omit images and thumbnails",
 65	} {
 66		if !strings.Contains(read.Description, want) {
 67			t.Fatalf("read description missing %q: %q", want, read.Description)
 68		}
 69	}
 70
 71	save := saveRecipeTool()
 72	for _, want := range []string{
 73		"raw text",
 74		"prepared markdown",
 75		"URL import",
 76		"reviewed URL-import draft",
 77		"existing recipe update",
 78		"URL imports will return a draft_id for review",
 79	} {
 80		if !strings.Contains(save.Description, want) {
 81			t.Fatalf("save_recipe description missing %q: %q", want, save.Description)
 82		}
 83	}
 84
 85	shopping := changeShoppingListTool()
 86	for _, want := range []string{
 87		"add",
 88		"update_item",
 89		"replace_selection",
 90		"add_selection",
 91		"remove_selection",
 92		"remove",
 93		"clear",
 94		"Remove and clear are destructive",
 95		"put quantities before names",
 96	} {
 97		if !strings.Contains(shopping.Description, want) {
 98			t.Fatalf("change_shopping_list description missing %q: %q", want, shopping.Description)
 99		}
100	}
101	if strings.Contains(shopping.Description, "Simple read-output lines") {
102		t.Fatalf("change_shopping_list description includes removed read-output guidance: %q", shopping.Description)
103	}
104}
105
106// tools.SCHEMA.6
107func TestChangeShoppingListToolACIDToolsSchema6ExposesProductGroupIDsAsOptionalNonNullableStringArray(t *testing.T) {
108	schema := changeShoppingListInputSchema()
109	property, ok := schema.Properties["product_group_ids"]
110	if !ok {
111		t.Fatal("product_group_ids schema missing")
112	}
113	if slices.Contains(schema.Required, "product_group_ids") {
114		t.Fatalf("required fields = %#v, want product_group_ids optional", schema.Required)
115	}
116	if property.Type != "array" {
117		t.Fatalf("product_group_ids type = %q, want array", property.Type)
118	}
119	if len(property.Types) != 0 {
120		t.Fatalf("product_group_ids types = %#v, want none", property.Types)
121	}
122	if len(property.AnyOf) != 0 {
123		t.Fatalf("product_group_ids anyOf = %#v, want none", property.AnyOf)
124	}
125	if property.Items == nil || property.Items.Type != "string" {
126		t.Fatalf("product_group_ids items = %#v, want string items", property.Items)
127	}
128
129	resolved, err := schema.Resolve(nil)
130	if err != nil {
131		t.Fatalf("Resolve() error = %v", err)
132	}
133	if err := resolved.Validate(map[string]any{"action": "clear"}); err != nil {
134		t.Fatalf("Validate() with omitted product_group_ids error = %v", err)
135	}
136	if err := resolved.Validate(map[string]any{"action": "clear", "product_group_ids": nil}); err == nil {
137		t.Fatal("Validate() with null product_group_ids error = nil, want rejection")
138	}
139}
140
141// tools.READ_TOOL.1 tools.SAVE_RECIPE_TOOL.3 tools.SAVE_RECIPE_TOOL.5 tools.SAVE_RECIPE_TOOL.6
142func TestArgumentDescriptionsACIDToolsReadTool1AndSaveRecipeTool3And5And6PointToExampleRecipes(t *testing.T) {
143	readTarget, ok := reflect.TypeFor[ReadArguments]().FieldByName("Target")
144	if !ok {
145		t.Fatal("ReadArguments.Target field missing")
146	}
147	if !strings.Contains(string(readTarget.Tag), "example_recipes") {
148		t.Fatalf("ReadArguments.Target tag = %q, want example_recipes", readTarget.Tag)
149	}
150
151	saveMarkdown, ok := reflect.TypeFor[SaveRecipeArguments]().FieldByName("Markdown")
152	if !ok {
153		t.Fatal("SaveRecipeArguments.Markdown field missing")
154	}
155	for _, want := range []string{"example_recipes", "Cooked's markdown dialect"} {
156		if !strings.Contains(string(saveMarkdown.Tag), want) {
157			t.Fatalf("SaveRecipeArguments.Markdown tag missing %q: %q", want, saveMarkdown.Tag)
158		}
159	}
160}