Im März 2026 begann die Arbeit an einem Testsystem für den Peakboard Designer, eine mit Windows Presentation Foundation (WPF) entwickelte Anwendung. Ein KI-Agent, Claude Code, bedient die Benutzeroberfläche (UI) über Windows UI Automation wie ein menschlicher Tester, beurteilt das Ergebnis und meldet jeden gefundenen Fehler. Alles, was wiederholt werden muss, läuft ohne Modell und wird von einem separaten Runner als JSON-Testskript ausgeführt.
Der Peakboard Designer ist der Editor für Peakboard-Boards und bietet eine Arbeitsfläche, eine Toolbox, Eigenschaftsdialoge sowie ein eigenes Dateiformat für Projekte. Der folgende Screenshot zeigt ein Board mit sechs Stationen auf der Arbeitsfläche sowie rechts die Eigenschaften eines Text-Steuerelements.

Das Testsystem ist ein internes Werkzeug zum Testen des Designers und nicht Teil des Produkts.
Die Idee
Agentisch heißt, dass das Modell ein Tool aufruft, das Ergebnis liest und den nächsten Schritt selbst festlegt. Das Modell übernimmt alles, was Verständnis erfordert, wie das Erkunden eines unbekannten Dialogs, die Wahl des nächsten Klicks, die Prüfung eines Werts sowie das Schreiben des Fehlerberichts. Für alles, was jedes Mal auf dieselbe Weise wiederholt werden muss (z. B. eine nächtliche Regressionssuite), wird ein Runner ohne Modell eingesetzt.
Im Code des Systems existiert kein Modellaufruf. Neben Claude Code, das als Prozess läuft und einen Prompt sowie mehrere Tools erhält, besteht das System nur aus .NET, Markdown und JSON.
Das folgende Diagramm zeigt die Teile und die Verbindungen zwischen ihnen.
Zunächst schreibt ein Tester eine Anfrage in Slack, woraufhin ein Bot Claude Code mit einem Command und der Anfrage als Text startet. Eine Karte auf einem Kanban-Board startet Claude Code ebenfalls. Anschließend startet Claude Code den MCP-Server als Kindprozess, liest die eigenen Notizen und bedient die Anwendung über die Tools des Servers.
Für einen später zu wiederholenden Ablauf schreibt Claude Code ein JSON-Testskript und startet den Runner. Der Bericht sowie die Befunde werden an Slack gesendet und die neuen Erkenntnisse vom Agenten in den Notizen ergänzt.
Der MCP-Server
Das Model Context Protocol (MCP) ist der offene Standard, über den ein Modell Tools in einem anderen Programm aufruft. Der Server ist ein Konsolenprozess auf Basis von .NET 8, der von Claude Code als Kindprozess gestartet wird. Über stdin werden JSON-RPC-Nachrichten an den Server gesendet und die Antworten über stdout mit jeweils einer Nachricht je Zeile zurückgegeben.
Die Nachrichtenschleife für die drei Methoden initialize, tools/list und tools/call wurde ohne Software Development Kit (SDK) von Hand geschrieben, da das Protokoll klein ist. Der Server stellt mehr als 30 Tools in vier Gruppen bereit.
Die Lese-Tools liefern den UI-Baum, ein einzelnes Element, dessen Text oder eine Eigenschaft sowie die Liste der geöffneten Fenster. Mit den Aktions-Tools werden Klick, Doppelklick, Rechtsklick, Hover, Texteingabe, Tastendrücke, Listenauswahl sowie Drag-and-drop ausgeführt. Die Assertion-Tools prüfen Existenz, Sichtbarkeit, Bedienbarkeit, Wert und Text eines Elements. Für das Warten, für Screenshots, die Videoaufzeichnung, den Build der Anwendung sowie den Commit in Git stehen weitere Tools zur Verfügung.
Die Tools verwenden System.Windows.Automation, die verwaltete UI-Automation-API von .NET. Als zweites Backend steht über eine Umgebungsvariable die Bibliothek FlaUI zur Verfügung, die mit der neueren, auf dem Component Object Model (COM) basierenden Schnittstelle von UI Automation arbeitet. Bei der Elementsuche wird zunächst die AutomationId, d. h. die vom Entwickler für ein Steuerelement vergebene feste ID, und anschließend der Name verwendet.
Die zentrale Entscheidung des Entwurfs ist, dass das Modell weder Bilder noch vollständige Bäume erhält. Ein Klick liefert immer einen Bericht über die geöffneten und geschlossenen Fenster und auf Wunsch zusätzlich einen Diff des Baums (die hinzugekommenen, verschwundenen und geänderten Elemente, je Art höchstens 30). Das folgende JSON zeigt die Antwort auf einen Klick auf das Menü „File“:
{ "success": true, "automationId": "MainMenu_File", "executionMs": 2840,
"diff": { "appeared": ["FileMenu_New", "FileMenu_Open"], "disappeared": [], "changed": [] },
"activeDialog": null, "windows": { "count": 1, "opened": [], "closed": [] } }
Das Feld executionMs enthält die Antwortzeit in Millisekunden. Der Wert ist hoch, da für diesen Klick der Diff eingeschaltet war. Das Feld activeDialog ist null, da der Klick keinen Dialog geöffnet hat.
Die zwei für den Diff benötigten Momentaufnahmen des Baums erhöhen die Antwortzeit von etwa 200 auf etwa 3000 Millisekunden. Standardmäßig ist der Diff ausgeschaltet und wird vom Modell über einen Parameter aktiviert, wenn die Änderungen eines Klicks benötigt werden. Beide Werte stehen in der Toolbeschreibung, die das Modell liest.
Der Fensterbericht wird immer geliefert, damit dem Modell kein neuer Dialog entgeht. Vollständige Bäume und Screenshots speichert das Tool auf der Festplatte und gibt nur den Dateipfad und nie den Inhalt zurück.
Der Agent besteht aus Markdown-Dateien
Das Repository enthält keinen KI-Code, da der Agent aus Claude Code und Prompts in Markdown besteht. Eine Rolle ist ein Command (eine Markdown-Datei mit Anweisungen) und wird von Claude Code geladen, wenn der Prompt mit dem Namen des Commands beginnt. Neben der Testrolle mit dem Command /test schreiben weitere Rollen ein Konzept, setzen eine Aufgabe um oder schreiben Dokumentation.
Der Test-Prompt ist ein Router mit etwa 250 Zeilen und enthält zwei Grundsätze, fünf Regeln sowie das Format der Ergebniszeile. Dabei gehen die Grundsätze jeder anderen Regel vor. Die Details stehen in Referenzdateien (z. B. Memory-Protokoll, Fehlersuche, Live-Testen und Schreiben von Skripten), die Claude Code nur bei Bedarf liest.
Nach dem ersten Grundsatz „live first“ wird die Anwendung standardmäßig über die MCP-Tools bedient und das Ergebnis nach jedem Schritt geprüft. Ein JSON-Testskript wird nur für die Wiederholung geschrieben (z. B. für einen nächtlichen Lauf, eine Regressionssuite oder die dauerhafte Reproduktion eines Fehlers).
Nach dem zweiten Grundsatz „memory first“ liest der Agent vor dem ersten Klick seine Notizen. Bei einem fehlgeschlagenen Schritt durchsucht er die Notizen nach dem Symptom, statt den Aufruf zu wiederholen.
Fünf Regeln bestimmen die Qualität eines Laufs. Für jeden Wert wird ein Round-Trip durchgeführt, d. h. ein vom Standard abweichender Wert wird gesetzt, gespeichert und nach dem erneuten Öffnen wieder gelesen. AutomationIds werden nicht geraten, sondern nur dem Live-Baum entnommen.
Bei jedem Fehlschlag wird in den Notizen nachgeschlagen und der Aufruf nicht wiederholt. Eine fehlende AutomationId wird im Quellcode ergänzt und nicht umgangen. Für jeden Fehler der Anwendung wird auch bei bestandenem Test ein Befund erzeugt.
Die erste Regel geht auf einen Fehler in einem Verbindungsdialog zurück, in dem eine Portnummer nach dem Speichern und erneuten Öffnen der Datei wieder auf dem Standardwert stand. Gefunden wurde der Fehler nur, weil der Test einen abweichenden Wert gesetzt, gespeichert, die Datei erneut geöffnet und den gespeicherten Wert geprüft hat. Ohne einen dieser vier Schritte bleibt der Fehler unsichtbar.
Memory, Feedback und Glossar
Da Claude Code jeden Lauf ohne Kontext beginnt, muss der Agent ohne Notizen jedes bekannte Problem der Anwendung in jedem Lauf erneut finden. Die Notizen sind Markdown-Dateien im Repository, derzeit rund tausend.
Eine Router-Datei listet etwa 30 Themen mit jeweils einer Zeile. Eine Themendatei listet ihre Notizen ebenfalls mit jeweils einer Zeile und verweist auf die benachbarten Themen. Zunächst liest der Agent den Router, öffnet anschließend ein bis drei Themen und folgt höchstens einem Verweis. Bei einem Fehlschlag werden die Notizen nach dem Symptom (z. B. „value reverts“) und nicht nach der Funktion durchsucht.
Die erste der drei Arten von Notizen ist die Fehlernotiz mit einem Fehlschlag, dessen Ursache sowie dem funktionierenden Schritt. Eine Workflow-Notiz enthält eine geprüfte Folge von Schritten, eine Prüfnotiz das Urteil über eine Funktion oder eine Aufgabe.
In einem Kombinationsfeld (ComboBox) schloss die Eingabetaste laut einer Fehlernotiz den gesamten Dialog, da WPF die Eingabetaste an den Standard-Button des Dialogs sendet. Als Lösung wird die Autovervollständigung mit der Pfeiltaste nach unten übernommen und anschließend auf den Button „OK“ geklickt.
Feedback ist der zweite Speicher und enthält zwei Dutzend von Menschen festgelegte Regeln. Eine Regel mit ihrem Geltungsbereich (z. B. Test oder Dokumentation) wird in Slack mit einer Nachricht angelegt, die mit „remember“ beginnt. In jedem Lauf werden die Regeln gelesen und angewendet sowie deren Nummern im Bericht genannt.
Das Glossar ist der dritte Speicher und enthält je Sprache die erlaubten Begriffe mit einer Definition sowie die falschen Begriffe mit ihrem Ersatz. Vor dem Schreiben von Text für Menschen (z. B. ein Fehlerbericht oder ein Hilfeartikel) wird das Glossar von jeder Rolle gelesen.
Der Skript-Runner
Der Runner ist ein Konsolenprogramm, das JSON-Testskripte ausführt. Runner und MCP-Server verwenden denselben Code, z. B. für die Elementsuche und für Klicks. Das Modell bedient die Anwendung live und der Runner führt dieselben Aktionen mit demselben Code aus einem Skript aus.
Ein Skript besteht aus Setup, Schritten, Teardown, Variablen, Importen und Invarianten. Es gibt zwölf Schritttypen (z. B. action, assert, wait_for, store, if, for_each sowie run für ein Unterskript). Das Format wird durch eine Schemadatei definiert, anhand derer ein unbekannter Schritttyp beim Laden des Skripts abgelehnt wird.
Für eine Aktion können die Elemente angegeben werden, die im Baum erscheinen oder verschwinden müssen. Ohne diese Angabe gilt ein wirkungsloser Doppelklick auf ein Symbol der Toolbox als bestandener Schritt. Mit der Angabe schlägt der Schritt fehl.
Nach jedem Schritt führt der Runner sieben automatische Prüfungen aus, sog. Testorakel (engl. Test Oracles). Unabhängig vom Schritt selbst erkennen die Orakel die folgenden Zustände der Anwendung:
- Der Prozess der Anwendung wurde beendet
- Ein Fenster beantwortet eine Nachricht nicht innerhalb eines Timeouts
- Ein neues Fenster ist erschienen, gemeldet durch einen WinEvent-Hook, auch wenn es bereits wieder geschlossen wurde
- Ein neues Fenster ist erschienen, gefunden durch Polling, wenn der Hook ausfällt
- Im Fenster ist ein Fehler-Label sichtbar
- Die Logdatei der Anwendung enthält neue Fehlerzeilen
- Das Anwendungsprotokoll der Windows-Ereignisanzeige enthält neue Einträge der Anwendung
Ein Absturzdialog wird an seinem Inhalt und nicht an seinem Titel erkannt, da der WPF-Dialog für eine unbehandelte Ausnahme den normalen Titel der Anwendung trägt. Hierzu liest der Runner den Text eines neuen Fensters und sucht nach einem Ausnahmetyp, einer Zeile des Stacktraces oder den beiden Buttons zum Fortsetzen und Beenden.
Der Runner schreibt einen HTML-Bericht mit einer Tabelle der Schritte und Screenshots, ein GIF aus den Screenshots sowie eine Zeile in eine SQLite-Datenbank. Für jeden Befund wird ein Fingerprint erzeugt (ein Hash über den Namen des Orakels, den normalisierten Titel und die erste Zeile des Stacktraces). Ein von zehn Skripten gefundener Absturz ist damit ein Problem und nicht zehn.
Für den zusätzlichen Random Walk listet eine Operationsdatei vier Operationen, nach denen die Anwendung wieder im selben Zustand ist (z. B. das Anlegen eines Textblocks mit anschließendem Undo). Ein Generator zieht daraus mit einem Seed 40 Operationen und schreibt ein normales Skript. In jeder Nacht werden drei Walks ausgeführt. Ein Absturz aus der Nacht lässt sich mit dem Seed reproduzieren.
Fehlende AutomationIds
UI Automation findet ein Element über seine AutomationId, die der Entwickler in der Extensible Application Markup Language (XAML) setzt. Ohne AutomationId bleiben nur der Name oder eine Koordinate. Der Test-Prompt verbietet beides und behandelt eine fehlende ID als Fehler der Anwendung.
Die Korrektur übernimmt ein zweiter Agent, den der Hauptagent mit einem fertigen Aufgabentext aus einem Tool des MCP-Servers startet. Der Subagent verfügt über keine UI-Tools, sondern nur über Datei-Tools. Im XAML- oder C#-Quellcode sucht der Subagent das Element, ergänzt die AutomationId mit einem Namen aus Bereich, Steuerelementtyp und Zweck und ändert sonst nichts.
Nach höchstens 50 Elementen je Lauf baut der Hauptagent die Anwendung mit dem Build-Tool, prüft die neuen IDs im Live-Baum, schreibt eine Notiz und committet die Änderung.
Slack und das Kanban-Board
Ein Bot verbindet Slack mit dem Testrechner. Der Bot ist ein .NET-Dienst ohne Modell, der die Bibliothek SlackNet im Socket Mode verwendet und dadurch keinen öffentlichen Endpunkt benötigt. Zunächst wird eine Allowlist geprüft und das erste Wort der Nachricht gelesen. Anschließend startet der Bot Claude Code im Headless-Modus mit dem Command und dem Text der Anfrage und liest die Ausgabe als JSON-Stream.
Aus dem Stream liest der Bot die Session-ID und sendet alle vier Sekunden die neuen Tool-Aufrufe und Nachrichten des Agenten an Slack. Die letzte Zeile eines Laufs ist eine Ergebniszeile mit dem Pfad des HTML-Berichts oder einem Fehlertext. Der Bot lädt den Bericht und das GIF hoch und speichert die Session-ID je Kanal.
Eine spätere Nachricht im selben Kanal setzt dieselbe Session mit dem Resume-Flag fort. Dadurch kann ein Tester eine Frage zu Schritt 12 stellen und erhält eine Antwort mit dem vollständigen Kontext des Laufs.
Da auf dem Desktop nur eine Instanz der Anwendung laufen kann, wird jeweils nur ein Lauf ausgeführt und weitere Anfragen werden in eine Warteschlange gestellt. Der Bot läuft als geplante Aufgabe bei der Anmeldung und nicht als Windows-Dienst, da UI Automation einen interaktiven Desktop benötigt.
Im Juli 2026 kam das Kanban-Board als zweiter Weg hinzu, einen Lauf zu starten. Eine Karte ist eine Aufgabe und ein Lauf ein Start von Claude Code für eine Karte in einer Rolle. Entwickeln und Testen sind zwei getrennte Läufe. Der Testlauf setzt nicht voraus, dass der Entwicklungslauf etwas geprüft hat. Im Test-Prompt steht dazu in einer Zeile, dass eine gerade entwickelte Karte ungetestet ist.
Grenzen
Da im Live-Modus kein Orakel läuft, kann nur der Agent einen Fehler bemerken. Der Random Walk umfasst vier Operationen. Sessions werden je Slack-Kanal und nicht je Thread nur im Arbeitsspeicher des Bot-Prozesses gehalten und gehen bei einem Neustart des Bots verloren.
Es gibt kein Retrieval über Embeddings, kein Fine-Tuning und keinen separaten Modellaufruf. Die Notizen werden mit einem Router und grep gelesen.
Das war’s.
Bei Fragen gerne melden.
Links:
Model Context Protocol →
Claude Code, programmatisch ausführen →
Claude Code, Skills und Commands →
Claude Code, Subagents →
Übersicht zu UI Automation →
FlaUI →
SlackNet →