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>
113 lines
4.5 KiB
C#
113 lines
4.5 KiB
C#
using ClawdDotNet.Core.Config;
|
|
using ClawdDotNet.Core.Engine;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Engine;
|
|
|
|
public sealed class LoopGuardTests
|
|
{
|
|
// ═══════════════════════════════════════════════════════════
|
|
// L1 — step counting
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void RecordStep_erlaubt_genau_MaxSteps_Schritte()
|
|
{
|
|
var guard = new LoopGuard(new LoopGuardConfig { MaxSteps = 5 });
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
guard.RecordStep();
|
|
|
|
guard.Steps.ShouldBe(5);
|
|
Should.Throw<LoopLimitExceededException>(() => guard.RecordStep());
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordStep_nennt_das_erreichte_Limit_in_der_Meldung()
|
|
{
|
|
var guard = new LoopGuard(new LoopGuardConfig { MaxSteps = 3 });
|
|
for (var i = 0; i < 3; i++) guard.RecordStep();
|
|
|
|
var ex = Should.Throw<LoopLimitExceededException>(() => guard.RecordStep());
|
|
ex.Message.ShouldContain("3");
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// L2 — the maxTokens semantics (bug B3)
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// Core problem B3: RecordTokens sums up the TotalTokens of every step.
|
|
/// Since every step sends the complete context again, this sum grows
|
|
/// quadratically — a completely normal chat thus aborts after a few steps.
|
|
///
|
|
/// This test describes the DESIRED behavior: an agent with a stable
|
|
/// 20k context must be able to last 10 steps.
|
|
///
|
|
/// Since the separation of maxCumulativeTokens (cost budget) and
|
|
/// maxContextTokens (context size) it runs through.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Ein_stabiler_Kontext_ueberlebt_zehn_Schritte()
|
|
{
|
|
var config = new LoopGuardConfig(); // deliberately the production defaults
|
|
var guard = new LoopGuard(config);
|
|
|
|
Should.NotThrow(() =>
|
|
{
|
|
for (var step = 0; step < 10; step++)
|
|
{
|
|
guard.RecordStep();
|
|
// Realistic: 20k context goes in, ~500 tokens come out.
|
|
guard.RecordTokens(20_000 + 500);
|
|
}
|
|
}, $"Ein Kontext von 20k über 10 Schritte ist normal und darf nicht am " +
|
|
$"Kostenbudget {config.MaxCumulativeTokens} scheitern.");
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordTokens_greift_wenn_das_Kostenbudget_wirklich_erschoepft_ist()
|
|
{
|
|
var guard = new LoopGuard(new LoopGuardConfig { MaxCumulativeTokens = 1_000 });
|
|
|
|
Should.Throw<LoopLimitExceededException>(() => guard.RecordTokens(1_001));
|
|
}
|
|
|
|
[Fact]
|
|
public void Die_Meldung_unterscheidet_Schritt_und_Kostenlimit()
|
|
{
|
|
var stepGuard = new LoopGuard(new LoopGuardConfig { MaxSteps = 1 });
|
|
stepGuard.RecordStep();
|
|
Should.Throw<LoopLimitExceededException>(() => stepGuard.RecordStep())
|
|
.Message.ShouldContain("Schritt-Limit");
|
|
|
|
var tokenGuard = new LoopGuard(new LoopGuardConfig { MaxCumulativeTokens = 100 });
|
|
Should.Throw<LoopLimitExceededException>(() => tokenGuard.RecordTokens(101))
|
|
.Message.ShouldContain("Kostenbudget");
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// L3 — thread safety
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void RecordStep_zaehlt_unter_Parallelzugriff_korrekt()
|
|
{
|
|
var guard = new LoopGuard(new LoopGuardConfig { MaxSteps = int.MaxValue });
|
|
|
|
Parallel.For(0, 1_000, _ => guard.RecordStep());
|
|
|
|
guard.Steps.ShouldBe(1_000);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordTokens_summiert_unter_Parallelzugriff_korrekt()
|
|
{
|
|
var guard = new LoopGuard(new LoopGuardConfig { MaxCumulativeTokens = int.MaxValue });
|
|
|
|
Parallel.For(0, 1_000, _ => guard.RecordTokens(10));
|
|
|
|
guard.Tokens.ShouldBe(10_000);
|
|
}
|
|
}
|