using ClawdDotNet.Core.Tasks; using Shouldly; namespace ClawdDotNet.Core.Tests.Tasks; /// /// Termine pruefen, naechster Lauf und Laeufe je Tag (#594, #595, #597) sowie der Hinweis /// auf wachsenden Kontext (#598). /// public sealed class TaskboardAusbauTests { private static DateTime Utc(int y, int mo, int d, int h, int mi) => new(y, mo, d, h, mi, 0, DateTimeKind.Utc); private static TaskWhen Cron(string value, string tz = "Europe/Berlin") => new() { Kind = TaskWhenKind.Cron, Value = value, TimeZone = tz }; // ─── #597 Termin pruefen ─── [Fact] public void Gueltiger_Termin_mit_Bereich_und_Schritt_besteht_die_Pruefung() => TaskSchedule.ValidateWhen(Cron("0 7-22/3 * * *")).ShouldBeNull(); [Fact] public void Kaputter_Cron_Ausdruck_wird_benannt() => TaskSchedule.ValidateWhen(Cron("0 7-22/x * * *")).ShouldNotBeNull().ShouldContain("'x' ist keine Zahl"); [Fact] public void Unbekannte_Zeitzone_wird_benannt() => TaskSchedule.ValidateWhen(Cron("0 9 * * *", "Phantasie/Ort")).ShouldNotBeNull().ShouldContain("Phantasie/Ort"); [Theory] [InlineData(TaskWhenKind.Every, "30m", true)] [InlineData(TaskWhenKind.Every, "halbstündlich", false)] [InlineData(TaskWhenKind.At, "2026-09-15T08:00", true)] [InlineData(TaskWhenKind.At, "morgen früh", false)] public void Intervall_und_Zeitpunkt_werden_geprueft(TaskWhenKind kind, string value, bool gueltig) => (TaskSchedule.ValidateWhen(new TaskWhen { Kind = kind, Value = value, TimeZone = "Europe/Berlin" }) is null) .ShouldBe(gueltig); // ─── #594 naechster Lauf ─── [Fact] public void Naechster_Lauf_eines_Cron_Termins_in_Berliner_Zeit() { var task = new TaskItem { Id = "t-7e0bbe", When = Cron("0 7-22/3 * * *") }; // 12:30 Berlin (MESZ) -> naechster Termin 13:00 Berlin = 11:00 UTC TaskSchedule.NextOccurrenceUtc(task, Utc(2026, 9, 14, 10, 30)).ShouldBe(Utc(2026, 9, 14, 11, 0)); } [Fact] public void Naechster_Lauf_eines_Intervalls_zaehlt_ab_dem_letzten_Lauf() { var task = new TaskItem { Id = "t-1", When = new TaskWhen { Kind = TaskWhenKind.Every, Value = "2h", TimeZone = "" }, LastOccurrence = "2026-09-14T09:00:00.0000000Z" }; TaskSchedule.NextOccurrenceUtc(task, Utc(2026, 9, 14, 10, 0)).ShouldBe(Utc(2026, 9, 14, 11, 0)); } [Fact] public void Einmalige_Aufgabe_steht_an_bis_sie_lief() { var jetzt = Utc(2026, 9, 14, 10, 0); TaskSchedule.NextOccurrenceUtc(new TaskItem { Id = "t-1" }, jetzt).ShouldBe(jetzt); TaskSchedule.NextOccurrenceUtc(new TaskItem { Id = "t-1", LastOccurrence = "x" }, jetzt).ShouldBeNull(); } // ─── #595 Laeufe je Tag ─── [Fact] public void Laeufe_je_Tag_aus_Cron_und_Intervall() { var jetzt = Utc(2026, 9, 14, 10, 0); TaskSchedule.RunsPerDay(new TaskItem { When = Cron("0 7-22/3 * * *") }, jetzt).ShouldBe(6); TaskSchedule.RunsPerDay(new TaskItem { When = Cron("5 * * * *") }, jetzt).ShouldBe(24); TaskSchedule.RunsPerDay(new TaskItem { When = new TaskWhen { Kind = TaskWhenKind.Every, Value = "30m" } }, jetzt).ShouldBe(48); TaskSchedule.RunsPerDay(new TaskItem(), jetzt).ShouldBe(0); } // ─── #598 Hinweis ─── [Fact] public void Wiederkehrend_im_Chatverlauf_bekommt_den_Hinweis() => TaskHinweise.WachsenderKontext(new TaskItem { Assignee = "@nachrichten", When = Cron("0 7-22/3 * * *") }) .ShouldNotBeNull().ShouldContain("@new:nachrichten"); [Theory] [InlineData("@new:nachrichten", true)] [InlineData("@nachrichten", false)] [InlineData("@human", true)] public void Kein_Hinweis_fuer_frische_Laeufe_einmalige_und_menschliche_Aufgaben(string assignee, bool wiederkehrend) => TaskHinweise.WachsenderKontext(new TaskItem { Assignee = assignee, When = wiederkehrend ? Cron("5 * * * *") : null }).ShouldBeNull(); [Fact] public void Kein_Hinweis_fuer_Werkzeugjobs() => TaskHinweise.WachsenderKontext(new TaskItem { Assignee = "@telegram", Type = TaskItemType.ToolJob, When = Cron("* * * * *") }).ShouldBeNull(); }