using ClawdDotNet.Core.Tasks; using Shouldly; namespace ClawdDotNet.Core.Tests.Tasks; /// /// The frontmatter parser is the bridge between the human/agent-readable /// task file and the DB state. What it reads wrongly, the board imports wrongly — /// that's why the round trip is at the center here: serializing and reading back /// must yield the same definition. /// public sealed class TaskFrontmatterTests { [Fact] public void A_complete_frontmatter_is_read() { 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 Serialising_and_reading_back_gives_the_same_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 An_assignee_with_at_survives_the_round_trip() { // In YAML a leading '@' is reserved — without quoting it would get lost. 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 Without_when_it_is_a_one_off_task() { 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 Missing_fields_fall_back_to_safe_defaults() { 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 An_unknown_schedule_kind_is_discarded_instead_of_guessed() { 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 If_the_frontmatter_block_is_missing_the_parser_reports_it() { TaskFrontmatter.TryParse("Nur Text, kein Frontmatter.", out _, out var error).ShouldBeFalse(); error.ShouldNotBeNull(); } [Fact] public void An_unclosed_block_is_reported() { var text = "---\nid: t-1\ntitle: x\n"; TaskFrontmatter.TryParse(text, out _, out var error).ShouldBeFalse(); error.ShouldNotBeNull(); } [Fact] public void Windows_line_endings_do_no_harm() { 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"); } }