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