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
14// recipes.SAVE.7 recipes.SAVE.7-2 recipes.SAVE.9 recipes.SAVE.16 recipes.SAFETY.1 tools.SAVE_RECIPE_TOOL.1 tools.SAVE_RECIPE_TOOL.5
15func TestCallToolACIDRecipesSave7And9And16SavesReviewedDraft(t *testing.T) {
16 draftID := "draft/with space"
17 backend := &fakeBackend{draftSaveRecipeID: testRecipeID}
18 server := NewServer(backend, "test")
19
20 _, output, err := server.callSaveRecipeTool(
21 context.Background(),
22 nil,
23 SaveRecipeArguments{
24 Source: " draft ",
25 DraftID: " " + draftID + " ",
26 Markdown: " # Pasta\n\n1. Boil pasta.\n",
27 Portions: 2,
28 },
29 )
30 if err != nil {
31 t.Fatalf("callSaveRecipeTool() error = %v", err)
32 }
33
34 if backend.draftSaveCalls != 1 {
35 t.Fatalf("draft save calls = %d, want 1", backend.draftSaveCalls)
36 }
37 if backend.draftSaveID != draftID {
38 t.Fatalf("backend draft ID = %q, want draft ID", backend.draftSaveID)
39 }
40 if backend.draftSaveMarkdown != " # Pasta\n\n1. Boil pasta.\n" {
41 t.Fatalf("backend draft markdown = %q, want raw markdown preserved", backend.draftSaveMarkdown)
42 }
43 if backend.draftSavePortions != 2 {
44 t.Fatalf("backend draft portions = %g, want 2", backend.draftSavePortions)
45 }
46 if output.RecipeID != testRecipeID || output.DraftID != "" || output.Markdown != "" || output.Portions != 0 {
47 t.Fatalf("draft save output = %#v, want recipe_id only", output)
48 }
49 if backend.importRecipeURLCalls != 0 || backend.metadataCalls != 0 || backend.contentCalls != 0 {
50 t.Fatalf(
51 "unexpected import/read calls = import %d metadata %d content %d, want none",
52 backend.importRecipeURLCalls,
53 backend.metadataCalls,
54 backend.contentCalls,
55 )
56 }
57 if backend.saveCalls != 0 || backend.updateCalls != 0 {
58 t.Fatalf("prepared/update calls = %d/%d, want none", backend.saveCalls, backend.updateCalls)
59 }
60}
61
62// recipes.SAVE.7-5 recipes.SAVE.9-1 recipes.SAVE.16 recipes.SAFETY.1 recipes.SAFETY.2
63func TestCallToolACIDRecipesSave7_5And9_1OverwritesExistingRecipeFromDraft(t *testing.T) {
64 backend := &fakeBackend{}
65 server := NewServer(backend, "test")
66
67 _, output, err := server.callSaveRecipeTool(
68 context.Background(),
69 nil,
70 SaveRecipeArguments{
71 Source: " draft ",
72 DraftID: " " + testDraftID + " ",
73 RecipeID: " " + testRecipeID + " ",
74 Markdown: " # Pasta\n\n1. Boil pasta.\n",
75 Portions: 2,
76 },
77 )
78 if err != nil {
79 t.Fatalf("callSaveRecipeTool() error = %v", err)
80 }
81
82 if backend.updateCalls != 1 {
83 t.Fatalf("update calls = %d, want 1", backend.updateCalls)
84 }
85 if backend.updateRecipeID != testRecipeID {
86 t.Fatalf("backend update recipe ID = %q, want test recipe ID", backend.updateRecipeID)
87 }
88 if backend.updateMarkdown != " # Pasta\n\n1. Boil pasta.\n" {
89 t.Fatalf("backend update markdown = %q, want raw markdown preserved", backend.updateMarkdown)
90 }
91 if backend.updatePortions != 2 {
92 t.Fatalf("backend update portions = %g, want 2", backend.updatePortions)
93 }
94 if output.RecipeID != testRecipeID {
95 t.Fatalf("save output recipe ID = %q, want test recipe ID", output.RecipeID)
96 }
97 if backend.draftSaveCalls != 0 || backend.saveCalls != 0 {
98 t.Fatalf("draft save/prepared calls = %d/%d, want none", backend.draftSaveCalls, backend.saveCalls)
99 }
100}
101
102// recipes.SAVE.7 recipes.SAVE.7-3 recipes.SAVE.9 recipes.SAVE.16 tools.SAVE_RECIPE_TOOL.5
103func TestCallToolACIDRecipesSave7_3SavesCurrentDraftContentWhenContentOmitted(t *testing.T) {
104 backend := &fakeBackend{
105 draftSaveRecipeID: testRecipeID,
106 recipeContent: cooked.RecipeContent{
107 Content: "# Pasta\n\n1. Boil pasta.",
108 Portions: 1.5,
109 },
110 }
111 server := NewServer(backend, "test")
112
113 _, output, err := server.callSaveRecipeTool(
114 context.Background(),
115 nil,
116 SaveRecipeArguments{Source: "draft", DraftID: " " + testDraftID + " "},
117 )
118 if err != nil {
119 t.Fatalf("callSaveRecipeTool() error = %v", err)
120 }
121
122 if backend.contentCalls != 1 || backend.contentRecipeID != testDraftID {
123 t.Fatalf("content reads = %d for %q, want 1 for test draft ID", backend.contentCalls, backend.contentRecipeID)
124 }
125 if backend.draftSaveCalls != 1 {
126 t.Fatalf("draft save calls = %d, want 1", backend.draftSaveCalls)
127 }
128 if backend.draftSaveMarkdown != "# Pasta\n\n1. Boil pasta." {
129 t.Fatalf("backend draft markdown = %q, want current draft content", backend.draftSaveMarkdown)
130 }
131 if backend.draftSavePortions != 1.5 {
132 t.Fatalf("backend draft portions = %g, want current draft portions 1.5", backend.draftSavePortions)
133 }
134 if output.RecipeID != testRecipeID || output.DraftID != "" || output.Markdown != "" || output.Portions != 0 {
135 t.Fatalf("draft save output = %#v, want recipe_id only", output)
136 }
137 if backend.metadataCalls != 0 || backend.importRecipeURLCalls != 0 {
138 t.Fatalf(
139 "unexpected metadata/import calls = %d/%d, want none",
140 backend.metadataCalls,
141 backend.importRecipeURLCalls,
142 )
143 }
144}
145
146// recipes.SAVE.7-1 recipes.SAVE.7-4 tools.SAVE_RECIPE_TOOL.5
147func TestCallToolACIDRecipesSave7_1And7_4AndToolsSaveRecipeTool5RequiresDraftIDAndCompleteContent(t *testing.T) {
148 tests := []struct {
149 name string
150 arguments SaveRecipeArguments
151 }{
152 {
153 name: "draft_id",
154 arguments: SaveRecipeArguments{Source: "draft", DraftID: " ", Markdown: "# Pasta", Portions: 2},
155 },
156 {
157 name: "markdown_without_portions",
158 arguments: SaveRecipeArguments{Source: "draft", DraftID: testDraftID, Markdown: "# Pasta", Portions: 0},
159 },
160 {
161 name: "portions_without_markdown",
162 arguments: SaveRecipeArguments{Source: "draft", DraftID: testDraftID, Markdown: " ", Portions: 2},
163 },
164 }
165
166 for _, tt := range tests {
167 t.Run(tt.name, func(t *testing.T) {
168 backend := &fakeBackend{}
169 server := NewServer(backend, "test")
170
171 _, _, err := server.callSaveRecipeTool(context.Background(), nil, tt.arguments)
172 if err == nil {
173 t.Fatal("callSaveRecipeTool() error = nil, want missing draft field error")
174 }
175 if backend.draftSaveCalls != 0 {
176 t.Fatalf("draft save calls = %d, want none", backend.draftSaveCalls)
177 }
178 if backend.contentCalls != 0 {
179 t.Fatalf("content calls = %d, want none", backend.contentCalls)
180 }
181 if backend.saveCalls != 0 || backend.updateCalls != 0 || backend.importRecipeURLCalls != 0 {
182 t.Fatalf(
183 "unexpected backend calls = prepared %d update %d import %d, want none",
184 backend.saveCalls,
185 backend.updateCalls,
186 backend.importRecipeURLCalls,
187 )
188 }
189 })
190 }
191}
192
193// recipes.VALIDATION.1 recipes.VALIDATION.2
194func TestCallToolACIDRecipesValidation1And2RejectsMalformedDraftOverwriteRecipeIDBeforeCooked(t *testing.T) {
195 backend := &fakeBackend{}
196 server := NewServer(backend, "test")
197
198 _, _, err := server.callSaveRecipeTool(
199 context.Background(),
200 nil,
201 SaveRecipeArguments{
202 Source: "draft",
203 DraftID: testDraftID,
204 RecipeID: "not-a-uuid",
205 Markdown: "# Pasta",
206 Portions: 2,
207 },
208 )
209 if err == nil {
210 t.Fatal("callSaveRecipeTool() error = nil, want malformed recipe_id error")
211 }
212 if backend.updateCalls != 0 || backend.draftSaveCalls != 0 {
213 t.Fatalf("update/draft save calls = %d/%d, want none", backend.updateCalls, backend.draftSaveCalls)
214 }
215}