server_save_draft_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	"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	backend := &fakeBackend{draftSaveRecipeID: "recipe-1"}
 17	server := NewServer(backend, "test")
 18
 19	_, output, err := server.callSaveRecipeTool(
 20		context.Background(),
 21		nil,
 22		SaveRecipeArguments{
 23			Source:   " draft ",
 24			DraftID:  " draft-1 ",
 25			Markdown: "  # Pasta\n\n1. Boil pasta.\n",
 26			Portions: 2,
 27		},
 28	)
 29	if err != nil {
 30		t.Fatalf("callSaveRecipeTool() error = %v", err)
 31	}
 32
 33	if backend.draftSaveCalls != 1 {
 34		t.Fatalf("draft save calls = %d, want 1", backend.draftSaveCalls)
 35	}
 36	if backend.draftSaveID != "draft-1" {
 37		t.Fatalf("backend draft ID = %q, want draft-1", backend.draftSaveID)
 38	}
 39	if backend.draftSaveMarkdown != "  # Pasta\n\n1. Boil pasta.\n" {
 40		t.Fatalf("backend draft markdown = %q, want raw markdown preserved", backend.draftSaveMarkdown)
 41	}
 42	if backend.draftSavePortions != 2 {
 43		t.Fatalf("backend draft portions = %g, want 2", backend.draftSavePortions)
 44	}
 45	if output.RecipeID != "recipe-1" || output.DraftID != "" || output.Markdown != "" || output.Portions != 0 {
 46		t.Fatalf("draft save output = %#v, want recipe_id only", output)
 47	}
 48	if backend.importRecipeURLCalls != 0 || backend.metadataCalls != 0 || backend.contentCalls != 0 {
 49		t.Fatalf(
 50			"unexpected import/read calls = import %d metadata %d content %d, want none",
 51			backend.importRecipeURLCalls,
 52			backend.metadataCalls,
 53			backend.contentCalls,
 54		)
 55	}
 56	if backend.saveCalls != 0 || backend.updateCalls != 0 {
 57		t.Fatalf("prepared/update calls = %d/%d, want none", backend.saveCalls, backend.updateCalls)
 58	}
 59}
 60
 61// recipes.SAVE.7 recipes.SAVE.7-3 recipes.SAVE.9 recipes.SAVE.16 tools.SAVE_RECIPE_TOOL.5
 62func TestCallToolACIDRecipesSave7_3SavesCurrentDraftContentWhenContentOmitted(t *testing.T) {
 63	backend := &fakeBackend{
 64		draftSaveRecipeID: "recipe-1",
 65		recipeContent: cooked.RecipeContent{
 66			Content:  "# Pasta\n\n1. Boil pasta.",
 67			Portions: 1.5,
 68		},
 69	}
 70	server := NewServer(backend, "test")
 71
 72	_, output, err := server.callSaveRecipeTool(
 73		context.Background(),
 74		nil,
 75		SaveRecipeArguments{Source: "draft", DraftID: " draft-1 "},
 76	)
 77	if err != nil {
 78		t.Fatalf("callSaveRecipeTool() error = %v", err)
 79	}
 80
 81	if backend.contentCalls != 1 || backend.contentRecipeID != "draft-1" {
 82		t.Fatalf("content reads = %d for %q, want 1 for draft-1", backend.contentCalls, backend.contentRecipeID)
 83	}
 84	if backend.draftSaveCalls != 1 {
 85		t.Fatalf("draft save calls = %d, want 1", backend.draftSaveCalls)
 86	}
 87	if backend.draftSaveMarkdown != "# Pasta\n\n1. Boil pasta." {
 88		t.Fatalf("backend draft markdown = %q, want current draft content", backend.draftSaveMarkdown)
 89	}
 90	if backend.draftSavePortions != 1.5 {
 91		t.Fatalf("backend draft portions = %g, want current draft portions 1.5", backend.draftSavePortions)
 92	}
 93	if output.RecipeID != "recipe-1" || output.DraftID != "" || output.Markdown != "" || output.Portions != 0 {
 94		t.Fatalf("draft save output = %#v, want recipe_id only", output)
 95	}
 96	if backend.metadataCalls != 0 || backend.importRecipeURLCalls != 0 {
 97		t.Fatalf(
 98			"unexpected metadata/import calls = %d/%d, want none",
 99			backend.metadataCalls,
100			backend.importRecipeURLCalls,
101		)
102	}
103}
104
105// recipes.SAVE.7-1 recipes.SAVE.7-4 tools.SAVE_RECIPE_TOOL.5
106func TestCallToolACIDRecipesSave7_1And7_4AndToolsSaveRecipeTool5RequiresDraftIDAndCompleteContent(t *testing.T) {
107	tests := []struct {
108		name      string
109		arguments SaveRecipeArguments
110	}{
111		{
112			name:      "draft_id",
113			arguments: SaveRecipeArguments{Source: "draft", DraftID: "   ", Markdown: "# Pasta", Portions: 2},
114		},
115		{
116			name:      "markdown_without_portions",
117			arguments: SaveRecipeArguments{Source: "draft", DraftID: "draft-1", Markdown: "# Pasta", Portions: 0},
118		},
119		{
120			name:      "portions_without_markdown",
121			arguments: SaveRecipeArguments{Source: "draft", DraftID: "draft-1", Markdown: "   ", Portions: 2},
122		},
123	}
124
125	for _, tt := range tests {
126		t.Run(tt.name, func(t *testing.T) {
127			backend := &fakeBackend{}
128			server := NewServer(backend, "test")
129
130			_, _, err := server.callSaveRecipeTool(context.Background(), nil, tt.arguments)
131			if err == nil {
132				t.Fatal("callSaveRecipeTool() error = nil, want missing draft field error")
133			}
134			if backend.draftSaveCalls != 0 {
135				t.Fatalf("draft save calls = %d, want none", backend.draftSaveCalls)
136			}
137			if backend.contentCalls != 0 {
138				t.Fatalf("content calls = %d, want none", backend.contentCalls)
139			}
140			if backend.saveCalls != 0 || backend.updateCalls != 0 || backend.importRecipeURLCalls != 0 {
141				t.Fatalf(
142					"unexpected backend calls = prepared %d update %d import %d, want none",
143					backend.saveCalls,
144					backend.updateCalls,
145					backend.importRecipeURLCalls,
146				)
147			}
148		})
149	}
150}