Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Tasks/TaskFrontmatterTests.cs
T
RichardandClaude Opus 5.5 77062facc9 Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen
Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt).
Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e):
Properties, Record-Parameter, Enum-Member sowie die Parameter der
Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder
anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:18:43 +02:00

189 lines
6.1 KiB
C#

using ClawdDotNet.Core.Tasks;
using Shouldly;
namespace ClawdDotNet.Core.Tests.Tasks;
/// <summary>
/// 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.
/// </summary>
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");
}
}