Files
ClawdDotNet/tests/ClawdDotNet.App.Tests/Services/TaskAdministrationTests.cs
T
RichardandClaude Opus 5 0b6ea73ec1 Entscheidungen aus dem Testbetrieb umgesetzt (#572): #577 #579 #581 #589 #193 #205 #594 #595 #597 #598 #599 #588
- Engine: leere Antworten nachfassen, Aufgaben enden mit Ergebniszeile (EmptyResponse/Incomplete),
  gleiche Fehlschlaege erkennen (RepeatedFailure), Zeitzeile vor Chatnachrichten,
  Werkzeugergebnisse gerahmt, Vorpruefung vor dem Staging, Belegkosten wie RunUsage
- Staging: gescheiterte Freigaben als Failed; AgentEditor-Aenderungen freigabepflichtig,
  Selbstbearbeitung gesperrt (allowSelfEdit)
- Taskboard: Termine beim Anlegen pruefen, naechster/letzter Lauf, pausieren, Sofortlauf,
  Kostenprognose, Hinweis auf wachsenden Kontext; Jobs-Reiter und /api/jobs entfernt
- Release: Instanzmanager samt Weboberflaeche im Paket, setup.json fragt manager.json ab,
  Token nicht mehr ins systemd-Journal
Version 0.1.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 11:23:10 +02:00

121 lines
4.7 KiB
C#

using ClawdDotNet.App.Services;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.App.Tests.Services;
/// <summary>
/// Das Taskboard fuer die Oberflaechen (#594) mit den Regeln aus #597 (Termin pruefen) und
/// #598 (Hinweis auf wachsenden Kontext).
/// </summary>
public sealed class TaskAdministrationTests : IDisposable
{
private readonly string _dir = Path.Combine(Path.GetTempPath(), "clawd-tests", "tasks-" + Guid.NewGuid().ToString("N")[..8]);
private readonly SqliteStorage _storage;
private readonly TaskboardService _board;
private readonly List<AgentConfig> _agents = [TestHost.Agent("nachrichten"), TestHost.Agent("wetterdienst")];
private readonly TaskAdministration _admin;
public TaskAdministrationTests()
{
_storage = new SqliteStorage(Path.Combine(_dir, "state.db"));
_board = new TaskboardService(new SqliteTaskRepository(_storage), Path.Combine(_dir, "tasks"));
_admin = new TaskAdministration(_board, null, null, null, null, () => _agents, NullLogger.Instance);
}
public void Dispose()
{
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
try { Directory.Delete(_dir, recursive: true); }
catch { /* Aufraeumen ist Nebensache */ }
}
private async Task<TaskItem> Einzige() => (await _board.ListAsync(new TaskQuery(), default)).Single();
[Fact]
public async Task Ein_Termin_der_nie_feuern_kann_wird_nicht_gespeichert()
{
var ergebnis = await _admin.CreateAsync(new TaskInput(
"Nachrichten-Digest", Assignee: "@new:nachrichten",
WhenKind: "cron", WhenValue: "0 7-22/x * * *", TimeZone: "Europe/Berlin"), default);
ergebnis.Ok.ShouldBeFalse();
ergebnis.Message.ShouldContain("keine Zahl");
(await _board.ListAsync(new TaskQuery(), default)).ShouldBeEmpty();
}
[Fact]
public async Task Wiederkehrend_im_Chatverlauf_wird_angelegt_aber_mit_Hinweis()
{
var ergebnis = await _admin.CreateAsync(new TaskInput(
"Nachrichten-Digest", Assignee: "@nachrichten",
WhenKind: "cron", WhenValue: "0 7-22/3 * * *", TimeZone: "Europe/Berlin"), default);
ergebnis.Ok.ShouldBeTrue();
ergebnis.Message.ShouldContain("@new:nachrichten");
}
[Theory]
[InlineData("@new:gibtsnicht")]
[InlineData("@gibtsnicht")]
[InlineData("")]
public async Task Ohne_gueltige_Zuordnung_entsteht_keine_Aufgabe(string assignee)
=> (await _admin.CreateAsync(new TaskInput("X", Assignee: assignee), default)).Ok.ShouldBeFalse();
[Fact]
public async Task Pausieren_und_Aktivieren_setzen_backlog_und_todo()
{
await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *"), default);
var id = (await Einzige()).Id;
(await _admin.SetEnabledAsync(id, false, default)).Ok.ShouldBeTrue();
(await Einzige()).Status.ShouldBe(TaskItemStatus.Backlog);
(await _admin.SetEnabledAsync(id, true, default)).Ok.ShouldBeTrue();
(await Einzige()).Status.ShouldBe(TaskItemStatus.Todo);
}
[Fact]
public async Task Die_Liste_nennt_den_naechsten_Lauf()
{
await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *", TimeZone: "Europe/Berlin"), default);
var zeile = (await _admin.ListAsync(50, default))!.Single();
zeile.NextRunUtc.ShouldNotBeNull();
zeile.Hinweis.ShouldBeNull();
zeile.LastRun.ShouldBeNull();
}
[Fact]
public async Task Bearbeiten_aendert_den_Termin()
{
await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *"), default);
var id = (await Einzige()).Id;
var ergebnis = await _admin.UpdateAsync(id, new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "every", WhenValue: "2h"), default);
ergebnis.Ok.ShouldBeTrue();
(await Einzige()).When!.Kind.ShouldBe(TaskWhenKind.Every);
}
[Fact]
public async Task Sofortlauf_ohne_Scanner_wird_abgelehnt_statt_still_nichts_zu_tun()
{
await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst"), default);
var ergebnis = await _admin.RunNowAsync((await Einzige()).Id, default);
ergebnis.Ok.ShouldBeFalse();
ergebnis.Message.ShouldContain("Scanner");
}
[Fact]
public async Task Ohne_Taskboard_liefert_die_Liste_null()
=> (await new TaskAdministration(null, null, null, null, null, () => _agents, NullLogger.Instance)
.ListAsync(10, default)).ShouldBeNull();
}