using ClawdDotNet.Core.Tasks; using Shouldly; namespace ClawdDotNet.Core.Tests.Tasks; /// /// Der Frontmatter-Parser ist die Brücke zwischen der menschen-/agentenlesbaren /// Task-Datei und dem DB-Zustand. Was er falsch liest, importiert das Board falsch — /// deshalb steht hier der Rundlauf im Mittelpunkt: Serialisieren und wieder Einlesen /// muss dieselbe Definition ergeben. /// public sealed class TaskFrontmatterTests { [Fact] public void Ein_vollstaendiges_Frontmatter_wird_gelesen() { var text = """ --- id: t-8f3a2c title: NVDA Earnings recherchieren status: todo type: work priority: 4 assignee: "@crawler" when: kind: cron value: "0 7 * * 1-5" tz: Europe/Berlin require_approval: true acceptance: | Aktuelle Zahlen mit Datum und Quelle. In SharedWorkspace/data/nvda.json abgelegt. blocked_by: [t-4b1e, t-9a2c] onlyWhenMarketOpen: false --- Recherchiere die neuesten Quartalszahlen. """; TaskFrontmatter.TryParse(text, out var task, out var error).ShouldBeTrue(error); task.Id.ShouldBe("t-8f3a2c"); task.Title.ShouldBe("NVDA Earnings recherchieren"); task.Status.ShouldBe(TaskItemStatus.Todo); task.Type.ShouldBe(TaskItemType.Work); task.Priority.ShouldBe(4); task.Assignee.ShouldBe("@crawler"); task.When.ShouldNotBeNull(); task.When!.Kind.ShouldBe(TaskWhenKind.Cron); task.When.Value.ShouldBe("0 7 * * 1-5"); task.When.TimeZone.ShouldBe("Europe/Berlin"); task.RequireApproval.ShouldBeTrue(); task.Acceptance.ShouldContain("nvda.json"); task.Acceptance.ShouldContain("Aktuelle Zahlen"); task.BlockedBy.ShouldBe(["t-4b1e", "t-9a2c"]); task.OnlyWhenMarketOpen.ShouldBeFalse(); task.Body.ShouldBe("Recherchiere die neuesten Quartalszahlen."); } [Fact] public void Serialisieren_und_wieder_Einlesen_ergibt_dieselbe_Definition() { var original = new TaskItem { Id = "t-abc123", Title = "Titel mit: Doppelpunkt und # Raute", Status = TaskItemStatus.InReview, Type = TaskItemType.Approval, Priority = 5, Assignee = "@human", When = new TaskWhen { Kind = TaskWhenKind.Every, Value = "30m", TimeZone = "UTC" }, RequireApproval = true, Acceptance = "Zeile eins\nZeile zwei", BlockedBy = ["t-1", "t-2"], OnlyWhenMarketOpen = true, Body = "Mehrzeiliger\nRumpf mit Umlauten äöü und 🦀." }; var text = TaskFrontmatter.Serialize(original); TaskFrontmatter.TryParse(text, out var round, out var error).ShouldBeTrue(error); round.Id.ShouldBe(original.Id); round.Title.ShouldBe(original.Title); round.Status.ShouldBe(original.Status); round.Type.ShouldBe(original.Type); round.Priority.ShouldBe(original.Priority); round.Assignee.ShouldBe(original.Assignee); round.When!.Kind.ShouldBe(TaskWhenKind.Every); round.When.Value.ShouldBe("30m"); round.When.TimeZone.ShouldBe("UTC"); round.RequireApproval.ShouldBeTrue(); round.Acceptance.ShouldBe(original.Acceptance); round.BlockedBy.ShouldBe(["t-1", "t-2"]); round.OnlyWhenMarketOpen.ShouldBeTrue(); round.Body.ShouldBe(original.Body); } [Fact] public void Ein_Assignee_mit_At_ueberlebt_den_Rundlauf() { // In YAML ist ein führendes '@' reserviert — ohne Quotierung ginge es verloren. var task = new TaskItem { Id = "t-1", Title = "x", Assignee = "@new" }; var text = TaskFrontmatter.Serialize(task); TaskFrontmatter.TryParse(text, out var round, out _).ShouldBeTrue(); round.Assignee.ShouldBe("@new"); TaskAssignee.KindOf(round.Assignee).ShouldBe(TaskAssigneeKind.New); } [Fact] public void Ohne_when_ist_es_eine_einmalige_Aufgabe() { var text = """ --- id: t-1 title: Einmalig assignee: "@new" --- Mach das einmal. """; TaskFrontmatter.TryParse(text, out var task, out _).ShouldBeTrue(); task.When.ShouldBeNull(); } [Fact] public void Fehlende_Felder_fallen_auf_sichere_Standards_zurueck() { var text = """ --- id: t-1 title: Minimal --- """; TaskFrontmatter.TryParse(text, out var task, out _).ShouldBeTrue(); task.Status.ShouldBe(TaskItemStatus.Todo); task.Type.ShouldBe(TaskItemType.Work); task.Priority.ShouldBe(3); task.Assignee.ShouldBe(TaskAssignee.Human, "ohne Zuordnung wartet die Aufgabe auf einen Menschen"); task.RequireApproval.ShouldBeFalse(); task.BlockedBy.ShouldBeEmpty(); } [Fact] public void Eine_unbekannte_Terminart_wird_verworfen_statt_geraten() { var text = """ --- id: t-1 title: x when: kind: phantasie value: irgendwas tz: UTC --- """; TaskFrontmatter.TryParse(text, out var task, out _).ShouldBeTrue(); task.When.ShouldBeNull(); } [Fact] public void Fehlt_der_Frontmatter_Block_meldet_der_Parser_das() { TaskFrontmatter.TryParse("Nur Text, kein Frontmatter.", out _, out var error).ShouldBeFalse(); error.ShouldNotBeNull(); } [Fact] public void Ein_nicht_geschlossener_Block_wird_gemeldet() { var text = "---\nid: t-1\ntitle: x\n"; TaskFrontmatter.TryParse(text, out _, out var error).ShouldBeFalse(); error.ShouldNotBeNull(); } [Fact] public void Windows_Zeilenenden_stoeren_nicht() { var text = "---\r\nid: t-1\r\ntitle: Mit CRLF\r\n---\r\nRumpf\r\n"; TaskFrontmatter.TryParse(text, out var task, out var error).ShouldBeTrue(error); task.Title.ShouldBe("Mit CRLF"); task.Body.ShouldBe("Rumpf"); } }