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; /// /// The taskboard for the user interfaces (#594) with the rules from #597 (check schedule) and /// #598 (hint about growing context). /// 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 _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() { ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir); try { Directory.Delete(_dir, recursive: true); } catch { /* Cleaning up is secondary */ } } private async Task Single() => (await _board.ListAsync(new TaskQuery(), default)).Single(); [Fact] public async Task A_schedule_that_can_never_fire_is_not_saved() { var result = await _admin.CreateAsync(new TaskInput( "Nachrichten-Digest", Assignee: "@new:nachrichten", WhenKind: "cron", WhenValue: "0 7-22/x * * *", TimeZone: "Europe/Berlin"), default); result.Ok.ShouldBeFalse(); result.Message.ShouldContain("keine Zahl"); (await _board.ListAsync(new TaskQuery(), default)).ShouldBeEmpty(); } [Fact] public async Task Recurring_in_chat_history_is_created_but_with_a_hint() { var result = await _admin.CreateAsync(new TaskInput( "Nachrichten-Digest", Assignee: "@nachrichten", WhenKind: "cron", WhenValue: "0 7-22/3 * * *", TimeZone: "Europe/Berlin"), default); result.Ok.ShouldBeTrue(); result.Message.ShouldContain("@new:nachrichten"); } [Theory] [InlineData("@new:gibtsnicht")] [InlineData("@gibtsnicht")] [InlineData("")] public async Task Without_a_valid_assignment_no_task_is_created(string assignee) => (await _admin.CreateAsync(new TaskInput("X", Assignee: assignee), default)).Ok.ShouldBeFalse(); [Fact] public async Task Pausing_and_activating_set_backlog_and_todo() { await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *"), default); var id = (await Single()).Id; (await _admin.SetEnabledAsync(id, false, default)).Ok.ShouldBeTrue(); (await Single()).Status.ShouldBe(TaskItemStatus.Backlog); (await _admin.SetEnabledAsync(id, true, default)).Ok.ShouldBeTrue(); (await Single()).Status.ShouldBe(TaskItemStatus.Todo); } [Fact] public async Task The_list_names_the_next_run() { await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *", TimeZone: "Europe/Berlin"), default); var line = (await _admin.ListAsync(50, default))!.Single(); line.NextRunUtc.ShouldNotBeNull(); line.Hinweis.ShouldBeNull(); line.LastRun.ShouldBeNull(); } [Fact] public async Task Editing_changes_the_schedule() { await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "cron", WhenValue: "5 * * * *"), default); var id = (await Single()).Id; var result = await _admin.UpdateAsync(id, new TaskInput("Wetter", Assignee: "@new:wetterdienst", WhenKind: "every", WhenValue: "2h"), default); result.Ok.ShouldBeTrue(); (await Single()).When!.Kind.ShouldBe(TaskWhenKind.Every); } [Fact] public async Task Immediate_run_without_scanner_is_rejected_instead_of_silently_doing_nothing() { await _admin.CreateAsync(new TaskInput("Wetter", Assignee: "@new:wetterdienst"), default); var result = await _admin.RunNowAsync((await Single()).Id, default); result.Ok.ShouldBeFalse(); result.Message.ShouldContain("Scanner"); } [Fact] public async Task Without_taskboard_the_list_returns_null() => (await new TaskAdministration(null, null, null, null, null, () => _agents, NullLogger.Instance) .ListAsync(10, default)).ShouldBeNull(); }