using ClawdDotNet.Core.Accounting; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Engine; using ClawdDotNet.Core.Tests.Infrastructure; using Shouldly; namespace ClawdDotNet.Core.Tests.Budget; /// /// #608: Ein wegen erschoepften Budgets abgelehnter Lauf hinterliess nur eine Log-Warnung - /// kein Beleg, keine Anzeige an der Aufgabe. Im Testbetrieb fehlten deshalb ab 18:20 alle /// Wetterdaten, ohne dass es ausserhalb des Logs zu sehen war. /// public sealed class BudgetsperreSichtbarTests { /// Meldet fuer jeden Agenten einen Tagesverbrauch ueber jeder ueblichen Grenze. private sealed class ErschoepftesBudget : IUsageRepository { public Task RecordAsync(RunUsage usage, CancellationToken ct) => Task.CompletedTask; public Task GetDailyAsync(DateOnly date, string agentId, CancellationToken ct) => Task.FromResult(new DailyUsage(date, 400_000, 20_000, 0.408m, true, 28)); public Task> GetRecentAsync(int limit, CancellationToken ct) => Task.FromResult>([]); public Task PurgeBeforeAsync(DateOnly date, CancellationToken ct) => Task.FromResult(0); } private static (EngineFixture Fixture, InMemoryAuditRepository Audit, AgentConfig Agent) Aufbau() { var audit = new InMemoryAuditRepository(); var fixture = new EngineFixture(audit, usage: new ErschoepftesBudget()); var agent = fixture.AddAgent("wetterdienst"); agent.Budget.DailyCostUsd = 0.40m; return (fixture, audit, agent); } [Fact] public async Task Abgelehnter_frischer_Aufgabenlauf_hinterlaesst_einen_Beleg_an_der_Aufgabe() { var (fixture, audit, agent) = Aufbau(); var result = await fixture.Engine.RunAsync( agent, "Hole die Warnungen", "inst", CancellationToken.None, source: "task", taskId: "t-unwetter"); result.Status.ShouldBe(AgentRunStatus.BudgetExceeded); var beleg = audit.Receipts.ShouldHaveSingleItem(); beleg.TaskId.ShouldBe("t-unwetter"); beleg.AgentId.ShouldBe("wetterdienst"); beleg.Status.ShouldBe(nameof(AgentRunStatus.BudgetExceeded)); beleg.ResultRef.ShouldContain("Budget erschöpft"); beleg.ResultRef.ShouldContain("0,40", Case.Insensitive); beleg.PromptTokens.ShouldBe(0); beleg.CostUsd.ShouldBe(0m); } [Fact] public async Task Abgelehnter_Lauf_im_Chatverlauf_hinterlaesst_ebenfalls_einen_Beleg() { var (fixture, audit, agent) = Aufbau(); var result = await fixture.Engine.ChatAsync( agent, "Digest", "inst", CancellationToken.None, source: "task", taskId: "t-digest"); result.Status.ShouldBe(AgentRunStatus.BudgetExceeded); audit.Receipts.ShouldHaveSingleItem().TaskId.ShouldBe("t-digest"); } [Fact] public async Task Jede_weitere_Ablehnung_des_Tages_steht_als_eigener_Beleg_da() { var (fixture, audit, agent) = Aufbau(); for (var i = 0; i < 3; i++) await fixture.Engine.RunAsync(agent, "Los", "inst", CancellationToken.None, source: "task", taskId: "t-" + i); audit.Receipts.Select(r => r.TaskId).ShouldBe(["t-0", "t-1", "t-2"]); } }