Files

189 lines
6.2 KiB
C#

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