Testfundament aufbauen und Bestandsaufnahme dokumentieren
IChatCompletionClient aus OpenRouterClient extrahiert, damit AgentEngine und ContextCompactor ohne echte API-Aufrufe testbar sind. Neues Testprojekt tests/ClawdDotNet.Core.Tests (xUnit, Shouldly, NSubstitute, FsCheck) mit: - FakeChatClient (programmierbare Antwortfolgen, Deep-Copy der Requests) - ContextInvariants (prueft die API-Regeln fuer tool_call-Paarung) - Conversation-Builder fuer gueltige Testkonversationen - 26 Tests: Compaction, LoopGuard, 2 Property-Tests 10 Tests sind bewusst rot — sie reproduzieren die Bugs B1, B3 und B14 aus der Bestandsaufnahme und werden mit den Fixes gruen. Ausserdem: fehlende Tool-Projekte in slnx ergaenzt, Test-Pakete im packageSourceMapping der NuGet.Config eingetragen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
92e50d3ac4
commit
667cecce25
@@ -0,0 +1,100 @@
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Engine;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Core.Tests.Engine;
|
||||
|
||||
public sealed class LoopGuardTests
|
||||
{
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// L1 — Schrittzählung
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[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 — Die maxTokens-Semantik (Bug B3)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Kernproblem B3: RecordTokens summiert die TotalTokens jedes Schritts.
|
||||
/// Da jeder Schritt den kompletten Kontext erneut sendet, wächst diese Summe
|
||||
/// quadratisch — ein völlig normaler Chat bricht dadurch nach wenigen Schritten ab.
|
||||
///
|
||||
/// Dieser Test beschreibt das GEWÜNSCHTE Verhalten: Ein Agent mit einem stabilen
|
||||
/// 20k-Kontext muss 10 Schritte durchhalten können.
|
||||
///
|
||||
/// Er schlägt mit den aktuellen Defaults fehl und wird grün, sobald
|
||||
/// maxCumulativeTokens (Kostenbudget) und maxContextTokens (Kontextgröße)
|
||||
/// getrennt sind.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Ein_stabiler_Kontext_ueberlebt_zehn_Schritte()
|
||||
{
|
||||
var config = new LoopGuardConfig(); // bewusst die Produktiv-Defaults
|
||||
var guard = new LoopGuard(config);
|
||||
|
||||
Should.NotThrow(() =>
|
||||
{
|
||||
for (var step = 0; step < 10; step++)
|
||||
{
|
||||
guard.RecordStep();
|
||||
// Realistisch: 20k Kontext geht rein, ~500 Tokens kommen raus.
|
||||
guard.RecordTokens(20_000 + 500);
|
||||
}
|
||||
}, $"Ein Kontext von 20k über 10 Schritte ist normal und darf nicht am " +
|
||||
$"Limit maxTokens={config.MaxTokens} scheitern.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordTokens_greift_wenn_das_Kostenbudget_wirklich_erschoepft_ist()
|
||||
{
|
||||
var guard = new LoopGuard(new LoopGuardConfig { MaxTokens = 1_000 });
|
||||
|
||||
Should.Throw<LoopLimitExceededException>(() => guard.RecordTokens(1_001));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// L3 — Thread-Sicherheit
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[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 { MaxTokens = int.MaxValue });
|
||||
|
||||
Parallel.For(0, 1_000, _ => guard.RecordTokens(10));
|
||||
|
||||
guard.Tokens.ShouldBe(10_000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user