using ClawdDotNet.Core.Engine; using ClawdDotNet.Core.Tasks; using ClawdDotNet.Core.Tests.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Core.Tests.Engine; /// /// Regressionstests fuer die Befunde aus dem Testbetrieb auf der Linux-VM (#572). /// public sealed class TestbetriebRegressionTests { // ═══════════════════════════════════════════════════════════ // #578 — Abgebrochene Laeufe tragen ihre bezahlten Token // ═══════════════════════════════════════════════════════════ [Fact] public async Task RunAsync_an_der_Schrittgrenze_meldet_die_bis_dahin_bezahlten_Token() { var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1))); var agent = fixture.AddAgent("agent-grenze", "TestTool"); agent.LoopGuard.MaxSteps = 3; // Jede Antwort ist ein Werkzeugaufruf mit 100 Prompt- und 20 Completion-Token. fixture.Client.AlwaysRespondsWithToolCall("TestTool"); var result = await fixture.Engine.RunAsync(agent, "Arbeite", "test-instance", default); result.Status.ShouldBe(AgentRunStatus.LoopLimitExceeded); fixture.Client.CallCount.ShouldBe(3); result.PromptTokens.ShouldBe(300, "drei bezahlte Aufrufe - vorher stand hier 0"); result.CompletionTokens.ShouldBe(60); } [Fact] public async Task ChatAsync_an_der_Schrittgrenze_meldet_die_bis_dahin_bezahlten_Token() { var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1))); var agent = fixture.AddAgent("agent-chat-grenze", "TestTool"); agent.LoopGuard.MaxSteps = 2; fixture.Client.AlwaysRespondsWithToolCall("TestTool"); var result = await fixture.Engine.ChatAsync(agent, "Arbeite", "test-instance", default); result.Status.ShouldBe(AgentRunStatus.LoopLimitExceeded); result.PromptTokens.ShouldBe(200); result.CompletionTokens.ShouldBe(40); } [Fact] public async Task RunAsync_mit_Fehler_des_Anbieters_meldet_die_bis_dahin_bezahlten_Token() { var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1))); var agent = fixture.AddAgent("agent-fehler", "TestTool"); fixture.Client .RespondsWithToolCall("TestTool") .Throws(new HttpRequestException("502 vom Anbieter")); var result = await fixture.Engine.RunAsync(agent, "Arbeite", "test-instance", default); result.Status.ShouldBe(AgentRunStatus.Failed); result.PromptTokens.ShouldBe(100); } // ═══════════════════════════════════════════════════════════ // #581 — Unvollstaendiges Argument-JSON // ═══════════════════════════════════════════════════════════ [Theory] [InlineData("{\"action\": \"write\", \"workspace\": \"personal\", \"path\": \"gross.txt\"", false)] [InlineData("{\"action\": \"send_message\", \"targetAgentId\": \"netzpruefer\"", false)] [InlineData("{\"action\": \"list\"}", true)] public void IstVollstaendigesJson_erkennt_abgeschnittene_Argumente(string arguments, bool erwartet) => AgentEngine.IstVollstaendigesJson(arguments).ShouldBe(erwartet); [Fact] public async Task Abgeschnittene_Argumente_erreichen_das_Werkzeug_nicht_und_das_Modell_erfaehrt_warum() { var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1))); var agent = fixture.AddAgent("agent-abgeschnitten", "TestTool"); // Genau die Form aus dem Pruefstand: Das JSON bricht vor dem grossen Wert ab. fixture.Client .RespondsWithToolCall("TestTool", "{\"action\": \"write\", \"path\": \"gross.txt\"") .RespondsWithText("verstanden"); await fixture.Engine.RunAsync(agent, "Schreibe viel", "test-instance", default); var antwortAnsModell = fixture.Client.ReceivedRequests[1].Messages.Last(); antwortAnsModell.Role.ShouldBe("tool"); antwortAnsModell.Content.ShouldNotBeNull(); antwortAnsModell.Content.ShouldContain("kein vollstaendiges JSON"); antwortAnsModell.Content.ShouldContain("mehrere kleinere Aufrufe"); antwortAnsModell.Content.ShouldNotContain("Expected depth"); } // ═══════════════════════════════════════════════════════════ // #589 — Zeit und Termin in der Auftragsnachricht // ═══════════════════════════════════════════════════════════ [Fact] public void Zeitangabe_nennt_Ortszeit_der_Aufgabe_UTC_und_Termin() { var task = new TaskItem { Id = "t-1", Title = "Wetter", When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = "5 * * * *", TimeZone = "Europe/Berlin" } }; var text = EngineTaskDispatcher.Zeitangabe(task, new DateTimeOffset(2026, 9, 14, 13, 5, 0, TimeSpan.Zero)); text.ShouldContain("14.09.2026 15:05 Uhr"); text.ShouldContain("Europe/Berlin"); text.ShouldContain("2026-09-14T13:05Z"); text.ShouldContain("cron \"5 * * * *\""); } [Fact] public void Auftragsnachricht_enthaelt_die_Zeitangabe() { var fixture = new EngineFixture(); var dispatcher = new EngineTaskDispatcher( fixture.Engine, () => fixture.Agents, "test-instance", fixture.Registry, fixture.StateStore, NullLoggerFactory.Instance, new FesteZeit(new DateTimeOffset(2026, 1, 5, 7, 15, 0, TimeSpan.Zero))); var nachricht = dispatcher.BuildMessage(new TaskItem { Id = "t-2", Title = "Morgenbericht", Body = "Schreibe den Bericht.", When = new TaskWhen { Kind = TaskWhenKind.Cron, Value = "15 7 * * *", TimeZone = "Europe/Berlin" } }); nachricht.ShouldStartWith("[Aufgabe t-2] Morgenbericht"); nachricht.ShouldContain("Aktuelle Zeit: 05.01.2026 08:15 Uhr (Europe/Berlin, UTC+01:00)"); nachricht.ShouldContain("Schreibe den Bericht."); } private sealed class FesteZeit(DateTimeOffset jetzt) : TimeProvider { public override DateTimeOffset GetUtcNow() => jetzt; } }