Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Tasks/TaskboardServiceTests.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

164 lines
6.3 KiB
C#

using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tasks;
using Shouldly;
namespace ClawdDotNet.Core.Tests.Tasks;
/// <summary>
/// The split of truth in action: the service keeps the Markdown file (definition) and
/// DB (execution state) in step. Tested against a real file system and real
/// SQLite — both sides are the point.
/// </summary>
public sealed class TaskboardServiceTests : IDisposable
{
private readonly string _directory;
private readonly string _tasksDir;
private readonly SqliteStorage _storage;
private readonly SqliteTaskRepository _repo;
private readonly TaskboardService _board;
public TaskboardServiceTests()
{
_directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
_tasksDir = Path.Combine(_directory, "SharedWorkspace", "tasks");
_storage = new SqliteStorage(Path.Combine(_directory, "state.db"));
_repo = new SqliteTaskRepository(_storage);
_board = new TaskboardService(_repo, _tasksDir);
}
public void Dispose()
{
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* Cleaning up is secondary */ }
}
private static TaskItem Def(string title = "Aufgabe", string assignee = "@new")
=> new() { Title = title, Assignee = assignee, Body = "Tu etwas." };
[Fact]
public async Task Creating_writes_file_and_DB_row_in_one_go()
{
var created = await _board.CreateAsync(Def("NVDA recherchieren"), default);
created.Id.ShouldNotBeNullOrWhiteSpace();
created.Id.ShouldStartWith("t-");
// DB side
(await _repo.GetAsync(created.Id, default)).ShouldNotBeNull();
// File side: exists and can be read back to the same definition
var path = Path.Combine(_tasksDir, created.FileName);
File.Exists(path).ShouldBeTrue();
TaskFrontmatter.TryParse(File.ReadAllText(path), out var fromFile, out _).ShouldBeTrue();
fromFile.Id.ShouldBe(created.Id);
fromFile.Title.ShouldBe("NVDA recherchieren");
fromFile.Assignee.ShouldBe("@new");
}
[Fact]
public async Task The_file_name_is_descriptive()
{
var created = await _board.CreateAsync(Def("NVDA Earnings recherchieren"), default);
created.FileName.ShouldStartWith("nvda-earnings-recherchieren-");
created.FileName.ShouldEndWith(".md");
}
[Fact]
public async Task Changing_updates_file_and_DB()
{
var created = await _board.CreateAsync(Def("Alt"), default);
var updated = await _board.UpdateAsync(created.Id,
c => c with { Title = "Neu", Status = TaskItemStatus.Done }, default);
updated!.Title.ShouldBe("Neu");
updated.Status.ShouldBe(TaskItemStatus.Done);
// Both sides mirror the change
(await _repo.GetAsync(created.Id, default))!.Status.ShouldBe(TaskItemStatus.Done);
TaskFrontmatter.TryParse(
File.ReadAllText(Path.Combine(_tasksDir, created.FileName)), out var fromFile, out _);
fromFile.Title.ShouldBe("Neu");
fromFile.Status.ShouldBe(TaskItemStatus.Done);
}
[Fact]
public async Task A_comment_ends_up_in_the_body()
{
var created = await _board.CreateAsync(Def(), default);
await _board.AddCommentAsync(created.Id, "agent-a", "Erstes Zwischenergebnis.", default);
await _board.AddCommentAsync(created.Id, "agent-b", "Bitte nachbessern.", default);
var body = (await _repo.GetAsync(created.Id, default))!.Body;
body.ShouldContain("Erstes Zwischenergebnis.");
body.ShouldContain("agent-b");
body.ShouldContain("Bitte nachbessern.");
}
[Fact]
public async Task Import_mirrors_existing_files_into_the_DB()
{
Directory.CreateDirectory(_tasksDir);
File.WriteAllText(Path.Combine(_tasksDir, "eins.md"),
"---\nid: t-eins\ntitle: Eins\nassignee: \"@new\"\n---\nMach eins.");
File.WriteAllText(Path.Combine(_tasksDir, "zwei.md"),
"---\nid: t-zwei\ntitle: Zwei\nassignee: \"@new\"\n---\nMach zwei.");
var imported = await _board.ImportAllAsync(default);
imported.ShouldBe(2);
(await _repo.GetAsync("t-eins", default))!.Title.ShouldBe("Eins");
(await _repo.GetAsync("t-zwei", default))!.Title.ShouldBe("Zwei");
}
[Fact]
public async Task A_hand_made_file_without_id_gets_one_and_keeps_it()
{
Directory.CreateDirectory(_tasksDir);
var path = Path.Combine(_tasksDir, "handarbeit.md");
File.WriteAllText(path, "---\ntitle: Von Hand\nassignee: \"@human\"\n---\nManuell angelegt.");
var imported = await _board.ImportFileAsync(path, default);
imported.ShouldNotBeNull();
imported!.Id.ShouldNotBeNullOrWhiteSpace();
// The ID was written back into the file — survives the next start.
TaskFrontmatter.TryParse(File.ReadAllText(path), out var fromFile, out _);
fromFile.Id.ShouldBe(imported.Id);
}
[Fact]
public async Task A_vanished_file_is_archived_instead_of_deleted()
{
var created = await _board.CreateAsync(Def("Vergänglich"), default);
File.Delete(Path.Combine(_tasksDir, created.FileName));
await _board.ImportAllAsync(default);
// The history stays: the row is archived, not gone.
var after = await _repo.GetAsync(created.Id, default);
after.ShouldNotBeNull();
after!.Status.ShouldBe(TaskItemStatus.Archived);
}
[Fact]
public async Task A_re_import_does_not_reset_a_running_status()
{
// The core rule of the split of truth, checked via the service.
var created = await _board.CreateAsync(Def(), default);
await _repo.TryClaimAsync(created.Id, "2026-07-31T07:00:00.0000000Z", "tok",
DateTime.UtcNow, DateTime.UtcNow.AddHours(-1), default);
await _repo.CompleteClaimAsync(created.Id, "tok", TaskItemStatus.Done, DateTime.UtcNow, default);
// The file still says "todo" (that is how it was created) — an import must not
// impose that over the done status reached.
await _board.ImportAllAsync(default);
(await _repo.GetAsync(created.Id, default))!.Status.ShouldBe(TaskItemStatus.Done);
}
}