Der Stand lag ueber eine Bestandsaufnahme, drei Konzeptpapiere, vier Umsetzungsplaene und zwei Deploymentcenter-Dokumente verteilt - jedes mit eigener Reihenfolge, teils widersprueglich. Alle offenen Punkte daraus sind in docs/Roadmap.md zusammengefuehrt. Aufbau der neuen Roadmap - Statusvokabular: erledigt / beschlossen-offen / Entscheidung noetig / zurueckgestellt / Idee. Damit steht das Geparkte sichtbar drin, statt in einem Konzeptpapier zu verschwinden. - Herkunft-Spalte traegt das alte Kuerzel (S4, K2, B9, T4, F-A1, C1, DC1), damit die archivierten Papiere auffindbar bleiben, ohne sie zu lesen. - Abschnitt 1 Reihenfolge, 2 offene Entscheidungen, 3 die Vorhaben nach Bereich, 4 Ideenspeicher, 5 Chronik des Erledigten, 6 Modell-Einstufung, 7 Herkunftskarte. Was dabei sichtbar wurde - Neun Entscheidungen blockieren Arbeit, ohne dass sie Aufwand kosten - allen voran Matrix oder Rocket.Chat. Sie stehen jetzt gesammelt in Abschnitt 2 statt verstreut in den Diskussionsteilen der Konzepte. - B8 (keine Tiefenbegrenzung bei AgentComm) ist unveraendert offen. Das ist keine Theorie: A haelt sein Gate, waehrend es auf B wartet - ruft B nun A, warten beide bis zum Timeout. Der Testfall A13 dafuer fehlt bis heute. - Die vier Umsetzungsplaene vom 2026-08-05 sind alle unumgesetzt und waren in keiner Roadmap verzeichnet. Archiv Sechs Dokumente ziehen nach docs/archiv/: Bestandsaufnahme, Konzepte Backup/Finanz/Analyse, Linux-Portierung-Analyse, Lizenz-HardwareId-v2 (gegenstandslos - LicenseLabrador ist ersetzt), Deploymentcenter-Review und der 2.4-Integrationsplan. Sie bleiben als Begruendung lesbar, werden aber nicht mehr fortgeschrieben; das README ordnet jedes einzeln ein und warnt, dass ihre Quelltext-Verweise ins Leere gehen koennen. Bauplan bleibt Bauplan Taskboard, Audit, Staging, Memory, Agentenkommunikation, RocketChat und Deploymentcenter-Integration bleiben in docs/ - sie sind die Detailvorgabe fuer die Umsetzung, nicht Vorhabenlisten. Jedes bekommt oben eine Zeile, die seine Rolle und den Umsetzungsstand nennt und auf die Roadmap zeigt. Dasselbe fuer die vier Umsetzungsplaene: die Reihenfolge gilt in der Roadmap, nicht im Plan. Nebenbei repariert: Taskboard-Konzept und Teststrategie verwiesen auf AgentScheduler und ToolJobScheduler, die seit der Scanner-Konsolidierung geloescht sind. Alle Dokument-Verweise in docs/ sind geprueft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
21 KiB
ClawdDotNet — Teststrategie
Ergänzung zur Bestandsaufnahme. Ziel: Fehlerklassen wie B1 (Compaction zerstört tool_call-Paarung) und B2 (Race Condition) strukturell unmöglich machen, statt sie im Betrieb zu entdecken.
Leitgedanke: Nicht Coverage maximieren, sondern die teuren Fehler abfangen. In diesem Projekt sind das drei Kategorien:
- Kontext-Invarianten — ungültige Nachrichtensequenzen kosten API-Fehler mitten im Lauf
- Sicherheitsgrenzen — Pfad-, SQL-, URL- und Empfänger-Prüfungen
- Nebenläufigkeit & Zeit — Scheduler, paralleler Chat-Zugriff, Cron
UI-Layout, Designer-Code und Fremdbibliotheken werden bewusst nicht getestet.
1. Voraussetzung: Testbarkeit herstellen
Vier kleine Refactorings, ohne die die wichtigsten Tests nicht schreibbar sind.
R1 — IChatCompletionClient extrahieren ⚠️ blockiert alles Weitere
AgentEngine.cs:15 und
ContextCompactor.cs:10 hängen an der
konkreten, sealed-Klasse OpenRouterClient. Ohne Interface ist kein einziger Engine-Test möglich.
public interface IChatCompletionClient
{
Task<ChatResponse> CompleteAsync(ChatRequest request, CancellationToken ct);
}
public sealed class OpenRouterClient : IChatCompletionClient, IDisposable { … }
Dazu ein FakeChatClient im Testprojekt, der eine vorprogrammierte Antwortfolge liefert und
alle empfangenen Requests mitschreibt:
internal sealed class FakeChatClient : IChatCompletionClient
{
private readonly Queue<Func<ChatRequest, ChatResponse>> _responses = new();
public List<ChatRequest> ReceivedRequests { get; } = new();
public FakeChatClient RespondsWithText(string text) { … }
public FakeChatClient RespondsWithToolCall(string tool, string argsJson) { … }
public FakeChatClient Throws(Exception ex) { … }
public Task<ChatResponse> CompleteAsync(ChatRequest request, CancellationToken ct)
{
// Tiefe Kopie ablegen — die Engine mutiert die Nachrichtenliste weiter!
ReceivedRequests.Add(DeepClone(request));
…
}
}
Die tiefe Kopie ist wesentlich:
AgentEnginereicht dieselbeList<ChatMessage>-Instanz weiter und verändert sie danach. Ohne Kopie prüfen die Tests den Endzustand statt des tatsächlich gesendeten Requests.
R2 — TimeProvider statt DateTime.Now
.NET 10 bringt TimeProvider mit; FakeTimeProvider steckt in
Microsoft.Extensions.TimeProvider.Testing. Betrifft
CronExpression.cs:53 und den
TaskScanner. (Die ursprünglich hier
genannten AgentScheduler und ToolJobScheduler sind gelöscht — der Scanner hat sie
abgelöst.)
Damit werden Zeitumstellungs- und Langzeit-Tests deterministisch und laufen in Millisekunden statt in Echtzeit.
R3 — IProcessRunner für externe Prozesse
SocialMediaManagerTool.cs:442
startet yt-dlp/ffmpeg direkt. Für den Injection-Test (S2) muss die Argumentbildung ohne
Prozessstart prüfbar sein:
internal static string[] BuildYtDlpArgs(string url, …); // rein, testbar
public interface IProcessRunner { Task<ProcessResult> RunAsync(string exe, string[] args, CancellationToken ct); }
R4 — Dateisystem nicht abstrahieren
Bewusste Entscheidung: Die Path-Traversal-Tests sollen echte Windows-Pfadsemantik prüfen
(.., UNC, Alternate Data Streams, Trailing Dots). Eine IFileSystem-Abstraktion würde genau
die Fehlerklasse verstecken, um die es geht. Stattdessen ein Temp-Verzeichnis-Fixture:
internal sealed class TempWorkspace : IDisposable
{
public string Personal { get; }
public string Shared { get; }
public TempWorkspace() { /* unter Path.GetTempPath()/clawd-tests/{guid} anlegen */ }
public void Dispose() { /* rekursiv löschen, Fehler schlucken */ }
}
2. Projektstruktur & Stack
tests/
ClawdDotNet.Core.Tests/ ← Engine, Compaction, LoopGuard, Cron, Config, State
ClawdDotNet.Tools.Tests/ ← alle Tool-Projekte, v.a. Sicherheitsgrenzen
ClawdDotNet.Integration.Tests/ ← echte Endpunkte, standardmäßig übersprungen
| Paket | Zweck |
|---|---|
xunit.v3 |
Test-Framework |
Shouldly |
Assertions — lesbar, frei nutzbar. (FluentAssertions ist ab v8 für kommerzielle Nutzung kostenpflichtig; falls gewünscht ist AwesomeAssertions ein freier Fork von v7.) |
NSubstitute |
Mocks für ILogger, IStateStore, IAgentTool |
Microsoft.Extensions.TimeProvider.Testing |
FakeTimeProvider |
FsCheck.Xunit |
Property-Based Tests für die Kontext-Invarianten |
Verify.Xunit |
Snapshot-Tests des Request-JSON |
Die drei Projekte in ClawdDotNet.slnx unter einem Ordner /tests/ eintragen.
Nebenbefund: In
ClawdDotNet.slnxfehlen vier Tool-Projekte (AgentComm, AgentSpawn, AgentEditor, SocialMediaManager), obwohl sie insrc/ClawdDotNet.App/ClawdDotNet.App.csprojreferenziert sind. Sie sollten mit aufgenommen werden, sonst laufen sie in der IDE-Solution nicht mit.
3. Die Tests
Prio 1 — Regressionstests für die gefundenen Bugs
Vorgehen: erst den roten Test, dann den Fix. Diese Tests sind der eigentliche Beleg, dass die Bugs behoben sind — und sie bleiben als Wächter erhalten.
3.1 ContextCompactor — Kontext-Invarianten (deckt B1 ab)
Das Herzstück. Ich würde eine wiederverwendbare Prüffunktion bauen und sie überall einsetzen:
internal static class ContextInvariants
{
/// Wirft, wenn die Nachrichtenfolge von der API abgelehnt würde.
public static void AssertValid(IReadOnlyList<ChatMessage> messages)
{
// 1. Höchstens eine system-Nachricht, und zwar an Position 0
// 2. Jede tool-Nachricht hat eine unmittelbar vorausgehende Gruppe,
// deren assistant-Nachricht ein tool_call mit passender Id enthält
// 3. Jede assistant-Nachricht mit N tool_calls hat genau N tool-Antworten
// 4. Keine tool-Nachricht ohne ToolCallId
}
}
| # | Test | Erwartung |
|---|---|---|
| C1 | Compaction bei Tail-Grenze mitten in einer Tool-Sequenz | AssertValid besteht — schlägt heute fehl |
| C2 | Compaction bei assistant(2 tool_calls) → tool → tool an der Grenze |
Beide tool-Antworten bleiben zusammen mit ihrem Aufruf |
| C3 | System-Prompt vorhanden | Bleibt an Position 0 erhalten |
| C4 | Kein System-Prompt vorhanden | Kein leerer/erfundener System-Eintrag |
| C5 | LLM-Zusammenfassung wirft Exception | Nachrichtenliste bleibt unverändert und gültig (kein halb geleerter Zustand) |
| C6 | LLM liefert leeren String zurück | dito |
| C7 | PruneToolResults |
Kürzt nur außerhalb der letzten 6 Nachrichten |
| C8 | PruneToolResults bei Inhalt ≤ 2000 Zeichen |
Bleibt unverändert (kein Marker angehängt) |
| C9 | EstimateTokens |
Monoton: mehr Inhalt ⇒ nie weniger Tokens |
| C10 | Compaction unterhalb der Schwelle | Kein API-Call an den Fake (spart Geld!) |
Property-Based (FsCheck) — der wertvollste Test des ganzen Projekts:
[Property(MaxTest = 500)]
public Property Compaction_erhaelt_immer_eine_gueltige_Sequenz()
{
return Prop.ForAll(ValidConversationGenerator(), // 1..200 gültige Nachrichten
async conversation =>
{
await _compactor.CompactIfNeededAsync(conversation, 999_999, _guard, "m", default);
ContextInvariants.AssertValid(conversation);
});
}
Ein Generator für gültige Konversationen (System, dann abwechselnd User/Assistant, gelegentlich Assistant-mit-N-Tool-Calls gefolgt von genau N Tool-Antworten) findet Grenzfälle, die von Hand niemand schreibt — genau die Klasse, in die B1 fällt.
3.2 LoopGuard (deckt B3 ab)
| # | Test | Erwartung |
|---|---|---|
| L1 | MaxSteps erreicht |
Wirft bei Schritt N+1, nicht bei N |
| L2 | Konstanter 20k-Kontext über 10 Schritte | Bricht nicht ab — definiert die Zielsemantik nach dem Fix |
| L3 | Kostenbudget überschritten | Wirft mit klarer Meldung, welches Limit griff |
| L4 | 100 parallele RecordStep |
Zähler exakt 100 (Interlocked-Korrektheit) |
L2 ist der Test, der den heutigen Fehlentwurf festnagelt: Er schlägt mit den aktuellen Defaults
fehl und geht nach der Trennung von maxCumulativeTokens und maxContextTokens durch.
3.3 AgentEngine — Nebenläufigkeit (deckt B2 ab)
| # | Test | Erwartung |
|---|---|---|
| E1 | Zwei parallele ChatAsync für denselben Agenten |
Kontext danach AssertValid; schlägt heute fehl |
| E2 | 50 parallele ChatAsync, je 2 Tool-Schritte |
Keine Exception, Nachrichtenzahl deterministisch |
| E3 | AbortChat bei zwei laufenden Läufen |
Beide werden abgebrochen |
| E4 | Parallele Läufe für verschiedene Agenten | Laufen wirklich parallel (keine globale Serialisierung) |
E4 ist wichtig als Gegenprobe: Der Fix für B2 darf nicht versehentlich alle Agenten gegeneinander sperren.
3.4 CronExpression & Scheduler (deckt B6, B7 ab)
| # | Test | Erwartung |
|---|---|---|
| R1 | Tabelle: */30 * * * *, 0 3 * * 1-5, 0 0 29 2 *, 15,45 * * * * |
Korrekte nächste Zeitpunkte |
| R2 | Invariante über viele Ausdrücke | GetNextOccurrence(t) > t und Matches(ergebnis) |
| R3 | Ungültig: "* * *", "99 * * * *", "a b c d e", "*/0 * * * *" |
Saubere FormatException — heute wirft teils int.Parse/DivideByZero |
| R4 | Zeitumstellung Oktober, 0 2 * * * |
Feuert genau einmal |
| R5 | Zeitumstellung März, 0 2 * * * |
Feuert trotzdem (wird nicht übersprungen) |
| R6 | 0 3 1 1 * (jährlich) |
Scheduler überlebt Wartezeit > 24 Tage — schlägt heute fehl |
| R7 | Lauf dauert länger als das Intervall | Keine Überlappung, keine Aufstauung |
| R8 | Exception in einem Job-Tick | Scheduler-Schleife läuft weiter, Fehler wird geloggt |
3.5 Kostenerfassung (deckt B4 ab)
| # | Test | Erwartung |
|---|---|---|
| K1 | Modell nicht in der Preistabelle | Sichtbar markiert (nicht stillschweigend 0) |
| K2 | anthropic/claude-sonnet-4-5 (der Default!) |
Liefert einen Preis ≠ 0 |
| K3 | 95k Prompt / 5k Completion | Exakter Betrag gegen handgerechneten Wert — kein 50/50-Split |
Prio 2 — Sicherheitstests
Diese Tests sind gleichzeitig die dauerhafte Dokumentation der Bedrohungsmodelle. Jeder
Angriffsvektor aus der Bestandsaufnahme wird ein [Theory]-Fall.
3.6 FileRW — Pfadsicherheit (deckt S6 ab)
[Theory]
[InlineData("../../../Windows/System32/drivers/etc/hosts")]
[InlineData(@"..\..\evil.txt")]
[InlineData(@"C:\Windows\System32\config\SAM")]
[InlineData(@"\\server\share\evil.txt")]
[InlineData("subdir/../../../outside.txt")]
[InlineData("file.txt:hidden")] // Alternate Data Stream
[InlineData("file.txt.")] // Windows entfernt den Punkt
[InlineData(" .. /../evil.txt")]
public async Task Read_lehnt_Pfade_ausserhalb_des_Workspace_ab(string path) { … }
Plus der Präfix-Angriff, der heute durchgeht:
| # | Test | Erwartung |
|---|---|---|
| F1 | Root …\Workspace, Ziel …\Workspace-Backup\x.txt |
Abgelehnt — schlägt heute fehl |
| F2 | Endungs-Whitelist: .exe, .ps1, .dll |
Abgelehnt |
| F3 | .TXT bei Whitelist .txt |
Erlaubt (Groß/Kleinschreibung) |
| F4 | report.txt.exe |
Abgelehnt |
| F5 | copy mit destinationPath außerhalb des Ziel-Workspace |
Abgelehnt (eigener Pfad, eigener Test!) |
| F6 | sharedAccessLevel: Denied |
Jede shared-Aktion abgelehnt |
| F7 | ReadWrite + delete in shared |
Abgelehnt (nur Admin) |
| F8 | protectedPath stocks/, Schreiben auf existierende stocks/x.json |
Abgelehnt |
| F9 | protectedPath stocks/, Schreiben auf neue stocks/y.json |
Erlaubt |
| F10 | protectedPath stocks/, Pfad stocks-alt/x.json |
Nicht geschützt (Präfix-Verwechslung) |
| F11 | Admin-Level + protectedPath | Umgeht den Schutz |
| F12 | 20 parallele stock_add auf denselben Ticker |
_index.json enthält exakt 20 Einträge, gültiges JSON |
F12 deckt B9 ab und ist ein echter Nebenläufigkeitstest gegen das Dateisystem.
3.7 DatabaseTool — SQL-Whitelist (deckt S1 ab)
Die Bypass-Tests. Alle schlagen heute fehl und definieren die Zielsemantik:
| # | SQL | AccessLevel | Erwartung |
|---|---|---|---|
| D1 | DELETE FROM users -- prices |
ReadWrite | Abgelehnt |
| D2 | SELECT * FROM users WHERE note='prices' |
ReadOnly | Abgelehnt |
| D3 | SELECT * FROM prices; DROP TABLE users |
Admin | Abgelehnt (Mehrfach-Statement) |
| D4 | SELECT * FROM prices |
ReadOnly | Erlaubt |
| D5 | SELECT * FROM prices WHERE note='update' |
ReadOnly | Erlaubt (heutiger False Positive) |
| D6 | UPDATE prices SET x=1 |
ReadOnly | Abgelehnt |
| D7 | UPDATE prices SET x=1 |
ReadWrite | Erlaubt |
| D8 | DROP TABLE prices |
ReadWrite | Abgelehnt |
| D9 | allowedTables fehlt komplett |
egal | Abgelehnt (fail closed) |
| D10 | SELECT mit 10.000 Zeilen Ergebnis |
ReadOnly | Ergebnis wird gekappt |
D4/D5/D7 sind die Gegenproben — sie verhindern, dass der Fix zu restriktiv wird und legitime Abfragen blockiert.
3.8 WebFetch — SSRF (deckt S5 ab)
Mit einem FakeHttpMessageHandler, der Redirects simuliert:
| # | Test | Erwartung |
|---|---|---|
| W1 | Erlaubte Domain → Redirect auf nicht erlaubte | Abgelehnt — schlägt heute fehl |
| W2 | http://169.254.169.254/latest/meta-data/ |
Abgelehnt |
| W3 | http://localhost:8418/ |
Abgelehnt (privates Netz) |
| W4 | http://192.168.178.10/ |
Abgelehnt |
| W5 | file:///C:/Windows/win.ini |
Abgelehnt |
| W6 | Whitelist example.com, URL example.com.attacker.net |
Abgelehnt |
| W7 | Whitelist example.com, URL sub.example.com |
Erlaubt |
| W8 | Host mywww.example.com |
Wird nicht zu myexample.com verstümmelt |
| W9 | Antwort größer als maxResponseKb |
Sauber gekappt, kein OOM |
3.9 Mail — Empfängerprüfung
| # | Test | Erwartung |
|---|---|---|
| M1 | Nicht gelistete Adresse | Abgelehnt |
| M2 | ALLOWED@X.DE bei Whitelist allowed@x.de |
Erlaubt |
| M3 | allowed@x.de, evil@y.de (zwei in einem Feld) |
Abgelehnt |
| M4 | "allowed@x.de" <evil@y.de> (Display-Name-Trick) |
Abgelehnt |
| M5 | allowedRecipients fehlt |
Abgelehnt (fail closed) |
| M6 | read_inbox mit 500 Mails |
Ergebnis gekappt |
3.10 SocialMediaManager — Argument-Injection (deckt S2 ab)
| # | Test | Erwartung |
|---|---|---|
| Y1 | BuildYtDlpArgs("--exec calc.exe https://youtube.com/x") |
Wirft oder quotet — nie als eigene Option |
| Y2 | URL https://evil.com/x |
Abgelehnt (nur YouTube-Domains) |
| Y3 | URL mit Leerzeichen/Anführungszeichen | Erscheint als ein Argument in ArgumentList |
| Y4 | Legitime URL/Handle (@kanal, volle URL) |
Wird korrekt aufgelöst |
3.11 DirectAPI — Key-Leak (deckt S3 ab)
Ein einzelner, sehr einfacher Test mit hohem Wert:
[Fact]
public async Task Ergebnis_enthaelt_niemals_den_ApiKey()
{
const string key = "SECRET-KEY-12345";
var result = await _tool.ExecuteAsync(QuoteFor("NVDA"), ContextWithKey(key), default);
result.Content.ShouldNotContain(key); // schlägt heute fehl
}
Analog für alle Provider (twelvedata, alphavantage, massive) und alle Aktionen — als
[Theory]. Dieselbe Prüfung sollte es zusätzlich für die Log-Ausgabe geben.
Prio 3 — Engine-Verhalten (mit FakeChatClient)
| # | Test | Erwartung |
|---|---|---|
| A1 | Antwort ohne tool_calls | Genau 1 API-Call, FinalMessage korrekt, Status Completed |
| A2 | Antwort mit 2 tool_calls, dann Text | 2 Tools ausgeführt, 2 tool-Nachrichten mit passenden Ids, dann 2. Call |
| A3 | Tool wirft Exception | Lauf läuft weiter, Fehler kommt als tool-Antwort ins Modell |
| A4 | Modell ruft nicht zugewiesenes Tool auf | PermissionGate greift, Fehlermeldung als tool-Antwort, kein Absturz |
| A5 | Modell ruft unbekanntes Tool auf | Saubere Fehlermeldung |
| A6 | Timeout (FakeTimeProvider) |
Status Cancelled |
| A7 | Externe Cancellation | Status Cancelled |
| A8 | Loop-Limit erreicht | Status LoopLimitExceeded |
| A9 | OnRunCompleted feuert in jedem Endzustand genau einmal |
Parametrisiert über alle 5 Status |
| A10 | System-Prompt | Im Request genau einmal enthalten |
| A11 | FullSystemPrompt leer |
Kein leerer system-Eintrag im Request |
| A12 | Tool-Ergebnis über Limit | Gekappt (nach T2-Fix) |
| A13 | send_message A→B→A |
Nach maxDelegationDepth abgebrochen (nach B8-Fix) |
| A14 | spawn auf bereits laufenden Agenten |
Abgelehnt mit Hinweis |
| A15 | Snapshot des Request-JSON (Verify) | Wire-Format bleibt stabil |
A9 und A15 sind Invarianten-Tests mit überproportionalem Nutzen: A9 verhindert, dass beim Erweitern der Fehlerbehandlung die Token-Erfassung stillschweigend ausfällt; A15 schützt beim Caching-Umbau (T1) davor, das Wire-Format versehentlich zu verändern.
Prio 4 — Konfiguration & Persistenz
| # | Bereich | Test |
|---|---|---|
| P1 | ConfigLoader |
Fehlender openRouterApiKey → aussagekräftige Exception |
| P2 | ConfigLoader |
Doppelte agentId → Exception mit beiden Namen |
| P3 | ConfigLoader |
JSON mit Kommentaren und trailing commas wird akzeptiert |
| P4 | ConfigLoader |
Datei fehlt → FileNotFoundException mit Pfad |
| P5 | Chat-Persistenz | Round-Trip History + Context, inkl. tool_calls |
| P6 | Chat-Persistenz | Beide Alt-Formate lesbar (Array und {"messages":[…]}) |
| P7 | Chat-Persistenz | Korrupte JSON-Datei → Warnung, Agent startet trotzdem |
| P8 | Chat-Persistenz | Umlaute/Emoji überleben den Round-Trip (UTF-8) |
| P9 | SqliteStateStore |
Set/Get/Delete Round-Trip; Überschreiben; unbekannter Key → null |
| P10 | SqliteStateStore |
50 parallele Schreibzugriffe → kein database is locked |
| P11 | AgentConfig.FullSystemPrompt |
Reihenfolge Identity → Soul → SystemPrompt |
| P12 | AgentConfig.FullSystemPrompt |
Leere Teile werden ausgelassen, kein doppelter Leerraum |
| P13 | ToolRegistry |
GetForAgent liefert nur zugewiesene Tools |
P10 ist relevant, weil alle Tool-Jobs denselben Store teilen.
Prio 5 — Token & Caching (nach dem Umbau aus T1)
Diese Tests sichern ab, dass die Optimierung tatsächlich greift — ein stillschweigend nicht funktionierender Cache wäre sonst nicht bemerkbar.
| # | Test | Erwartung |
|---|---|---|
| T1 | Cache-Breakpoint | Sitzt am Ende von System-Prompt und Tool-Definitionen |
| T2 | Präfix-Stabilität über 10 Schritte | Serialisierter Präfix ist byte-identisch — der entscheidende Test |
| T3 | cached_tokens |
Wird aus der Antwort gelesen und in AgentRunResult weitergereicht |
| T4 | Compaction | Nutzt summaryModel, nicht agentConfig.Model |
| T5 | Tool-Ergebnis-Kappung | Greift bei allen Tools zentral, nicht nur bei WebFetch |
| T6 | max_tokens |
Ist im Request gesetzt |
T2 ist der Test, der den ganzen Caching-Aufwand absichert: Ein einziger Zeitstempel im System-Prompt würde den Cache bei jedem Schritt invalidieren — die Kosten blieben unverändert, ohne dass es irgendwo auffällt.
4. Arbeitsregeln
Test-first bei Bugfixes. Für jeden Punkt aus der Bestandsaufnahme zuerst den roten Test, dann den Fix. Das beweist, dass der Test den Fehler wirklich fängt — ein nachträglich geschriebener Test bestätigt oft nur die Implementierung.
Keine echten Netzwerkzugriffe in Unit-Tests. Sie sind langsam, flaky und kosten bei OpenRouter echtes Geld. Integrationstests kommen ins separate Projekt und werden markiert:
[Trait("Category", "Integration")]
Standardlauf:
dotnet test --filter "Category!=Integration"
Zeit immer über TimeProvider. Kein Thread.Sleep, kein Task.Delay in Tests — sonst
dauert die Suite Minuten und wird instabil.
Nebenläufigkeitstests wiederholen. Race Conditions treten sporadisch auf; einmaliges Bestehen sagt wenig:
[Theory]
[Repeat(50)]
Benennung durchgängig deutsch, im Stil Methode_Bedingung_Erwartung:
Compaction_bei_Toolsequenz_an_der_Grenze_erhaelt_gueltige_Paarung.
5. Startpaket
Für die erste Sitzung schlage ich ~35 Tests vor, die exakt die fünf Sofort-Punkte abdecken:
| Reihenfolge | Umfang | Deckt ab |
|---|---|---|
1. R1 (IChatCompletionClient) + Testprojekte anlegen |
Refactoring | Voraussetzung |
2. ContextInvariants.AssertValid + C1–C10 + Property-Test |
~12 Tests | B1 |
| 3. L1–L4 | 4 Tests | B3 |
| 4. E1–E4 | 4 Tests | B2 |
| 5. Y1–Y4 | 4 Tests | S2 |
| 6. DirectAPI Key-Leak (alle Provider) | ~6 Tests | S3 |
| 7. A9 + A15 (Invarianten) | 6 Tests | Absicherung für den Umbau |
Danach ist die Basis da, um die Fixes aus der Bestandsaufnahme mit Netz umzusetzen — und die Prio-2-Sicherheitstests lassen sich schrittweise ergänzen, wenn die jeweiligen Tools angefasst werden.
Langfristig sinnvoll: ein Gitea-Actions-Workflow, der bei jedem Push
dotnet test --filter "Category!=Integration" ausführt.