CAWRestRemoteControl

An AI can’t see your user interface. So let it ask.

TCAWRestRemoteControl makes a running Delphi / VCL application answer questions over HTTP: which controls exist, what they contain, whether a button is enabled. Drop it on a data module, set Active := True, and UI testing stops being guesswork.

One unit, 2,845 lines, 59 tests. Built in a single day.

Eine KI sieht Ihre Oberfläche nicht. Also lassen Sie sie fragen.

TCAWRestRemoteControl lässt eine laufende Delphi-/VCL-Anwendung über HTTP Auskunft geben: welche Steuerelemente es gibt, was darin steht, ob eine Schaltfläche bedienbar ist. Auf ein Datenmodul legen, Active := True setzen — und UI-Tests hören auf, Raterei zu sein.

Eine Unit, 2.845 Zeilen, 59 Tests. An einem Tag entstanden.

The problem it solves

Automating a desktop UI from the outside means working from pictures. That is fragile for a human, and it is worse for an AI.

The usual approach is a screenshot and a pair of coordinates. It works until the window moves, until the display scaling changes, until a caption gets one word longer — and then it fails silently, clicking whatever happens to sit at 530 / 342 now.

For an AI agent the gap is wider still. It can be handed a screenshot, but it cannot know whether the button it sees is enabled, whether the value in that field was actually committed, or whether the screen has finished rebuilding. It infers, and inference is exactly what a test must not do.

This project ran into that wall repeatedly: clicks on custom-drawn buttons that silently did nothing, a grid that would not take keyboard focus, scroll attempts that moved nothing. Every one of those cost time and produced a test result nobody could trust.

Das Problem

Eine Desktop-Oberfläche von außen zu automatisieren heißt, mit Bildern zu arbeiten. Das ist für Menschen brüchig — für eine KI erst recht.

Der übliche Weg ist ein Bildschirmfoto und ein Koordinatenpaar. Das funktioniert, bis das Fenster verschoben wird, bis sich die Skalierung ändert, bis eine Beschriftung ein Wort länger wird — und dann scheitert es lautlos und klickt, was jetzt zufällig bei 530 / 342 liegt.

Für eine KI ist die Lücke noch größer. Man kann ihr ein Bild geben, aber sie kann nicht wissen, ob die Schaltfläche darauf bedienbar ist, ob der Wert im Feld wirklich übernommen wurde, ob der Bildschirm fertig aufgebaut ist. Sie schließt daraus — und Schließen ist genau das, was ein Test nicht tun darf.

In diesem Projekt lief das wiederholt gegen die Wand: Klicks auf selbst gezeichnete Schaltflächen, die stillschweigend nichts taten; ein Grid, das den Tastaturfokus nicht annahm; Scrollversuche, die nichts bewegten. Jedes Mal kostete es Zeit und lieferte ein Ergebnis, dem niemand trauen konnte.

Ask instead of guess

The application knows everything about itself. It just had no way to say so.

Fragen statt raten

Die Anwendung weiß alles über sich selbst. Sie hatte nur keine Möglichkeit, es zu sagen.

Guessing pixel coordinates versus asking the application by name

A VCL application holds its own component tree, with names, classes, properties and state. The component opens that up over HTTP and hands it out as JSON — and takes commands the same way. Nothing is inferred from pixels; every answer comes from the object itself.

Eine VCL-Anwendung trägt ihren Komponentenbaum in sich, mit Namen, Klassen, Eigenschaften und Zustand. Die Komponente öffnet ihn über HTTP und gibt ihn als JSON heraus — und nimmt auf demselben Weg Befehle entgegen. Nichts wird aus Pixeln geschlossen; jede Antwort kommt vom Objekt selbst.

Built so an AI can test

This is what it was actually made for. Remote-controlling a UI over REST is the mechanism; giving a machine a dependable grip on that UI is the point.

Gebaut, damit eine KI testen kann

Dafür ist es entstanden. Eine Oberfläche per REST fernzusteuern ist der Mechanismus — einer Maschine verlässlichen Zugriff darauf zu geben, ist der Zweck.

A test step: trigger, wait, verify

No timing guesswork

The usual fallback is sleep(3) and hope. Instead there is /wait: wait until a form appears, until a control becomes enabled, until it is gone, or until the main thread goes quiet. It answers 200 when the condition held and 408 when it never did — and reports how long it actually took.

Kein Raten über Zeiten

Der übliche Notbehelf ist sleep(3) und Hoffen. Stattdessen gibt es /wait: warten, bis ein Formular erscheint, bis ein Element bedienbar ist, bis es verschwunden ist, oder bis der Hauptthread zur Ruhe kommt. Antwort 200, wenn die Bedingung eintrat, 408, wenn nicht — samt der tatsächlich vergangenen Zeit.

Refusals instead of false positives

Triggering a disabled action returns 409, not a cheerful „done“. A test that asks for something impossible learns that it was impossible — the single most valuable property when nobody is watching the screen.

Absagen statt Scheinerfolge

Eine gesperrte Aktion auslösen ergibt 409, kein fröhliches „erledigt“. Ein Test, der Unmögliches verlangt, erfährt, dass es unmöglich war — die wertvollste Eigenschaft überhaupt, wenn niemand auf den Bildschirm sieht.

Modal dialogs don’t block

An action that opens a modal dialog would never return. With async=1 it is queued and answered immediately with 202 — checked first, so a disabled action is still refused. Message boxes can be answered in advance and are logged with their text, which is often worth more than the click.

Modale Dialoge blockieren nicht

Eine Aktion, die einen modalen Dialog öffnet, käme nie zurück. Mit async=1 wird sie vorgemerkt und sofort mit 202 beantwortet — vorher geprüft, eine gesperrte Aktion wird weiterhin abgelehnt. Meldungsdialoge lassen sich im Voraus beantworten und werden mit ihrem Text protokolliert, was oft mehr wert ist als der Klick.

Write, then read back

Setting a property answers with the value read back from the object, not with a confirmation. Some properties clamp or reformat what you give them, and that is precisely what the caller needs to know.

Schreiben und zurücklesen

Eine Eigenschaft zu setzen liefert den vom Objekt zurückgelesenen Wert, keine Bestätigung. Manche Eigenschaften begrenzen oder formatieren, was man ihnen gibt — und genau das muss der Aufrufer erfahren.

Names, not coordinates

Controls are addressed by name or by path (frmMain.pnlLeft.btnSave). Clicking goes through the control’s own Click method, so it works while the window is covered or in the background — and hits the intended element every time.

Namen statt Koordinaten

Steuerelemente werden über Namen oder Pfad angesprochen (frmMain.pnlLeft.btnSave). Der Klick geht über die Click-Methode des Elements und wirkt auch bei verdecktem oder im Hintergrund liegendem Fenster — und trifft immer das gemeinte.

Screenshots when they help

For the genuinely visual checks — colours, layout, a rendered label — /screenshot captures a window through PrintWindow, so the image is right even when the window is covered. The picture complements the facts; it no longer replaces them.

Bildschirmfotos, wo sie helfen

Für wirklich Visuelles — Farben, Layout, ein gerendertes Etikett — nimmt /screenshot ein Fenster über PrintWindow auf; das Bild stimmt auch bei Verdeckung. Es ergänzt die Fakten, statt sie zu ersetzen.

How it works

Wie es arbeitet

An AI agent asks the running application over HTTP and gets JSON back

Indy serves each request on its own thread, but the VCL is not thread-safe. Every access to forms, controls, actions or datasets is therefore marshalled onto the main thread; exceptions raised there are caught and re-raised on the server thread, so they reach the caller as an HTTP 500 with a message instead of disappearing.

A side effect that is deliberate: if the application is busy, the request waits. The answer then reflects the real state rather than a convenient fiction.

Indy bedient jede Anfrage in einem eigenen Thread, die VCL ist aber nicht threadsicher. Jeder Zugriff auf Formulare, Steuerelemente, Aktionen oder Datenmengen läuft deshalb über den Hauptthread; Ausnahmen von dort werden eingefangen und im Serverthread erneut ausgelöst, damit sie als HTTP 500 mit Meldung ankommen, statt zu verschwinden.

Ein gewollter Nebeneffekt: Ist die Anwendung beschäftigt, wartet die Anfrage. Die Antwort spiegelt dann den tatsächlichen Zustand statt einer bequemen Fiktion.

The endpoints

Die Endpunkte

/pingis anyone thereLebenszeichen
/componentsthe control tree, optionally with propertiesSteuerelementbaum, optional mit Eigenschaften
/componentone component, all published propertieseine Komponente, alle published Properties
/propertyread a value — or POST to set itWert lesen — oder per POST setzen
/focuswhere the focus is — or put it somewherewo der Fokus steht — oder ihn setzen
/actionsevery action with caption and statealle Aktionen mit Beschriftung und Zustand
/actionexecute one, optionally without waitingeine ausführen, auf Wunsch ohne Warten
/clicktrigger a control’s ClickClick eines Steuerelements auslösen
/keytype text or send a key with modifiersText tippen oder Taste mit Zusatztasten
/mouseposition, or move and clickPosition, oder bewegen und klicken
/waitwait for a form, a control state, or idleauf Formular, Zustand oder Ruhe warten
/dialogsanswer message boxes in advance, read the logMeldungen vorab beantworten, Protokoll lesen
/datasetfields and rows of a datasetFelder und Zeilen einer Datenmenge
/screenshota window, a control, or the whole screenein Fenster, ein Element oder der Bildschirm

Where it ends, your application takes over

The built-in endpoints know nothing about any particular program. Everything that needs application knowledge goes through one event.

Wo es aufhört, übernimmt die Anwendung

Die eingebauten Endpunkte wissen nichts über ein bestimmtes Programm. Alles, wofür Anwendungswissen nötig ist, läuft über ein Ereignis.

Built-in endpoints answer generically; application-specific paths go through OnCustomRequest

Any path under /custom/ is handed to OnCustomRequest with method, path, parameters and body. The handler runs on the main thread already, so it may touch datasets and the UI directly, and answers with JSON and an HTTP status. Leave it unhandled and the component replies 404 by itself.

In the application this was built for, that event exposes a query through the existing database layer and the current connection state. Other obvious uses: seed a fixture before a test, reset to a known state afterwards, or read an internal counter that no control displays.

Jeder Pfad unter /custom/ geht an OnCustomRequest, mit Verfahren, Pfad, Parametern und Rumpf. Der Handler läuft bereits im Hauptthread, darf also direkt auf Datenmengen und Oberfläche zugreifen, und antwortet mit JSON und HTTP-Status. Bleibt er unbehandelt, antwortet die Komponente selbst mit 404.

In der Anwendung, für die das entstand, stellt dieses Ereignis eine Abfrage über die vorhandene Datenbankschicht bereit sowie den Verbindungszustand. Weitere naheliegende Fälle: vor einem Test einen Datenbestand herstellen, danach auf einen bekannten Stand zurücksetzen, oder einen internen Zähler auslesen, den kein Steuerelement anzeigt.

// Anything under /custom/ lands here. APath is already stripped
// of the prefix: "/custom/sql" arrives as "sql".
procedure TdmMain.RestCustomRequest(Sender: TObject;
  const AMethod, APath: string; AParams: TStrings;
  const ABody: string; var AResponse: string;
  var AStatusCode: Integer; var AHandled: Boolean);
begin
  if SameText(APath, 'sql') then
  begin
    AResponse := QueryToJson(AParams.Values['sql']);
    AHandled := True;
  end;
end;
// Alles unter /custom/ landet hier. APath ist bereits um das
// Praefix gekuerzt: "/custom/sql" kommt als "sql" an.
procedure TdmMain.RestCustomRequest(Sender: TObject;
  const AMethod, APath: string; AParams: TStrings;
  const ABody: string; var AResponse: string;
  var AStatusCode: Integer; var AHandled: Boolean);
begin
  if SameText(APath, 'sql') then
  begin
    AResponse := QueryToJson(AParams.Values['sql']);
    AHandled := True;
  end;
end;

There is also Execute, which runs a request straight through the same dispatcher without any network involved — for cases where the transport is somewhere else entirely, or for testing the endpoints themselves.

Daneben gibt es Execute: Es schickt eine Anfrage direkt durch denselben Verteiler, ganz ohne Netz — für Fälle, in denen der Transport woanders liegt, oder um die Endpunkte selbst zu prüfen.

Built in a day

Not a rewrite, not a framework — a tool that was missing, so it got built between two other tasks.

The commit history is blunt about it: first version at 01:19, the last of the round at 11:34 the same morning. In between: the component itself, a rename after the compiler pointed out that Dispatch already means something on TObject, the endpoints for waiting and writing, the dialog handling — and 59 tests.

That pace is only possible because the loop was closed. Build, run the tests, read the failure, fix, run again — without a human copying anything between windows. Several things were only found because the tests ran every single time: a /ping that crashed in an application without a main form, because IfThen evaluates both branches; a test of mine that asserted a guarantee the component never made.

Every endpoint was then exercised against the live application before it counted as done — including the one that took 46 ms to answer instead of blocking on a modal dialog, and the dialog watcher that closed and logged a real dialog in 62 ms.

An einem Tag entstanden

Keine Neuentwicklung, kein Framework — ein Werkzeug, das fehlte, also wurde es zwischen zwei anderen Aufgaben gebaut.

Die Commit-Historie ist da unmissverständlich: erste Fassung um 01:19, der letzte Commit der Runde um 11:34 desselben Vormittags. Dazwischen: die Komponente selbst, eine Umbenennung, nachdem der Compiler darauf hinwies, dass Dispatch bei TObject bereits etwas bedeutet, die Endpunkte zum Warten und Schreiben, die Dialogbehandlung — und 59 Tests.

Dieses Tempo geht nur, weil der Kreis geschlossen ist. Bauen, Tests laufen lassen, Fehlschlag lesen, reparieren, erneut laufen lassen — ohne dass jemand etwas zwischen Fenstern kopiert. Mehreres kam nur heraus, weil die Tests wirklich jedes Mal liefen: ein /ping, das in einer Anwendung ohne Hauptformular abstürzte, weil IfThen beide Zweige auswertet; ein Test von mir, der eine Zusage prüfte, die die Komponente nie gemacht hat.

Danach wurde jeder Endpunkt an der laufenden Anwendung ausprobiert, bevor er als fertig galt — auch der, der nach 46 ms antwortet, statt an einem modalen Dialog hängen zu bleiben, und die Dialogüberwachung, die einen echten Dialog in 62 ms geschlossen und protokolliert hat.

Numbers

Zahlen

2,845 lines in one unit Zeilen in einer Unit
14 endpoints, plus your own Endpunkte, plus eigene
59 tests for the component Tests für die Komponente
3 lines to switch it on Zeilen zum Einschalten
0 external dependencies beyond Indy Fremdbibliotheken außer Indy

Limits — and one warning

Grenzen — und eine Warnung

Not for production

This is a testing and diagnostics tool. It can drive the application and read its data, and it is not built to withstand an attacker. It binds to 127.0.0.1 by default, an AuthToken can be required, and in the application it was written for it stays switched off unless an INI file enables it explicitly.

Binding it to anything other than the loopback address belongs in a sealed-off test environment — nowhere else. On a production machine it should not be enabled at all.

Nicht für Produktivumgebungen

Das ist ein Werkzeug zum Testen und zur Fehlersuche. Es kann die Anwendung fernsteuern und ihre Daten auslesen, und es ist nicht darauf ausgelegt, einem Angreifer standzuhalten. Es bindet per Vorgabe an 127.0.0.1, ein AuthToken lässt sich verlangen, und in der Anwendung, für die es entstand, bleibt es aus, solange es nicht ausdrücklich per INI-Datei eingeschaltet wird.

Eine Bindung an etwas anderes als die Loopback-Adresse gehört in eine abgeschottete Testumgebung — sonst nirgendwohin. Auf einem Produktivrechner sollte es gar nicht erst aktiviert sein.

VCL only

It reads the VCL component tree. Application.MessageBox is a plain Win32 window without a VCL form and therefore out of reach; so is anything drawn by a control that keeps its state to itself.

Nur VCL

Gelesen wird der VCL-Komponentenbaum. Application.MessageBox ist ein reines Win32-Fenster ohne VCL-Formular und damit nicht erreichbar — ebenso wenig, was ein Steuerelement nur für sich selbst zeichnet.

Setting is not typing

A property set through the interface does not raise the same events as a real keystroke. Where validation hangs off OnChange, use /key instead — the component documents the difference rather than hiding it.

Setzen ist nicht Tippen

Eine gesetzte Eigenschaft löst nicht dieselben Ereignisse aus wie eine echte Eingabe. Wo die Prüfung an OnChange hängt, führt /key zum Ziel — die Komponente dokumentiert den Unterschied, statt ihn zu verbergen.

It waits with the application

If the main thread is busy, so is the interface. That is deliberate — but it means the component cannot be used to inspect an application that has hung.

Es wartet mit der Anwendung

Ist der Hauptthread beschäftigt, wartet auch die Schnittstelle. Das ist gewollt — heißt aber, dass sich eine hängende Anwendung damit nicht untersuchen lässt.