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>
This commit is contained in:
Richard
2026-10-04 12:45:03 +02:00
co-authored by Claude Opus 5.5
parent da4ceac7d5
commit 837a634a22
311 changed files with 7238 additions and 7252 deletions
@@ -4,12 +4,12 @@ using ClawdDotNet.Core.Tests.Infrastructure;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Was ein Abbruch mitten im Zug mit dem Gesprächskontext macht.
/// What an abort in the middle of a turn does to the conversation context.
///
/// Der Kontext ist eine geteilte, an Ort und Stelle veränderte Liste. Bricht ein Lauf
/// zwischen dem Werkzeugaufruf und seiner Antwort ab, bleibt eine assistant-Nachricht
/// mit tool_calls stehen, auf die keine Antwort folgt — eine Nachrichtenfolge, die die
/// Schnittstelle nicht annimmt.
/// The context is a shared list, changed in place. If a run aborts
/// between the tool call and its answer, an assistant message
/// with tool_calls remains that no answer follows — a message sequence the
/// interface does not accept.
/// </summary>
public class AbbruchKontextTests
{
@@ -27,7 +27,7 @@ public class AbbruchKontextTests
var lauf = fixture.Engine.ChatAsync(agent, "Los", "inst", CancellationToken.None);
// Warten, bis der Aufruf sicher steht und das Werkzeug läuft.
// Wait until the call is surely in place and the tool is running.
await Task.Delay(300);
fixture.Engine.AbortChat("a1");
@@ -48,8 +48,8 @@ public class AbbruchKontextTests
"Eine solche Folge lehnt die Schnittstelle mit HTTP 400 ab — der Agent ist " +
"danach dauerhaft blockiert.");
// Die Ersatzantwort sagt auch, was passiert ist. Eine leere Antwort wäre formal
// gültig, ließe das Modell aber rätseln, warum sein Aufruf nichts ergab.
// The substitute answer also says what happened. An empty answer would be formally
// valid, but would leave the model puzzling why its call yielded nothing.
var ersatz = kontext.Last(m => m.Role == "tool");
Assert.Contains("abgebrochen", ersatz.Content, StringComparison.OrdinalIgnoreCase);
}
@@ -57,8 +57,8 @@ public class AbbruchKontextTests
[Fact]
public async Task Nach_einem_Abbruch_laesst_sich_der_Agent_wieder_ansprechen()
{
// Der eigentliche Punkt: Nicht die Form des Kontexts zählt, sondern ob der
// Agent danach noch arbeitet. Ein zweiter Chat muss durchlaufen.
// The actual point: not the form of the context counts, but whether the
// agent still works afterwards. A second chat has to run through.
var fixture = new EngineFixture().WithTool(
FakeTool.Slow(TimeSpan.FromSeconds(5), name: "LangsamTool"));
@@ -78,7 +78,7 @@ public class AbbruchKontextTests
Assert.Equal(AgentRunStatus.Completed, zweiter.Status);
Assert.Equal("Zweite Antwort.", zweiter.FinalMessage);
// Und die Folge, die dabei gesendet wurde, war vollständig beantwortet.
// And the sequence that was sent in doing so was completely answered.
var gesendet = fixture.Client.ReceivedRequests[^1].Messages;
var offen = gesendet
.Where(m => m.Role == "assistant" && m.ToolCalls is { Count: > 0 })
@@ -5,19 +5,19 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Regressionstests für Bug B2: _chatContexts[agentId] ist eine geteilte
/// List&lt;ChatMessage&gt;. Nur der Lookup läuft unter Lock — alle Add-Aufrufe im
/// Schleifenkörper sind ungeschützt.
/// Regression tests for bug B2: _chatContexts[agentId] is a shared
/// List&lt;ChatMessage&gt;. Only the lookup runs under lock — all Add calls in the
/// loop body are unprotected.
///
/// Derselbe Agent kann gleichzeitig von mehreren Seiten angestoßen werden:
/// Telegram-/Mail-ToolJob, WebView-Nachricht des Benutzers, send_message eines
/// anderen Agenten. Ergebnis: korrupte Liste, verschränkte Tool-Sequenzen und
/// dadurch HTTP-400-Fehler.
/// The same agent can be triggered from several sides at the same time:
/// Telegram/mail tool job, the user's WebView message, send_message of
/// another agent. Result: corrupt list, interleaved tool sequences and
/// thus HTTP 400 errors.
/// </summary>
public sealed class AgentEngineConcurrencyTests
{
// ═══════════════════════════════════════════════════════════
// E1 — Zwei parallele Chats auf demselben Agenten
// E1 — two parallel chats on the same agent
// ═══════════════════════════════════════════════════════════
[Theory]
@@ -30,8 +30,8 @@ public sealed class AgentEngineConcurrencyTests
.WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(15)));
var agent = fixture.AddAgent("agent-a", "TestTool");
// Jeder Lauf: ein Tool-Schritt, dann eine Textantwort — unabhängig davon,
// wie die beiden Konversationen ineinander verschränkt werden.
// Every run: one tool step, then a text answer — regardless of
// how the two conversations get interleaved.
fixture.Client.RespondsContextually();
await Task.WhenAll(
@@ -43,7 +43,7 @@ public sealed class AgentEngineConcurrencyTests
}
// ═══════════════════════════════════════════════════════════
// E2 — Stresstest
// E2 — stress test
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -65,8 +65,8 @@ public sealed class AgentEngineConcurrencyTests
var context = fixture.Engine.GetChatContext(agent.AgentId);
ContextInvariants.AssertValid(context);
// Jede der 30 Anfragen muss genau einmal im Kontext stehen — nichts verloren,
// nichts doppelt durch verlorene Schreibzugriffe auf die Liste.
// Each of the 30 requests must be in the context exactly once — nothing lost,
// nothing doubled through lost writes to the list.
var userMessages = context.Count(m => m.Role == "user");
userMessages.ShouldBe(30);
}
@@ -87,7 +87,7 @@ public sealed class AgentEngineConcurrencyTests
var first = fixture.Engine.ChatAsync(agent, "Erste", "test-instance", default);
var second = fixture.Engine.ChatAsync(agent, "Zweite", "test-instance", default);
// Warten, bis beide tatsächlich angelaufen sind.
// Wait until both have actually started.
await WaitUntilAsync(() => fixture.Engine.IsRunning(agent.AgentId));
await Task.Delay(50);
@@ -99,29 +99,29 @@ public sealed class AgentEngineConcurrencyTests
results.ShouldAllBe(r => r.Status == AgentRunStatus.Cancelled);
// Entscheidend: _runningChats[agentId] hielt bisher nur EINE CancellationTokenSource —
// der zweite Lauf überschrieb den ersten. AbortChat brach dann nur einen ab, der
// andere lief bis ins Run-Timeout. Ohne diese Zeitprüfung wäre der Test grün,
// obwohl der Abbruch gar nicht gegriffen hat.
// Crucial: _runningChats[agentId] used to hold only ONE CancellationTokenSource —
// the second run overwrote the first. AbortChat then only aborted one, the
// other ran until the run timeout. Without this time check the test would be green
// although the abort did not take effect at all.
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(5),
"AbortChat muss beide Läufe sofort beenden, nicht erst über das Timeout");
}
// ═══════════════════════════════════════════════════════════
// E4 — Gegenprobe: verschiedene Agenten dürfen parallel laufen
// E4 — counter-check: different agents may run in parallel
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Verschiedene_Agenten_laufen_wirklich_parallel()
{
// Wichtige Gegenprobe: Der Fix für B2 darf nicht versehentlich alle Agenten
// gegeneinander sperren.
// Important counter-check: the fix for B2 must not accidentally lock all agents
// against each other.
//
// Bis zum 2026-09-24 antwortete die Attrappe hier nur mit Text: Das langsame
// Werkzeug lief nie, die Läufe waren sofort fertig, und geprüft wurde nur, ob drei
// leere Läufe unter 450 ms bleiben. Das bestand auch mit gegenseitig gesperrten
// Agenten - und schlug unter Last fehl, weil es die Maschine mass statt der Engine.
// Jetzt ruft jeder Agent das Werkzeug wirklich auf, und bewiesen wird die Überlappung.
// Until 2026-09-24 the fake here only answered with text: the slow
// tool never ran, the runs were finished immediately, and all that was checked was whether three
// empty runs stay below 450 ms. That passed even with agents locking each other
// - and failed under load, because it measured the machine instead of the engine.
// Now every agent really calls the tool, and the overlap is proven.
var tool = FakeTool.Slow(TimeSpan.FromMilliseconds(500));
var fixture = new EngineFixture().WithTool(tool);
@@ -8,12 +8,12 @@ using FsCheck.Xunit;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Property-Based Tests: Statt einzelner Beispiele werden hunderte zufällige,
/// aber gültige Konversationen erzeugt und geprüft, dass die Compaction die
/// API-Invarianten in JEDEM Fall erhält.
/// Property-based tests: instead of individual examples, hundreds of random
/// but valid conversations are generated and it is checked that compaction preserves the
/// API invariants in EVERY case.
///
/// Genau diese Grenzfälle — wo die Tail-Grenze zufällig mitten in einen
/// Tool-Zyklus fällt — schreibt von Hand niemand auf. Bug B1 fällt in diese Klasse.
/// Exactly these edge cases — where the tail boundary happens to fall in the middle of a
/// tool cycle — nobody writes down by hand. Bug B1 belongs to this class.
/// </summary>
public sealed class ContextCompactorPropertyTests
{
@@ -30,7 +30,7 @@ public sealed class ContextCompactorPropertyTests
{
var messages = shape.ToMessages();
// Vorbedingung: Der Generator erzeugt nur gültige Ausgangsfolgen.
// Precondition: the generator only produces valid initial sequences.
if (!ContextInvariants.IsValid(messages))
return false.ToProperty().Label("Generator hat eine ungültige Folge erzeugt");
@@ -71,8 +71,8 @@ public sealed class ContextCompactorPropertyTests
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Beschreibt die Form einer Konversation. FsCheck erzeugt davon zufällige
/// Varianten und schrumpft sie im Fehlerfall auf das minimale Gegenbeispiel.
/// Describes the shape of a conversation. FsCheck generates random
/// variants of it and shrinks them in case of failure to the minimal counterexample.
/// </summary>
public sealed record ConversationShape(bool HasSystemPrompt, IReadOnlyList<TurnShape> Turns)
{
@@ -122,10 +122,10 @@ public static class ConversationArbitrary
return Arb.From(gen, Shrink);
}
/// <summary>Im Fehlerfall auf das kleinste Gegenbeispiel reduzieren.</summary>
/// <summary>In case of failure reduce to the smallest counterexample.</summary>
private static IEnumerable<ConversationShape> Shrink(ConversationShape shape)
{
// Weniger Turns
// Fewer turns
for (var i = 0; i < shape.Turns.Count; i++)
{
var reduced = shape.Turns.Where((_, idx) => idx != i).ToList();
@@ -7,19 +7,19 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Regressionstests für Bug B1: Die Compaction behält blind die letzten N Nachrichten.
/// Liegt diese Grenze mitten in einer Tool-Sequenz, entsteht eine tool-Antwort ohne
/// zugehörigen assistant-tool_call — die API lehnt das mit HTTP 400 ab.
/// Regression tests for bug B1: compaction blindly keeps the last N messages.
/// If this boundary lies in the middle of a tool sequence, a tool answer without
/// the corresponding assistant tool_call results — the API rejects that with HTTP 400.
/// </summary>
public sealed class ContextCompactorTests
{
private const string Model = "test/model";
/// <summary>Schwelle so gesetzt, dass Compaction sicher ausgelöst wird.</summary>
/// <summary>Threshold set so that compaction is surely triggered.</summary>
private static LoopGuardConfig AlwaysCompact => new()
{
MaxContextTokens = 1_000,
CompactionThreshold = 0.5 // Schwelle = 500 Tokens
CompactionThreshold = 0.5 // threshold = 500 tokens
};
private static LoopGuardConfig NeverCompact => new()
@@ -36,7 +36,7 @@ public sealed class ContextCompactorTests
}
// ═══════════════════════════════════════════════════════════
// C1/C2 — Die eigentliche Bug-Reproduktion
// C1/C2 — the actual bug reproduction
// ═══════════════════════════════════════════════════════════
[Theory]
@@ -50,14 +50,14 @@ public sealed class ContextCompactorTests
[InlineData(8)]
public async Task Compaction_erhaelt_gueltige_Sequenz_bei_beliebiger_Toolzyklus_Laenge(int toolCallsPerCycle)
{
// Je nach Anzahl der Tool-Calls fällt die "letzte 6 Nachrichten"-Grenze
// an eine andere Stelle im Zyklus. Mindestens eine Variante trifft mitten hinein.
// Depending on the number of tool calls the "last 6 messages" boundary falls
// at a different place in the cycle. At least one variant hits right in the middle.
var (compactor, _) = CreateCompactor();
var messages = Conversation.Start()
.Repeat(times: 4, toolCallsPerCycle: toolCallsPerCycle, resultLength: 200)
.Build();
ContextInvariants.AssertValid(messages); // Vorbedingung: Ausgangslage ist gültig
ContextInvariants.AssertValid(messages); // Precondition: the initial state is valid
await compactor.CompactIfNeededAsync(messages, AlwaysCompact, Model, default);
@@ -67,7 +67,7 @@ public sealed class ContextCompactorTests
[Fact]
public async Task Compaction_trennt_Toolantwort_nie_von_ihrem_Aufruf()
{
// Gezielt konstruiert: Der Tail von 6 Nachrichten beginnt genau bei einer tool-Antwort.
// Constructed on purpose: the tail of 6 messages begins exactly at a tool answer.
var (compactor, _) = CreateCompactor();
var messages = Conversation.Start()
.User("Erste Anfrage")
@@ -81,7 +81,7 @@ public sealed class ContextCompactorTests
ContextInvariants.AssertValid(messages);
// Keine verwaiste tool-Antwort
// No orphaned tool answer
for (var i = 0; i < messages.Count; i++)
{
if (messages[i].Role != "tool") continue;
@@ -126,7 +126,7 @@ public sealed class ContextCompactorTests
}
// ═══════════════════════════════════════════════════════════
// C5/C6 — Fehlerfälle der Zusammenfassung
// C5/C6 — failure cases of the summary
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -158,7 +158,7 @@ public sealed class ContextCompactorTests
}
// ═══════════════════════════════════════════════════════════
// C7/C8 — Tool-Result-Pruning
// C7/C8 — tool result pruning
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -192,7 +192,7 @@ public sealed class ContextCompactorTests
}
// ═══════════════════════════════════════════════════════════
// C9/C10 — Schätzung und Schwelle
// C9/C10 — estimate and threshold
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -213,7 +213,7 @@ public sealed class ContextCompactorTests
[Fact]
public async Task Unterhalb_der_Schwelle_wird_kein_ApiCall_ausgeloest()
{
// Wichtig fürs Budget: Compaction darf nicht unnötig ein Modell anwerfen.
// Important for the budget: compaction must not fire up a model unnecessarily.
var (compactor, client) = CreateCompactor();
var messages = Conversation.Start().Repeat(2).Build();
@@ -6,24 +6,24 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Test A13 der Teststrategie — Regressionstests für B8 (#206).
/// Test A13 of the test strategy — regression tests for B8 (#206).
///
/// <para><c>send_message</c> ist ein synchroner Aufruf: A hält sein Gate, während es
/// auf B wartet. Rief B nun A, wartete B auf ein Gate, das A erst freigibt, wenn B
/// geantwortet hat. Beide standen, bis der Timeout des äußeren Laufs sie auflöste.</para>
/// <para><c>send_message</c> is a synchronous call: A holds its gate while it
/// waits for B. If B then called A, B waited for a gate that A only releases once B
/// has answered. Both stood still until the timeout of the outer run resolved it.</para>
///
/// <para>Die Tests prüfen die drei Zusagen aus <see cref="DelegationChain"/>: kein
/// Agent zweimal in einer Kette, höchstens <c>MaxDelegationDepth</c> Weitergaben, und
/// die Kette gehört zum Aufruf statt zum Agenten.</para>
/// <para>The tests check the three promises from <see cref="DelegationChain"/>: no
/// agent twice in a chain, at most <c>MaxDelegationDepth</c> hand-offs, and
/// the chain belongs to the call instead of to the agent.</para>
///
/// <para>Ein Zeitlimit steht bewusst an jedem Test, der verklemmen könnte: Ohne die
/// Prüfung liefe er nicht rot, sondern gar nicht mehr weiter.</para>
/// <para>A time limit is deliberately on every test that could deadlock: without the
/// check it would not turn red, but not continue at all.</para>
/// </summary>
public sealed class DelegationDepthTests
{
private static readonly TimeSpan Frist = TimeSpan.FromSeconds(20);
/// <summary>Wartet auf den Lauf, scheitert aber statt zu hängen.</summary>
/// <summary>Waits for the run, but fails instead of hanging.</summary>
private static async Task<AgentRunResult> MitFrist(Task<AgentRunResult> lauf)
{
var fertig = await Task.WhenAny(lauf, Task.Delay(Frist));
@@ -32,7 +32,7 @@ public sealed class DelegationDepthTests
}
// ═══════════════════════════════════════════════════════════
// A13 — der gemeldete Fall
// A13 — the reported case
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -41,7 +41,7 @@ public sealed class DelegationDepthTests
var tool = new DelegatingTool(new Dictionary<string, string>
{
["agent-a"] = "agent-b",
["agent-b"] = "agent-a" // zurück zu A — der Zyklus
["agent-b"] = "agent-a" // back to A — the cycle
});
var fixture = new EngineFixture().WithTool(tool);
@@ -55,7 +55,7 @@ public sealed class DelegationDepthTests
result.Status.ShouldBe(AgentRunStatus.Completed);
// Der Rückweg B → A ist abgelehnt worden, der Hinweg A → B nicht.
// The way back B → A was rejected, the way there A → B was not.
var rueckweg = tool.Handoffs.Single(h => h is { From: "agent-b", To: "agent-a" });
rueckweg.Delivered.ShouldBeFalse();
rueckweg.Error.ShouldNotBeNull().ShouldContain("Zyklus");
@@ -67,8 +67,8 @@ public sealed class DelegationDepthTests
[Fact]
public async Task Die_Ablehnung_kommt_als_Ergebnis_zurueck_nicht_als_Wurf()
{
// Der abgewiesene Agent soll weiterarbeiten können — er bekommt einen Grund,
// keinen Abbruch. Sonst führte die Zyklusprüfung nur zu einem anderen Ausfall.
// The rejected agent should be able to continue working — it gets a reason,
// not an abort. Otherwise the cycle check would only lead to a different failure.
var tool = new DelegatingTool(new Dictionary<string, string>
{
["agent-a"] = "agent-b",
@@ -89,13 +89,13 @@ public sealed class DelegationDepthTests
}
// ═══════════════════════════════════════════════════════════
// Die Staffel, die keinen Zyklus bildet
// The relay that does not form a cycle
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Eine_Staffel_endet_an_der_Tiefengrenze()
{
// A → B → C → D wäre die Tiefe 3. Mit MaxDelegationDepth = 2 ist bei C Schluss.
// A → B → C → D would be depth 3. With MaxDelegationDepth = 2 it stops at C.
var tool = new DelegatingTool(new Dictionary<string, string>
{
["agent-a"] = "agent-b",
@@ -149,13 +149,13 @@ public sealed class DelegationDepthTests
}
// ═══════════════════════════════════════════════════════════
// Die Kette gehört zum Aufruf, nicht zum Agenten
// The chain belongs to the call, not to the agent
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Zwei_Laeufe_nacheinander_erben_die_Kette_des_anderen_nicht()
{
// Sonst könnte ein Agent nach einer tiefen Kette dauerhaft „verbraucht" sein.
// Otherwise an agent could be permanently "used up" after a deep chain.
var tool = new DelegatingTool(new Dictionary<string, string> { ["agent-a"] = "agent-b" });
var fixture = new EngineFixture().WithTool(tool);
@@ -172,7 +172,7 @@ public sealed class DelegationDepthTests
}
// ═══════════════════════════════════════════════════════════
// spawn zählt mit
// spawn counts too
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -198,23 +198,23 @@ public sealed class DelegationDepthTests
}
// ═══════════════════════════════════════════════════════════
// #461 — was die Kette grundsätzlich nicht sehen kann
// #461 — what the chain fundamentally cannot see
// ═══════════════════════════════════════════════════════════
/// <summary>
/// Zwei unabhängige Ketten über Kreuz: Lauf 1 startet bei A und beauftragt B, Lauf 2
/// startet zeitgleich bei B und beauftragt A. Keine der beiden Ketten enthält den
/// Agenten der anderen — beide sind für sich zyklusfrei, und trotzdem hält Lauf 1
/// gate(A) und wartet auf gate(B), während Lauf 2 gate(B) hält und auf gate(A) wartet.
/// Two independent chains crossing over: run 1 starts at A and commissions B, run 2
/// starts at the same time at B and commissions A. Neither chain contains the
/// other's agent — both are cycle-free on their own, and yet run 1 holds
/// gate(A) and waits for gate(B), while run 2 holds gate(B) and waits for gate(A).
///
/// <para>Das kann die Delegationskette prinzipiell nicht erkennen; nur eine Frist
/// löst es auf. Ein delegierter Lauf wartet deshalb höchstens
/// <c>DelegationGateTimeout</c> auf ein fremdes Gate und sagt danach ab.</para>
/// <para>The delegation chain fundamentally cannot recognize that; only a deadline
/// resolves it. A delegated run therefore waits at most
/// <c>DelegationGateTimeout</c> for a foreign gate and refuses afterwards.</para>
///
/// <para>Die Pause vor der Weitergabe ist der Kern des Aufbaus: Der Fake-Client
/// antwortet synchron, ohne sie liefe der erste Chat vollständig durch, bevor der
/// zweite beginnt — die Ketten könnten sich gar nicht überschneiden, und der Test
/// wäre grün, ohne etwas zu zeigen.</para>
/// <para>The pause before the hand-off is the core of the setup: the fake client
/// answers synchronously; without it the first chat would run through completely before the
/// second begins — the chains could not overlap at all, and the test
/// would be green without showing anything.</para>
/// </summary>
[Fact]
public async Task Zwei_unabhaengige_Ketten_ueber_Kreuz_verklemmen_sich_nicht()
@@ -241,9 +241,9 @@ public sealed class DelegationDepthTests
fertig.ShouldBe(beide, "Beide Laeufe warten auf das Gate des jeweils anderen (#461).");
// Beide äußeren Läufe kommen zu Ende. Mindestens eine Weitergabe ist an der
// Frist gescheitert — welche, entscheidet die Reihenfolge und ist nicht
// festgelegt; dass beide Läufe zurückkommen, schon.
// Both outer runs come to an end. At least one hand-off failed at the
// deadline — which one is decided by the order and is not
// fixed; that both runs come back is.
(await laufA).Status.ShouldBe(AgentRunStatus.Completed);
(await laufB).Status.ShouldBe(AgentRunStatus.Completed);
tool.Handoffs.ShouldContain(h => !h.Delivered && h.Error!.Contains("belegt"));
@@ -252,9 +252,9 @@ public sealed class DelegationDepthTests
[Fact]
public async Task Ein_Chat_vom_Menschen_wartet_weiter_so_lange_es_dauert()
{
// Die Gegenprobe zur Frist: Sie gilt nur für delegierte Läufe. Ein zweiter
// Chat desselben Agenten reiht sich ein und wird bedient, auch wenn der erste
// länger braucht als die Delegationsfrist.
// The counter-check to the deadline: it only applies to delegated runs. A second
// chat of the same agent queues up and is served, even if the first one
// takes longer than the delegation deadline.
var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(600)));
fixture.Engine.DelegationGateTimeout = TimeSpan.FromMilliseconds(50);
@@ -270,7 +270,7 @@ public sealed class DelegationDepthTests
}
// ═══════════════════════════════════════════════════════════
// Die Kette selbst
// The chain itself
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -285,8 +285,8 @@ public sealed class DelegationDepthTests
[Fact]
public void Append_veraendert_die_Kette_des_Aufrufers_nicht()
{
// Unveränderlich: Zwei Weitergaben aus demselben Lauf dürfen sich nicht
// gegenseitig sehen — sonst zählte die zweite die erste als Vorgänger.
// Immutable: two hand-offs from the same run must not see
// each other — otherwise the second would count the first as a predecessor.
var kette = DelegationChain.Root("a");
var nachB = kette.Append("b");
@@ -7,12 +7,12 @@ using ClawdDotNet.Core.Tests.Infrastructure;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Die Härtung des Harness gegen stillen Verlust: #547 bis #551.
/// The hardening of the harness against silent loss: #547 to #551.
///
/// Was diese fünf Befunde verbindet, ist nicht der Ort im Code, sondern der
/// Fehlermodus — keiner von ihnen erzeugt eine Fehlermeldung. Der Lauf wird fertig,
/// das Ergebnis sieht plausibel aus, und nur die Qualität ist schlechter. Genau
/// deshalb braucht jeder einen Test: Von Hand fällt so etwas nicht auf.
/// What connects these five findings is not the place in the code, but the
/// failure mode — none of them produces an error message. The run finishes,
/// the result looks plausible, and only the quality is worse. Exactly
/// that's why each needs a test: by hand such a thing is not noticed.
/// </summary>
public class HarnessHaertungTests
{
@@ -30,7 +30,7 @@ public class HarnessHaertungTests
await fixture.Engine.RunAsync(agent, "Los", "inst", CancellationToken.None);
// Die zweite Anfrage trägt den Verlauf, den die erste erzeugt hat.
// The second request carries the history the first one produced.
var verlauf = fixture.Client.ReceivedRequests[1].Messages;
var mitAufruf = verlauf.Single(m => m.Role == "assistant" && m.ToolCalls is { Count: > 0 });
@@ -40,8 +40,8 @@ public class HarnessHaertungTests
[Fact]
public async Task Ohne_Begleittext_bleibt_die_Nachricht_ohne_Inhalt()
{
// Gegenprobe: Ein leerer Text darf nicht als "" mitgehen. Manche Anbieter
// lehnen eine assistant-Nachricht mit leerem Inhalt und tool_calls ab.
// Counter-check: an empty text must not go along as "". Some providers
// reject an assistant message with empty content and tool_calls.
var fixture = new EngineFixture().WithTool(FakeTool.Returning("Ergebnis"));
var agent = fixture.AddAgent("a1", "TestTool");
@@ -86,8 +86,8 @@ public class HarnessHaertungTests
Assert.Equal(AgentRunStatus.Truncated, result.Status);
// Das Teilergebnis geht trotzdem mit — es wegzuwerfen wäre schlimmer, als es
// gekennzeichnet weiterzureichen.
// The partial result goes along anyway — throwing it away would be worse than
// passing it on marked.
Assert.Equal("Der Bericht beginnt mit", result.FinalMessage);
}
@@ -127,8 +127,8 @@ public class HarnessHaertungTests
Assert.NotNull(mitAufruf.ReasoningDetails);
// Signatur und Reihenfolge müssen erhalten sein — daran erkennt der Anbieter
// seinen eigenen Block wieder.
// Signature and order must be preserved — by them the provider recognizes
// its own block.
var block = mitAufruf.ReasoningDetails!.Value[0];
Assert.Equal("reasoning.text", block.GetProperty("type").GetString());
Assert.Equal("sha256:abc", block.GetProperty("signature").GetString());
@@ -137,8 +137,8 @@ public class HarnessHaertungTests
[Fact]
public void Der_Denkverlauf_ueberlebt_Serialisierung_und_Rueckweg()
{
// Der Verlauf wird auf die Platte geschrieben und beim Start wieder geladen.
// Überlebt der Denkverlauf das nicht, ist er nach einem Neustart weg.
// The history is written to disk and loaded again at startup.
// If the reasoning does not survive that, it is gone after a restart.
const string details = """
[{"type":"reasoning.encrypted","data":"AAAA","id":"r1","index":0}]
""";
@@ -200,7 +200,7 @@ public class HarnessHaertungTests
public async Task Ohne_Denktiefe_geht_kein_reasoning_Feld_mit()
{
var fixture = new EngineFixture();
var agent = fixture.AddAgent("a1"); // Vorgabe ist "off"
var agent = fixture.AddAgent("a1"); // the default is "off"
fixture.Client.RespondsWithText("ok");
await fixture.Engine.RunAsync(agent, "Los", "inst", CancellationToken.None);
@@ -249,8 +249,8 @@ public class HarnessHaertungTests
[Fact]
public async Task Ohne_gemeldeten_Betrag_bleibt_das_Feld_leer()
{
// Wichtig: nicht 0. Ein als kostenlos verbuchter Lauf wäre schlimmer als
// gar keine Angabe, weil er die Budgetrechnung still verfälscht.
// Important: not 0. A run booked as free would be worse than
// no information at all, because it silently falsifies the budget calculation.
var fixture = new EngineFixture();
var agent = fixture.AddAgent("a1");
fixture.Client.RespondsWithText("ok");
@@ -278,12 +278,12 @@ public class HarnessHaertungTests
await fixture.Engine.RunAsync(agent, "Los", "inst", CancellationToken.None);
// Der belastbare Nachweis: Aufrufe haben sich überlappt.
// The reliable proof: calls overlapped.
//
// Hier stand zusätzlich eine Wanduhr-Prüfung (unter 750 ms, seriell wären es 900).
// Sie schlug am 2026-09-24 mit 927 ms fehl, als die Testprojekte parallel liefen —
// die Zeit misst die Last der Maschine, nicht das Verhalten der Engine. Der
// Höchststand gleichzeitiger Aufrufe beweist dasselbe, ohne zu wackeln.
// Here there was additionally a wall-clock check (below 750 ms, serially it would be 900).
// It failed on 2026-09-24 with 927 ms when the test projects ran in parallel —
// the time measures the load of the machine, not the behavior of the engine. The
// peak of simultaneous calls proves the same without wobbling.
Assert.True(tool.MaxConcurrent > 1,
$"Erwartet: überlappende Aufrufe. Höchststand war {tool.MaxConcurrent}.");
}
@@ -310,8 +310,8 @@ public class HarnessHaertungTests
[Fact]
public async Task Die_Reihenfolge_der_Ergebnisse_folgt_den_Aufrufen()
{
// Der langsamste Aufruf kommt zuerst. Würde die Engine nach Fertigstellung
// einsortieren, stünde er am Ende — und jede tool_call_id wäre falsch zugeordnet.
// The slowest call comes first. If the engine sorted by completion,
// it would be at the end — and every tool_call_id would be assigned wrongly.
var tool = new StaffelTool();
var fixture = new EngineFixture().WithTool(tool);
var agent = fixture.AddAgent("a1", "StaffelTool");
@@ -378,9 +378,9 @@ public class HarnessHaertungTests
}
/// <summary>
/// Werkzeug, das nach einer im Aufruf angegebenen Zeit eine Marke zurückgibt.
/// Damit lässt sich prüfen, ob die Reihenfolge der Ergebnisse den Aufrufen folgt
/// und nicht der Fertigstellung.
/// Tool that returns a marker after a time given in the call.
/// This allows checking whether the order of the results follows the calls
/// and not the completion.
/// </summary>
internal sealed class StaffelTool : ClawdDotNet.Core.Tools.IAgentTool
{
@@ -7,9 +7,9 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// #605: Im Dauerbetrieb lief die Kompaktierung bei jedem Schritt an und blieb wirkungslos -
/// Stufe 1 pruefte mit der rohen statt der kalibrierten Schaetzung, kompaktierte nur knapp
/// unter die Schwelle, und der Zusammenfassungs-Aufruf wurde nicht verbucht.
/// #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
{
@@ -22,9 +22,9 @@ public sealed class KompaktierungAbstandTests
}
/// <summary>
/// Die rohe Schaetzung, wie sie nach Stufe 1 aussaehe - nachgebaut, um Schwellen zu
/// waehlen, an denen alte und neue Pruefung verschieden entscheiden. Werte wie im
/// ContextCompactor: die letzten 6 Nachrichten geschuetzt, Tool-Ergebnisse auf 2.000 Zeichen.
/// 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)
{
@@ -50,14 +50,14 @@ public sealed class KompaktierungAbstandTests
{
var (compactor, client) = Erzeuge();
// Dieses Modell zaehlt rund dreimal so viele Tokens, wie Zeichen/4 vermuten laesst.
// 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);
// Roh liegt Stufe 1 unter der Schwelle, kalibriert deutlich darueber.
// 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 };
@@ -74,7 +74,7 @@ public sealed class KompaktierungAbstandTests
var messages = LangerVerlauf();
var rohDanach = RohNachStufe1(messages);
// Stufe 1 bringt knapp unter die Schwelle (90 %), aber nicht unter das Ziel (55 %).
// 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 };
@@ -7,7 +7,7 @@ namespace ClawdDotNet.Core.Tests.Engine;
public sealed class LoopGuardTests
{
// ═══════════════════════════════════════════════════════════
// L1 — Schrittzählung
// L1 — step counting
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -33,24 +33,24 @@ public sealed class LoopGuardTests
}
// ═══════════════════════════════════════════════════════════
// L2 — Die maxTokens-Semantik (Bug B3)
// L2 — the maxTokens semantics (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.
/// 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.
///
/// Dieser Test beschreibt das GEWÜNSCHTE Verhalten: Ein Agent mit einem stabilen
/// 20k-Kontext muss 10 Schritte durchhalten können.
/// This test describes the DESIRED behavior: an agent with a stable
/// 20k context must be able to last 10 steps.
///
/// Seit der Trennung von maxCumulativeTokens (Kostenbudget) und
/// maxContextTokens (Kontextgröße) läuft er durch.
/// 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(); // bewusst die Produktiv-Defaults
var config = new LoopGuardConfig(); // deliberately the production defaults
var guard = new LoopGuard(config);
Should.NotThrow(() =>
@@ -58,7 +58,7 @@ public sealed class LoopGuardTests
for (var step = 0; step < 10; step++)
{
guard.RecordStep();
// Realistisch: 20k Kontext geht rein, ~500 Tokens kommen raus.
// 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 " +
@@ -87,7 +87,7 @@ public sealed class LoopGuardTests
}
// ═══════════════════════════════════════════════════════════
// L3 — Thread-Sicherheit
// L3 — thread safety
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -5,15 +5,15 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// #203 — die Compaction lief nach dem API-Aufruf.
/// #203 — compaction ran after the API call.
///
/// <para>Sie stützte sich auf die <c>promptTokens</c> der gerade beantworteten Anfrage:
/// Als der Wert vorlag, war der überfüllte Prompt längst abgerechnet, und gekürzt wurde
/// erst für den nächsten Schritt. Genau ein Schritt zu spät — und zwar der teuerste.</para>
/// <para>It relied on the <c>promptTokens</c> of the request just answered:
/// by the time the value was available, the overfull prompt had long been billed, and truncation
/// only happened for the next step. Exactly one step too late — and the most expensive one.</para>
///
/// <para>Die Zusage lautet jetzt: <b>Keine Anfrage geht über die Kontextgrenze hinaus.</b>
/// Geprüft wird vor dem Senden, mit der Schätzung; die gemessenen Werte ziehen die
/// Schätzung hinterher nach.</para>
/// <para>The promise now reads: <b>no request goes beyond the context limit.</b>
/// The check happens before sending, with the estimate; the measured values adjust the
/// estimate afterwards.</para>
/// </summary>
public sealed class ProactiveCompactionTests
{
@@ -26,17 +26,17 @@ public sealed class ProactiveCompactionTests
var fixture = new EngineFixture();
var agent = fixture.AddAgent("agent-a");
// Schwelle: 1000 geschätzte Tokens.
// Threshold: 1000 estimated tokens.
agent.LoopGuard.MaxContextTokens = 2_000;
agent.LoopGuard.CompactionThreshold = 0.5;
agent.LoopGuard.SummaryModel = SummaryModel;
fixture.Client.AlwaysRespondsWithText("Verstanden.");
// Jede Nachricht bringt rund 150 geschaetzte Tokens mit; nach wenigen Runden
// waere der Kontext ohne Eingriff ueber der Grenze. Klein genug, dass auch die
// sechs geschuetzten Tail-Nachrichten zusammen noch unter die Grenze passen -
// sonst koennte selbst eine Kompaktierung sie nicht mehr unterschreiten.
// Every message brings about 150 estimated tokens; after a few rounds
// the context would be above the limit without intervention. Small enough that even the
// six protected tail messages together still fit below the limit -
// otherwise not even a compaction could get under it any more.
for (var i = 0; i < 20; i++)
{
await fixture.Engine.ChatAsync(
@@ -49,9 +49,9 @@ public sealed class ProactiveCompactionTests
anfragenAnDenAgenten.Count.ShouldBe(20);
// Der eigentliche Punkt: nicht "irgendwann wurde gekürzt", sondern "es ging nie
// etwas Überfülltes hinaus". Vorher hing die Prüfung an den promptTokens der
// Antwort — beim Fake immer 100 —, kam also nie über die Schwelle und kürzte nie.
// The actual point: not "at some point it was truncated", but "nothing overfull ever
// went out". Previously the check hung on the promptTokens of the
// answer — always 100 with the fake —, so it never got over the threshold and never truncated.
foreach (var anfrage in anfragenAnDenAgenten)
{
ContextCompactor.EstimateTokens(anfrage.Messages)
@@ -77,12 +77,12 @@ public sealed class ProactiveCompactionTests
agent, $"Frage {i}: " + new string('y', 600), "test-instance", default);
}
// Es wurde überhaupt kompaktiert — sonst sagt der Test darüber nichts aus.
// Compaction happened at all — otherwise the test says nothing about it.
fixture.Client.ReceivedRequests.ShouldContain(r => r.Model == SummaryModel);
}
// ═══════════════════════════════════════════════════════════
// Kalibrierung
// Calibration
// ═══════════════════════════════════════════════════════════
private static ContextCompactor NeuerKompaktierer()
@@ -105,7 +105,7 @@ public sealed class ProactiveCompactionTests
var compactor = NeuerKompaktierer();
// Die API berechnet durchweg doppelt so viel wie geschätzt.
// The API consistently charges twice as much as estimated.
for (var i = 0; i < 20; i++)
compactor.Calibrate("m", 1_000, 2_000);
@@ -119,9 +119,9 @@ public sealed class ProactiveCompactionTests
var roh = ContextCompactor.EstimateTokens(messages);
var compactor = NeuerKompaktierer();
compactor.Calibrate("m", 1_000, 4_000); // einmal weit daneben
compactor.Calibrate("m", 1_000, 4_000); // far off once
// Ein Messwert verschiebt den Faktor nur um sein Gewicht, nicht auf seinen Wert.
// A measurement only shifts the factor by its weight, not to its value.
compactor.EstimateFor("m", messages).ShouldBeLessThan(roh * 2);
}
@@ -135,15 +135,15 @@ public sealed class ProactiveCompactionTests
for (var i = 0; i < 50; i++)
compactor.Calibrate("m", 1, 1_000_000);
// Höchstens das Vierfache — was darüber läge, ist kein Kalibrierungsfall mehr.
// At most four times — anything above that is no longer a calibration case.
compactor.EstimateFor("m", messages).ShouldBeLessThanOrEqualTo(roh * 4);
}
[Fact]
public void Jedes_Modell_wird_fuer_sich_kalibriert()
{
// Verschiedene Tokenizer zerlegen denselben Text verschieden — ein gemeinsamer
// Faktor wäre der Mittelwert von etwas, das nichts miteinander zu tun hat.
// Different tokenizers split the same text differently — a common
// factor would be the average of things that have nothing to do with each other.
var messages = Conversation.Start().Repeat(4).Build();
var roh = ContextCompactor.EstimateTokens(messages);
@@ -162,8 +162,8 @@ public sealed class ProactiveCompactionTests
var roh = ContextCompactor.EstimateTokens(messages);
var compactor = NeuerKompaktierer();
compactor.Calibrate("m", 0, 5_000); // ohne Schätzung kein Verhältnis
compactor.Calibrate("m", 1_000, 0); // ohne Messwert auch nicht
compactor.Calibrate("m", 0, 5_000); // without an estimate no ratio
compactor.Calibrate("m", 1_000, 0); // without a measurement neither
compactor.EstimateFor("m", messages).ShouldBe(roh);
}
@@ -9,7 +9,7 @@ namespace ClawdDotNet.Core.Tests.Engine;
public sealed class PromptCacheTests
{
// ═══════════════════════════════════════════════════════════
// Aktivierung
// Activation
// ═══════════════════════════════════════════════════════════
[Theory]
@@ -26,7 +26,7 @@ public sealed class PromptCacheTests
}
// ═══════════════════════════════════════════════════════════
// Platzierung der Breakpoints
// Placement of the breakpoints
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -54,7 +54,7 @@ public sealed class PromptCacheTests
[Fact]
public void Eine_AssistantNachricht_ohne_Inhalt_traegt_keinen_Breakpoint()
{
// assistant mit tool_calls hat keinen Textinhalt und kann keinen Block tragen.
// assistant with tool_calls has no text content and cannot carry a block.
var messages = Conversation.Start().User("A").Build();
messages.Add(ChatMessage.AssistantWithToolCalls([
new ToolCall { Id = "c1", Function = new ToolCallFunction { Name = "T", Arguments = "{}" } }
@@ -69,7 +69,7 @@ public sealed class PromptCacheTests
[Fact]
public void Es_werden_hoechstens_zwei_Breakpoints_gesetzt()
{
// Anbieter erlauben nur eine begrenzte Zahl — sie dürfen sich nicht ansammeln.
// Providers only allow a limited number — they must not accumulate.
var messages = Conversation.Start().Repeat(10).Build();
PromptCache.ApplyBreakpoints(messages);
@@ -101,14 +101,14 @@ public sealed class PromptCacheTests
}
// ═══════════════════════════════════════════════════════════
// Präfix-Stabilität — der entscheidende Test
// Prefix stability — the decisive test
// ═══════════════════════════════════════════════════════════
/// <summary>
/// Der gecachte Prompt-Abschnitt muss zwischen zwei Schritten zeichengenau identisch
/// sein. Ein einziger Zeitstempel im System-Prompt würde den Cache bei jedem Schritt
/// verwerfen — die Kosten blieben unverändert, ohne dass es irgendwo auffiele.
/// Genau davor schützt dieser Test.
/// The cached prompt section must be identical to the character between two
/// steps. A single timestamp in the system prompt would discard the cache at every step
/// — the costs would stay unchanged without it being noticed anywhere.
/// That is exactly what this test protects against.
/// </summary>
[Fact]
public async Task Der_Praefix_bleibt_ueber_alle_Schritte_zeichengleich()
@@ -118,7 +118,7 @@ public sealed class PromptCacheTests
agent.Model = "anthropic/claude-sonnet-4-5";
agent.PromptCaching = "on";
// Acht Tool-Schritte, dann eine Textantwort.
// Eight tool steps, then a text answer.
for (var i = 0; i < 8; i++)
fixture.Client.RespondsWithToolCall("TestTool");
fixture.Client.RespondsWithText("Fertig");
@@ -127,8 +127,8 @@ public sealed class PromptCacheTests
fixture.Client.ReceivedRequests.Count.ShouldBe(9);
// Die system-Nachricht ist der stabile Präfix — sie muss in jedem Request
// byte-identisch serialisiert werden.
// The system message is the stable prefix — it must be serialized
// byte-identically in every request.
var systemPayloads = fixture.Client.ReceivedRequests
.Select(r => JsonSerializer.Serialize(r.Messages[0]))
.Distinct()
@@ -147,7 +147,7 @@ public sealed class PromptCacheTests
var fixture = new EngineFixture().WithTool(FakeTool.Returning("ok"));
var agent = fixture.AddAgent("agent-nocache", "TestTool");
agent.Model = "openai/gpt-4o";
agent.PromptCaching = "auto"; // bei diesem Modell also aus
agent.PromptCaching = "auto"; // so off for this model
fixture.Client.RespondsWithText("Fertig");
@@ -4,8 +4,8 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Form des ausgehenden Requests. B11/T8: <c>max_tokens</c> muss gesetzt sein, damit ein
/// Modell nicht in einem Schritt sein volles (teuerstes) Ausgabe-Limit ausschöpft.
/// Shape of the outgoing request. B11/T8: <c>max_tokens</c> must be set so that a
/// model does not exhaust its full (most expensive) output limit in one step.
/// </summary>
public sealed class RequestShapeTests
{
@@ -28,7 +28,7 @@ public sealed class RequestShapeTests
{
var fixture = new EngineFixture();
var agent = fixture.AddAgent("agent-a");
agent.LoopGuard.MaxResponseTokens = 0; // Anbieter-Standard
agent.LoopGuard.MaxResponseTokens = 0; // provider default
fixture.Client.RespondsWithText("fertig");
await fixture.Engine.RunAsync(agent, "los", "test-instance", default);
@@ -11,15 +11,15 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Die Entscheidungen aus der Durchsprache der Testbetriebsbefunde (#572, 2026-09-14):
/// #577 (Abschluss), #579 (Vorpruefung), #581 (Wiederholungen), #589 (Zeit im Chat),
/// #193 (Rahmung), #205 (AgentEditor) und #599 (Belegkosten).
/// The decisions from the review of the test operation findings (#572, 2026-09-14):
/// #577 (completion), #579 (pre-check), #581 (repetitions), #589 (time in chat),
/// #193 (framing), #205 (AgentEditor) and #599 (receipt costs).
/// </summary>
public sealed class TestbetriebEntscheidungenTests
{
private static string? Text(ChatMessage m) => m.Content?.ToString();
// ─── #599 Belegkosten ───
// ─── #599 receipt costs ───
[Fact]
public async Task Beleg_verbucht_den_gemeldeten_Betrag_wie_der_Verbrauch()
@@ -37,7 +37,7 @@ public sealed class TestbetriebEntscheidungenTests
beleg.CostIsKnown.ShouldBeTrue();
}
// ─── #581 Wiederholte Fehlschlaege ───
// ─── #581 repeated failures ───
[Fact]
public async Task Derselbe_Fehlschlag_bekommt_beim_zweiten_Mal_einen_Hinweis_und_beendet_beim_dritten_den_Lauf()
@@ -80,7 +80,7 @@ public sealed class TestbetriebEntscheidungenTests
public void Ein_abgerufenes_JSON_mit_Feld_error_ist_kein_Fehlschlag()
=> WerkzeugFehler.TryRead(UntrustedContent.Rahmen("WebFetch", """{"error":"remote"}"""), out _).ShouldBeFalse();
// ─── #577 Abschluss ───
// ─── #577 completion ───
[Fact]
public async Task Leere_Antwort_wird_einmal_nachgefasst()
@@ -176,7 +176,7 @@ public sealed class TestbetriebEntscheidungenTests
.ShouldContain(LaufAbschluss.ErgebnisVorgabe);
}
// ─── #579 Vorpruefung ───
// ─── #579 pre-check ───
private sealed class VorpruefTool(string? grund) : IAgentTool
{
@@ -228,7 +228,7 @@ public sealed class TestbetriebEntscheidungenTests
(await staging.CountPendingAsync(default)).ShouldBe(1);
}
// ─── #193 Rahmung ───
// ─── #193 framing ───
[Fact]
public void Rahmen_entschaerft_eine_eingeschleuste_Schlussmarke()
@@ -267,7 +267,7 @@ public sealed class TestbetriebEntscheidungenTests
Text(fixture.Client.ReceivedRequests[0].Messages[0]).ShouldBe("Du bist agent-a.");
}
// ─── #589 Zeit im Chat ───
// ─── #589 time in chat ───
[Fact]
public async Task Chatnachricht_traegt_die_Zeit_im_Kontext_aber_nicht_im_Verlauf()
@@ -7,12 +7,12 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// Regressionstests fuer die Befunde aus dem Testbetrieb auf der Linux-VM (#572).
/// Regression tests for the findings from test operation on the Linux VM (#572).
/// </summary>
public sealed class TestbetriebRegressionTests
{
// ═══════════════════════════════════════════════════════════
// #578 — Abgebrochene Laeufe tragen ihre bezahlten Token
// #578 — aborted runs carry the tokens they paid for
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -22,7 +22,7 @@ public sealed class TestbetriebRegressionTests
var agent = fixture.AddAgent("agent-grenze", "TestTool");
agent.LoopGuard.MaxSteps = 3;
// Jede Antwort ist ein Werkzeugaufruf mit 100 Prompt- und 20 Completion-Token.
// Every answer is a tool call with 100 prompt and 20 completion tokens.
fixture.Client.AlwaysRespondsWithToolCall("TestTool");
var result = await fixture.Engine.RunAsync(agent, "Arbeite", "test-instance", default);
@@ -66,7 +66,7 @@ public sealed class TestbetriebRegressionTests
}
// ═══════════════════════════════════════════════════════════
// #581 — Unvollstaendiges Argument-JSON
// #581 — incomplete argument JSON
// ═══════════════════════════════════════════════════════════
[Theory]
@@ -82,7 +82,7 @@ public sealed class TestbetriebRegressionTests
var fixture = new EngineFixture().WithTool(FakeTool.Slow(TimeSpan.FromMilliseconds(1)));
var agent = fixture.AddAgent("agent-abgeschnitten", "TestTool");
// Genau die Form aus dem Pruefstand: Das JSON bricht vor dem grossen Wert ab.
// Exactly the shape from the test bench: the JSON breaks off before the large value.
fixture.Client
.RespondsWithToolCall("TestTool", "{\"action\": \"write\", \"path\": \"gross.txt\"")
.RespondsWithText("verstanden");
@@ -98,7 +98,7 @@ public sealed class TestbetriebRegressionTests
}
// ═══════════════════════════════════════════════════════════
// #589 — Zeit und Termin in der Auftragsnachricht
// #589 — time and schedule in the assignment message
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -9,15 +9,15 @@ namespace ClawdDotNet.Core.Tests.Engine;
public sealed class TokenAccountingTests
{
// ═══════════════════════════════════════════════════════════
// B4 — Prompt und Completion getrennt erfassen
// B4 — record prompt and completion separately
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Prompt_und_Completion_werden_getrennt_aufsummiert()
{
// Bisher schätzte die UI 50/50. Real liegt das Verhältnis eher bei 95:5 —
// und da Ausgabe-Tokens ein Vielfaches kosten, war die Kostenanzeige
// um ein Mehrfaches daneben.
// 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");
@@ -43,7 +43,7 @@ public sealed class TokenAccountingTests
var agent = fixture.AddAgent("agent-sum", "TestTool");
fixture.Client
.RespondsWithToolCall("TestTool") // 100 / 20 / 120 laut Fake
.RespondsWithToolCall("TestTool") // 100 / 20 / 120 according to the fake
.RespondsWithToolCall("TestTool")
.RespondsWithText("Fertig");
@@ -56,14 +56,14 @@ public sealed class TokenAccountingTests
}
// ═══════════════════════════════════════════════════════════
// T9 — Cache-Wirkung messbar machen
// T9 — make the cache effect measurable
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Gecachte_Tokens_werden_durchgereicht()
{
// Ohne diese Zahl liesse sich nicht belegen, ob das Prompt-Caching greift —
// ein still wirkungsloser Cache wäre sonst nicht zu bemerken.
// 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");
@@ -89,7 +89,7 @@ public sealed class TokenAccountingTests
}
// ═══════════════════════════════════════════════════════════
// T3 — Compaction läuft mit dem günstigen Modell
// T3 — compaction runs with the cheap model
// ═══════════════════════════════════════════════════════════
[Fact]
@@ -135,14 +135,14 @@ public sealed class TokenAccountingTests
}
// ═══════════════════════════════════════════════════════════
// Nur der wegfallende Teil wird zusammengefasst
// Only the part that goes away is summarized
// ═══════════════════════════════════════════════════════════
[Fact]
public async Task Der_erhaltene_Tail_geht_nicht_in_den_Zusammenfassungs_Aufruf()
{
// Der Tail bleibt wörtlich erhalten — ihn zusätzlich zusammenzufassen
// wäre doppelt bezahlter Kontext.
// 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);
@@ -5,9 +5,9 @@ using Shouldly;
namespace ClawdDotNet.Core.Tests.Engine;
/// <summary>
/// T2 aus der Token-Analyse: Ein einzelnes Tool-Ergebnis konnte den Kontext sprengen.
/// Ein WebFetch mit dem Standardlimit von 512 KB entspricht rund 130.000 Tokens in
/// EINER Antwort — die Compaction greift erst danach, bezahlt ist der Request längst.
/// T2 from the token analysis: a single tool result could blow up the context.
/// A WebFetch with the default limit of 512 KB corresponds to about 130,000 tokens in
/// ONE answer — compaction only applies afterwards, the request has long been paid for.
/// </summary>
public sealed class ToolResultTruncationTests
{
@@ -46,7 +46,7 @@ public sealed class ToolResultTruncationTests
await fixture.Engine.ChatAsync(agent, "Prüfe", "test-instance", default);
var context = fixture.Engine.GetChatContext(agent.AgentId);
// Unveraendert heisst seit #193: ungekuerzt, nur gerahmt.
// Since #193 unchanged means: untruncated, only framed.
context.Single(m => m.Role == "tool").Content.ShouldBe(UntrustedContent.Rahmen("TestTool", klein));
}