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"]);
}
}