Files
RichardandClaude Opus 5 740649789e Eine Roadmap statt neun verteilter Listen
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>
2026-08-23 18:31:06 +02:00

21 KiB
Raw Permalink Blame History

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:

  1. Kontext-Invarianten — ungültige Nachrichtensequenzen kosten API-Fehler mitten im Lauf
  2. Sicherheitsgrenzen — Pfad-, SQL-, URL- und Empfänger-Prüfungen
  3. 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: AgentEngine reicht dieselbe List<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.slnx fehlen vier Tool-Projekte (AgentComm, AgentSpawn, AgentEditor, SocialMediaManager), obwohl sie in src/ClawdDotNet.App/ClawdDotNet.App.csproj referenziert 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 + C1C10 + Property-Test ~12 Tests B1
3. L1L4 4 Tests B3
4. E1E4 4 Tests B2
5. Y1Y4 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.