189 lines
6.2 KiB
C#
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");
|
|
}
|
|
}
|