Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Engine/TokenAccountingTests.cs
T
RichardandClaude Opus 5.5 837a634a22 Stufe 4c (#764): Kommentare in C#, WebUI-Skripten, CSS und HTML auf Englisch
Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig
erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner
Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft.
Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch.

WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b
umbenannten Parameter angeglichen.

Dabei behoben:
- 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue"
  gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day.
- Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine
  CSS-Klasse gab - jetzt "success" wie laufende Instanzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:45:03 +02:00

162 lines
6.7 KiB
C#

using ClawdDotNet.Core.Api.Models;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Tests.Infrastructure;
using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
public sealed class TokenAccountingTests
{
// ═══════════════════════════════════════════════════════════
// B4 — record prompt and completion separately
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Prompt_und_Completion_werden_getrennt_aufsummiert()
{
// So far the UI estimated 50/50. In reality the ratio is more like 95:5 —
// and since output tokens cost a multiple, the cost display was
// off by several times.
var fixture = new EngineFixture().WithTool(FakeTool.Returning("ok"));
var agent = fixture.AddAgent("agent-tokens", "TestTool");
fixture.Client
.RespondsWithText("Fertig", new Usage
{
PromptTokens = 9_500,
CompletionTokens = 500,
TotalTokens = 10_000
});
var result = await fixture.Engine.ChatAsync(agent, "Frage", "test-instance", default);
result.PromptTokens.ShouldBe(9_500);
result.CompletionTokens.ShouldBe(500);
result.TokensUsed.ShouldBe(10_000);
}
[Fact]
public async Task Ueber_mehrere_Schritte_wird_korrekt_summiert()
{
var fixture = new EngineFixture().WithTool(FakeTool.Returning("ok"));
var agent = fixture.AddAgent("agent-sum", "TestTool");
fixture.Client
.RespondsWithToolCall("TestTool") // 100 / 20 / 120 according to the fake
.RespondsWithToolCall("TestTool")
.RespondsWithText("Fertig");
var result = await fixture.Engine.ChatAsync(agent, "Frage", "test-instance", default);
result.StepCount.ShouldBe(3);
result.PromptTokens.ShouldBe(300);
result.CompletionTokens.ShouldBe(60);
result.TokensUsed.ShouldBe(360);
}
// ═══════════════════════════════════════════════════════════
// T9 — make the cache effect measurable
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Gecachte_Tokens_werden_durchgereicht()
{
// Without this number it could not be proven whether prompt caching works —
// a silently ineffective cache would otherwise go unnoticed.
var fixture = new EngineFixture();
var agent = fixture.AddAgent("agent-cached");
fixture.Client.RespondsWithText("Fertig", new Usage
{
PromptTokens = 20_000,
CompletionTokens = 300,
TotalTokens = 20_300,
PromptTokensDetails = new PromptTokensDetails { CachedTokens = 18_000 }
});
var result = await fixture.Engine.ChatAsync(agent, "Frage", "test-instance", default);
result.CachedTokens.ShouldBe(18_000);
}
[Fact]
public void Fehlende_Cache_Angaben_ergeben_null_statt_eines_Fehlers()
{
var usage = new Usage { PromptTokens = 100, CompletionTokens = 10, TotalTokens = 110 };
usage.CachedTokens.ShouldBe(0);
}
// ═══════════════════════════════════════════════════════════
// T3 — compaction runs with the cheap model
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Die_Zusammenfassung_nutzt_das_guenstige_Modell()
{
var client = new FakeChatClient().AlwaysRespondsWithText("- Zusammenfassung.");
var compactor = new ContextCompactor(client, TestLogging.Factory);
var guard = new LoopGuardConfig
{
MaxContextTokens = 100,
CompactionThreshold = 0.5,
SummaryModel = "google/gemini-2.5-flash"
};
var messages = Conversation.Start().Repeat(10).Build();
await compactor.CompactIfNeededAsync(messages, guard, "anthropic/claude-opus-4", default);
client.ReceivedRequests.ShouldNotBeEmpty();
client.ReceivedRequests[0].Model.ShouldBe("google/gemini-2.5-flash",
"Zusammenfassen ist anspruchslos und darf nicht das teure Agentenmodell belegen");
}
[Fact]
public async Task Ohne_konfiguriertes_SummaryModel_wird_das_Agentenmodell_verwendet()
{
var client = new FakeChatClient().AlwaysRespondsWithText("- Zusammenfassung.");
var compactor = new ContextCompactor(client, TestLogging.Factory);
var guard = new LoopGuardConfig
{
MaxContextTokens = 100,
CompactionThreshold = 0.5,
SummaryModel = ""
};
var messages = Conversation.Start().Repeat(10).Build();
await compactor.CompactIfNeededAsync(messages, guard, "anthropic/claude-opus-4", default);
client.ReceivedRequests[0].Model.ShouldBe("anthropic/claude-opus-4");
}
// ═══════════════════════════════════════════════════════════
// Only the part that goes away is summarized
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Der_erhaltene_Tail_geht_nicht_in_den_Zusammenfassungs_Aufruf()
{
// The tail is kept verbatim — summarizing it additionally
// would be context paid for twice.
var client = new FakeChatClient().AlwaysRespondsWithText("- Zusammenfassung.");
var compactor = new ContextCompactor(client, TestLogging.Factory);
var guard = new LoopGuardConfig { MaxContextTokens = 100, CompactionThreshold = 0.5 };
var messages = Conversation.Start()
.Repeat(8)
.User("EINZIGARTIGE-LETZTE-NACHRICHT")
.Build();
await compactor.CompactIfNeededAsync(messages, guard, "test/model", default);
var summaryPrompt = client.ReceivedRequests[0].Messages.Last().Content!;
summaryPrompt.ShouldNotContain("EINZIGARTIGE-LETZTE-NACHRICHT");
}
}