Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Engine/KompaktierungAbstandTests.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

114 lines
4.4 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;
/// <summary>
/// #605: In continuous operation compaction started at every step and stayed ineffective -
/// stage 1 checked with the raw instead of the calibrated estimate, compacted only to just
/// below the threshold, and the summarization call was not booked.
/// </summary>
public sealed class KompaktierungAbstandTests
{
private const string Model = "test/model";
private static (ContextCompactor Compactor, FakeChatClient Client) Erzeuge()
{
var client = new FakeChatClient().AlwaysRespondsWithText("- Der Agent hat Daten geprueft.");
return (new ContextCompactor(client, TestLogging.Factory), client);
}
/// <summary>
/// The raw estimate as it would look after stage 1 - rebuilt in order to choose thresholds
/// at which the old and new check decide differently. Values as in the
/// ContextCompactor: the last 6 messages protected, tool results at 2,000 characters.
/// </summary>
private static int RohNachStufe1(List<ChatMessage> messages)
{
var kopie = messages
.Select(m => new ChatMessage { Role = m.Role, Content = m.Content, ToolCallId = m.ToolCallId, ToolCalls = m.ToolCalls })
.ToList();
var schutz = Math.Max(0, kopie.Count - 6);
for (var i = 0; i < schutz; i++)
{
if (kopie[i].Role == "tool" && kopie[i].Content is { Length: > 2_000 } inhalt)
kopie[i].Content = inhalt[..2_000] + "\n\n[... Ergebnis gekürzt ...]";
}
return ContextCompactor.EstimateTokens(kopie);
}
private static List<ChatMessage> LangerVerlauf()
=> Conversation.Start().Repeat(times: 6, toolCallsPerCycle: 1, resultLength: 8_000).Build();
[Fact]
public async Task Nach_Stufe_1_entscheidet_die_kalibrierte_Schaetzung()
{
var (compactor, client) = Erzeuge();
// This model counts about three times as many tokens as chars/4 suggests.
for (var i = 0; i < 12; i++)
compactor.Calibrate(Model, 1_000, 3_000);
var messages = LangerVerlauf();
var rohDanach = RohNachStufe1(messages);
// Raw, stage 1 lies below the threshold, calibrated clearly above it.
var schwelle = (int)(rohDanach * 1.5);
var guard = new LoopGuardConfig { MaxContextTokens = schwelle * 2, CompactionThreshold = 0.5 };
await compactor.CompactIfNeededAsync(messages, guard, Model, default);
client.CallCount.ShouldBe(1, "die rohe Zahl meldete faelschlich 'unter der Schwelle' und Stufe 2 lief nie");
ContextInvariants.AssertValid(messages);
}
[Fact]
public async Task Knapp_unter_der_Schwelle_reicht_nicht_es_wird_mit_Abstand_kompaktiert()
{
var (compactor, client) = Erzeuge();
var messages = LangerVerlauf();
var rohDanach = RohNachStufe1(messages);
// Stage 1 brings it just below the threshold (90 %), but not below the target (55 %).
var schwelle = (int)(rohDanach * 1.2);
var guard = new LoopGuardConfig { MaxContextTokens = (int)(schwelle / 0.9), CompactionThreshold = 0.9 };
await compactor.CompactIfNeededAsync(messages, guard, Model, default);
client.CallCount.ShouldBe(1, "knapp unter der Schwelle waere beim naechsten Schritt wieder darueber");
ContextInvariants.AssertValid(messages);
}
[Fact]
public async Task Der_Zusammenfassungs_Aufruf_wird_verbucht()
{
var (compactor, _) = Erzeuge();
var gebucht = new List<Usage>();
var messages = Conversation.Start().Repeat(15).Build();
await compactor.CompactIfNeededAsync(
messages, new LoopGuardConfig { MaxContextTokens = 1_000, CompactionThreshold = 0.5 }, Model, default, gebucht.Add);
gebucht.ShouldHaveSingleItem().PromptTokens.ShouldBe(100);
}
[Fact]
public async Task Unter_der_Schwelle_wird_nichts_verbucht()
{
var (compactor, client) = Erzeuge();
var gebucht = new List<Usage>();
await compactor.CompactIfNeededAsync(
Conversation.Start().Repeat(2).Build(),
new LoopGuardConfig { MaxContextTokens = 10_000_000, CompactionThreshold = 0.9 }, Model, default, gebucht.Add);
gebucht.ShouldBeEmpty();
client.CallCount.ShouldBe(0);
}
}