feat: lokaler MCP-Server, Phase 4 (Elternbrief-Vorlagen + Claude-Desktop-Registrierung)
Schließt die MCP-Server-Spec ab. "Worksheets" aus der Spec entsprechen im tatsächlichen Datenmodell den .lavorlage-Elternbrief-Vorlagen (LehrerApp.Templating) - es gibt kein separates Arbeitsblatt-Konzept mit Fach/Klassenstufe-Metadaten. Neue Tools list_letter_templates (Read) und render_letter (Read, liefert Base64-PDF, kein DB-Schreibzugriff). upload_worksheet/update_worksheet bewusst nicht umgesetzt: das Seitenlayout ist eine eigene positionsbasierte DSL mit eigenem visuellen Editor (LehrerApp.TemplateDesigner) - ein LLM müsste sie blind erzeugen, mit hohem Risiko für kaputte Layouts. Platzhalter-Logik aus CreateLetterDialogViewModel nach LetterPlaceholderBuilder extrahiert, damit Dialog und MCP-Tool nicht auseinanderdriften. Neuer McpClientRegistrationService trägt den Bridge-Pfad in Claude Desktops claude_desktop_config.json ein (Button in den Einstellungen, nie automatisch), ohne bestehende Fremdeinträge zu verlieren und ohne eine nicht lesbare Konfigurationsdatei zu überschreiben. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services.Mcp.Tools;
|
||||
using LehrerApp.Templating;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class LetterTemplateToolsTests : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"lehrerapp-lettertools-tests-{Guid.NewGuid():N}");
|
||||
public LetterTemplateToolsTests() => Directory.CreateDirectory(_directory);
|
||||
public void Dispose() { if (Directory.Exists(_directory)) Directory.Delete(_directory, true); }
|
||||
|
||||
private LetterTemplateTools BuildTool(TemplateStore store, FakeStudents? students = null, FakeGroups? groups = null) =>
|
||||
new(store, new TemplateLoader(), new QuestTemplateRenderer(), students ?? new FakeStudents([]), groups ?? new FakeGroups([]));
|
||||
|
||||
private TemplateStore StoreWithTemplate(string name, params PlaceholderDefinition[] definitions)
|
||||
{
|
||||
var source = Path.Combine(_directory, $"{Guid.NewGuid():N}.lavorlage");
|
||||
var manifest = new TemplateManifest { Id = $"brief-{Guid.NewGuid():N}", Name = name, Placeholders = [.. definitions] };
|
||||
var lines = new List<string> { "PAGE 210 297 mm" };
|
||||
var y = 20;
|
||||
foreach (var definition in definitions)
|
||||
{
|
||||
var element = definition.Type == PlaceholderType.Multiline ? "TEXTBOX" : "TEXT";
|
||||
lines.Add(element == "TEXTBOX" ? $"TEXTBOX 20 {y} 170 80 ${definition.Name}" : $"TEXT 20 {y} ${definition.Name}");
|
||||
y += 20;
|
||||
}
|
||||
TemplatePackage.Create(source, manifest, string.Join('\n', lines), new Dictionary<string, byte[]>());
|
||||
var store = new TemplateStore(Path.Combine(_directory, $"store-{Guid.NewGuid():N}"));
|
||||
store.Import(source);
|
||||
return store;
|
||||
}
|
||||
|
||||
private static Student StudentWithContact(string? salutation = "Sehr geehrte Frau Muster,") => new()
|
||||
{
|
||||
FirstName = "Lena", LastName = "Beispiel",
|
||||
Contacts = [new Contact
|
||||
{
|
||||
Name = "Frau Muster", Relation = "Mutter", LetterSalutation = salutation,
|
||||
Street = "Hauptstraße 1", PostalCode = "12345", City = "Musterstadt",
|
||||
}],
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void ListLetterTemplates_ListetVorlageMitPlatzhaltern()
|
||||
{
|
||||
var store = StoreWithTemplate("Elternbrief", new PlaceholderDefinition("Anrede", PlaceholderType.Text, true));
|
||||
var tool = BuildTool(store);
|
||||
|
||||
var result = Assert.Single(tool.ListLetterTemplates());
|
||||
|
||||
Assert.Equal("Elternbrief", result.Name);
|
||||
Assert.Contains(result.Placeholders, p => p.Name == "Anrede" && p.Required);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderLetter_UnbekannteVorlage_LiefertFehler()
|
||||
{
|
||||
var student = StudentWithContact();
|
||||
var tool = BuildTool(new TemplateStore(_directory), new FakeStudents([student]));
|
||||
|
||||
var result = tool.RenderLetter("nicht-vorhanden", student.Id, "Text", "Frau Lehrer");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Null(result.Base64Pdf);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderLetter_UnbekannterSchueler_LiefertFehler()
|
||||
{
|
||||
var store = StoreWithTemplate("Elternbrief", new PlaceholderDefinition("Anrede", PlaceholderType.Text, true));
|
||||
var installed = Assert.Single(store.GetTemplates());
|
||||
var tool = BuildTool(store);
|
||||
|
||||
var result = tool.RenderLetter(installed.Id, Guid.NewGuid(), "Text", "Frau Lehrer");
|
||||
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderLetter_PflichtplatzhalterFehlt_LiefertFehlerOhnePdf()
|
||||
{
|
||||
var store = StoreWithTemplate("Elternbrief", new PlaceholderDefinition("Anrede", PlaceholderType.Text, true));
|
||||
var installed = Assert.Single(store.GetTemplates());
|
||||
var student = StudentWithContact(salutation: null); // keine Anrede hinterlegt
|
||||
var tool = BuildTool(store, new FakeStudents([student]));
|
||||
|
||||
var result = tool.RenderLetter(installed.Id, student.Id, "Text", "Frau Lehrer");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("Anrede", result.Message);
|
||||
Assert.Null(result.Base64Pdf);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderLetter_VollstaendigeDaten_LiefertBase64Pdf()
|
||||
{
|
||||
var store = StoreWithTemplate("Elternbrief",
|
||||
new PlaceholderDefinition("Anrede", PlaceholderType.Text, true),
|
||||
new PlaceholderDefinition("Datum", PlaceholderType.Date, true),
|
||||
new PlaceholderDefinition("Brieftext", PlaceholderType.Multiline, true));
|
||||
var installed = Assert.Single(store.GetTemplates());
|
||||
var student = StudentWithContact();
|
||||
var tool = BuildTool(store, new FakeStudents([student]));
|
||||
|
||||
var result = tool.RenderLetter(installed.Id, student.Id, "Dies ist der Inhalt.", "Frau Lehrer");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Base64Pdf);
|
||||
var bytes = Convert.FromBase64String(result.Base64Pdf!);
|
||||
Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(bytes, 0, 4));
|
||||
Assert.Equal("Elternbrief_Beispiel_Lena.pdf", result.SuggestedFileName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderLetter_ExtraValues_FuelltZusaetzlichenPlatzhalter()
|
||||
{
|
||||
var store = StoreWithTemplate("Elternbrief",
|
||||
new PlaceholderDefinition("Anrede", PlaceholderType.Text, true),
|
||||
new PlaceholderDefinition("Betreff", PlaceholderType.Text, true));
|
||||
var installed = Assert.Single(store.GetTemplates());
|
||||
var student = StudentWithContact();
|
||||
var tool = BuildTool(store, new FakeStudents([student]));
|
||||
|
||||
var result = tool.RenderLetter(installed.Id, student.Id, "Text", "Frau Lehrer",
|
||||
extraValues: new Dictionary<string, string> { ["Betreff"] = "Elternabend" });
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Base64Pdf);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user