Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft. Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch. WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b umbenannten Parameter angeglichen. Dabei behoben: - 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue" gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day. - Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine CSS-Klasse gab - jetzt "success" wie laufende Instanzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.3 KiB
C#
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 Anlegen_erzeugt_Datei_und_DB_Zeile_in_einem_Zug()
|
|
{
|
|
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 Der_Dateiname_ist_beschreibend()
|
|
{
|
|
var created = await _board.CreateAsync(Def("NVDA Earnings recherchieren"), default);
|
|
created.FileName.ShouldStartWith("nvda-earnings-recherchieren-");
|
|
created.FileName.ShouldEndWith(".md");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Aendern_schreibt_Datei_und_DB_fort()
|
|
{
|
|
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 Ein_Kommentar_landet_im_Rumpf()
|
|
{
|
|
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_spiegelt_vorhandene_Dateien_in_die_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 Eine_von_Hand_angelegte_Datei_ohne_Id_bekommt_eine_und_behaelt_sie()
|
|
{
|
|
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 Eine_verschwundene_Datei_wird_archiviert_statt_geloescht()
|
|
{
|
|
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 Ein_Re_Import_setzt_einen_laufenden_Status_nicht_zurueck()
|
|
{
|
|
// 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);
|
|
}
|
|
}
|