Self-Healing Locators

Automatische Locator-Reparatur über Referenz-Screenshots — einmal angewendet, für immer gemerkt.

← Zurück zur Übersicht · 🇬🇧 English · ← API-Referenz · CI-Integration →


Was es macht

Wenn der Locator eines Controls zur Laufzeit sein Element nicht findet — meist nach einem App-Update, das eine Automation-ID, CSS-Klasse oder DOM-Position ändert — kann das Framework statt eines sofortigen Test-Fehlschlags eine automatische Reparatur versuchen:

  1. Es vergleicht einen Referenz-Screenshot (einmalig aufgenommen, als der TestStep geschrieben wurde) mit einem frischen Screenshot der aktuellen UI.
  2. Es schickt beide Bilder plus den aktuellen Element-Tree an ein Vision-Modell und fragt nach dem Element, das dem im Referenz-Bild markierten semantisch entspricht.
  3. Wird ein Kandidaten-Locator zurückgegeben, wiederholt das Framework den gescheiterten Lookup damit.
  4. Gelingt der Retry, wird der geheilte Locator am Ende des Test-Laufs in die Control-Quelldatei zurückgeschrieben — sodass der nächste Lauf ihn direkt findet, ohne erneuten Vision-Aufruf für diesen Locator.

Der Mechanismus hat zwei unabhängige Hälften: Discovery (Schritte 1–3, aktiv bei jedem Lauf mit gebundener Referenz) und Writeback (Schritt 4, Opt-in per Umgebungs-Variable — siehe unten). Du kannst Discovery ohne Writeback nutzen, wenn du Änderungen lieber selbst prüfen und übernehmen möchtest.

💡 Wann ist das nützlich? Locator-Brüche kommen typischerweise aus zwei Quellen: Desktop/Appium-Automation-IDs, die sich nach einem App-Update ändern, und Web-Frontend-Refactors, die CSS-Klassen umbenennen oder das DOM umstrukturieren. Self-Healing macht aus “Test schlägt fehl, jemand muss den Locator von Hand neu inspizieren und reparieren” ein “Test heilt sich einmal selbst und läuft danach unbeaufsichtigt weiter.”

Overlays ohne stabilen Namen heilen

Ein Dialog oder Overlay mit semantischem Marker — role="dialog", ein aria-label, ein passender Titel — heilt wie jedes andere Element: das Framework hat einen Namen, mit dem es Vision durchsuchen kann. Ein typisches Cookie-Consent-Banner oder ein eigenes Modal hat davon oft nichts; sein Container wird stattdessen generisch erkannt, anhand gängiger ARIA/HTML-Overlay-Signale (role="dialog"/"alertdialog", aria-modal, das <dialog>-Tag, oder eine Klasse/ID, die modal, overlay, dialog, banner, cookie oder consent enthält). Ist bei einem Step der checkTitleFromPageExists-/checkTitleFromDialogExists-Wächter aktiv, dient der Titeltext des Dialogs als Vision-Suchhinweis; sonst löst ein generischer Hinweis (“das aktuell auf dem Bildschirm offene Modal/Overlay/Dialog/Cookie-Banner”) genau das eine Overlay auf, das gerade vor dem Nutzer steht.


Setup: einen Referenz-Screenshot binden

Self-Healing aktiviert sich für einen TestStep-Aufruf nur, wenn für genau diesen Aufruf ein Referenz-Screenshot gebunden ist. Die Bindung ist eine Zeile am Anfang des TestStep-Bodies:

// 2_Apps/<DeineApp>/2_Steps/TS_Main.ts
import * as Core from '@meintest/cc-testframework';

export const TS_Main_Button_Click = async (refId: string, label: string): Promise<void> => {
    Core.Inspector.bindReference(refId);
    // ... restlicher TestStep-Body, unverändert
};
  • refId ist eine kurze, kebab-case-Kennung und immer der erste Parameter des TestSteps. Übergib '', wenn für einen konkreten Aufruf keine Referenz nötig ist — der Step verhält sich dann exakt wie vor Self-Healing, ohne Mehraufwand.

Einem TestCase eine stabile ID geben

Referenz-PNGs werden über eine TestCase-ID referenziert, nicht über den Playwright-Test-Titel des TestCase. Setze sie am Anfang einer TC_*.spec.ts-Datei — innerhalb des test(...)-Callbacks, oder in einem test.beforeEach, wenn die Datei mehrere Tests deklariert:

// 3_Cases/TC_<DeinSzenario>.spec.ts
import * as Project from '@GlobalRef';

Project.Core.test('TC_<DeinSzenario>', async () => {
    Project.Core.setTestCaseId('tc_<dein-szenario>');
    Project.Core.Step.setCurrentTestCaseName('TC_<DeinSzenario>');
    // ... restlicher TestCase-Body, unverändert
});
  • Eine UID ist eine stabile, maschinenfreundliche Kennung — zu unterscheiden von Step.setCurrentTestCaseName, das nur den menschenlesbaren Namen in Reports beeinflusst. Umbenennen der Test-Beschreibung oder der Spec-Datei verwaist keine unter einer UID gebundenen Referenz-Assets, weil ID und Titel unabhängig voneinander sind.
  • Empfohlenes Slug-Format: tc_[a-z0-9-]{4,} — kleingeschrieben, mit Bindestrichen getrennt, dateisystem-sicher. Beispiele: tc_login-happy-path, tc_dashboard-grid-sort.
  • Lies die aktuell gesetzte ID mit Core.getTestCaseId(): string | null zurück — null, wenn für den aktuellen TestCase noch keine gesetzt wurde.
  • Werden deine TestCases von einem Scaffolding-Werkzeug generiert, wird der Core.setTestCaseId(...)-Aufruf automatisch ergänzt; schreibst du eine TC_*.spec.ts-Datei selbst, fügst du ihn von Hand ein.

Wo das Referenz-PNG liegt

pm/
├── 3_Cases/
│   └── TC_<DeinSzenario>.spec.ts
└── 7_Assets/
    └── tc_<dein-szenario>/
        ├── login-btn.png
        └── dashboard-grid.png
  • 7_Assets/ ist ein Sibling von 3_Cases/ innerhalb des pm/-Ordners deines Projekts — nicht in 3_Cases/ verschachtelt — und folgt der nummerierten Ordner-Konvention des Frameworks. Siehe Konzepte — Die komplette Ordnerstruktur für alle 11 nummerierten Ordner.
  • Pfad: pm/7_Assets/<uid>/<assetId>.png, wobei <assetId> derselbe refId-String ist, der als erster Parameter eines TestSteps übergeben wird. Löschst du den 7_Assets/<uid>/-Unterordner eines TestCase, verschwinden auch seine Referenzen — keine verwaisten Dateien.

Reihenfolge der Pfadauflösung

  1. Ist für den aktuellen TestCase eine UID gesetzt, löst das Framework pm/7_Assets/<uid>/<refId>.png auf.
  2. Ist keine UID gesetzt, greift der Legacy-, Titel-basierte Pfad pm/3_Cases/Assets/<TestCase-Titel>/<refId>.png, aufgelöst ab dem Playwright-Config-Root deines Projekts (seit v0.25.0 pm/ selbst — siehe Migration auf v0.25.0 unten), und das Framework loggt einmalig eine Deprecation-Warnung für diesen TestCase, die auf die Schritte oben verweist.
  3. Existiert an keinem der beiden Orte eine passende Datei, ist für diesen Aufruf keine Referenz gebunden und Self-Healing wird für den Step still übersprungen — exakt wie bei refId gleich ''. Das ist nie ein harter Fehlschlag.

Aufruf eines TestSteps mit gebundener Referenz:

await Project.<DeineApp>.TS_Main_Button_Click('login-btn', 'Sign in');

Aufruf ohne Referenz — unverändert, exakt wie jeder TestStep heute:

await Project.<DeineApp>.TS_Main_Button_Click('', 'Sign in');

💡 Woher kommen die Referenz-PNGs? Nimm sie interaktiv während der Test-Erstellung mit Inspector.pause() auf, oder lass sie zusammen mit einem generierten TestCase von einem externen Tool oder KI-Agenten mitliefern. So oder so ist das PNG eine ganz normale Datei — direkt im Datei-Browser zu öffnen oder in einem GitHub-Diff zu reviewen, keine Sonder-Kodierung.


Migration der Referenz-Asset-Pfade von v0.15 auf v0.16

v0.16 ersetzt den bisherigen, Titel-basierten Referenz-Asset-Pfad durch die oben beschriebene UID-Konvention und korrigiert außerdem, wie der Fallback-Pfad selbst aufgelöst wird:

  • Der Legacy-Pfad war bisher hartcodiert mit einem tests/-Präfix (tests/3_Cases/Assets/<TestCase-Titel>/<refId>.png) — ein Layout, das nur zu einem Projekt passte, dessen kompletter Code unter einem tests/-Ordner liegt. Ein Projekt mit 3_Cases/ direkt am Root fand seine Referenz-Assets nie.
  • Der Legacy-Pfad ist zudem über den Playwright-Test-Titel des TestCase referenziert — ein Umbenennen der test(...)-Beschreibung oder der Spec-Datei verwaiste dessen Referenz-PNGs still.

So migrierst du ein bestehendes Projekt:

  1. Füge Core.setTestCaseId('tc_<slug>') am Anfang jeder TC_*.spec.ts-Datei ein, die Referenzen bindet (siehe oben).
  2. Verschiebe die passenden PNGs von ihrem bisherigen Ort nach 7_Assets/<uid>/, mit denselben Dateinamen.
  3. Lässt du die ID eines TestCase lieber vorerst ungesetzt, funktioniert der Legacy-Fallback in der Zwischenzeit weiter und zeigt einmalig einen Hinweis pro TestCase, der auf dieselben beiden Schritte verweist.

Breaking für tests/-verschachtelte Projekte. Der Legacy-Fallback-Pfad in v0.16 enthält kein tests/-Präfix mehr. Lagen deine Referenz-Assets bisher unter tests/3_Cases/Assets/<TestCase-Titel>/<refId>.png, löst dieser exakte Pfad nicht mehr auf — Self-Healing findet diese Referenzen dann still nicht mehr, genau wie bei einem refId ohne passende Datei. Verschiebe die Assets entweder eine Ebene nach oben zu 3_Cases/Assets/<TestCase-Titel>/<refId>.png (dieselbe Legacy-Konvention, ohne tests/-Präfix), oder — empfohlen — setze eine UID und verschiebe sie stattdessen nach 7_Assets/<uid>/<refId>.png.


Migration auf v0.24.0 (Projekt-Layout)

v0.24.0 verpackt jeden gescaffoldeten Projektordner — 2_Apps/, 3_Cases/, 7_Assets/ und die restlichen nummerierten Ordner — in einem einzigen pm/-Root. Siehe Konzepte — Migration auf v0.24.0 für die vollständige Ordner-Umnummerierung; für die Referenz-Asset-Auflösung im Speziellen:

  • Der empfohlene, UID-basierte Pfad wird zu pm/7_Assets/<uid>/<refId>.png (war 7_Assets/<uid>/<refId>.png).
  • Zum Zeitpunkt dieser Migration war der Legacy-, Titel-basierte Fallback-Pfad vom pm/-Wrapper nicht betroffen — er löste weiterhin unter 3_Cases/Assets/<TestCase-Titel>/<refId>.png relativ zum Playwright-Config-Root deines Projekts auf, der damals noch dein äußerer Repo-Root war, nicht innerhalb von pm/. Siehe Migration auf v0.25.0 unten — diese Asymmetrie schließt sich, sobald dein Playwright-Config-Root selbst in pm/ wandert.

Das ist ein breaking, harter Schnitt ohne Backward-Compat-Shim — das Framework befindet sich weiterhin in aktiver Entwicklung und liest Asset-Orte von vor v0.24.0 nicht automatisch ein.


Migration auf v0.25.0 (autonomes pm/-Projekt)

v0.25.0 verschiebt playwright.config.ts (zusammen mit package.json, tsconfig.json, .npmrc) aus dem äußeren Repo-Root in pm/ selbst — siehe Konzepte — Migration auf v0.25.0. Das ändert, wo die Repo-Root-Erkennung des Frameworks landet: sie löst jetzt auf pm/ auf — den Ordner, der playwright.config.ts und package.json trägt — statt auf den äußeren Ordner, der pm/ enthält. Beide oben beschriebenen Referenz-Asset-Pfade werden relativ zu genau dieser Root aufgelöst, landen also jetzt ohne zusätzliche Konfiguration beide innerhalb von pm/:

  • Der empfohlene, UID-basierte Pfad bleibt pm/7_Assets/<uid>/<refId>.png. Core.Constant.assetsDir in der mitgelieferten GlobalConfig.ts ändert sich von 'pm/7_Assets' zurück auf '7_Assets' — kein pm/-Präfix mehr nötig, da die aufgelöste Root bereits pm/ ist. Setzt du assetsDir von Hand, entferne das pm/-Präfix, um eine Doppel-Verschachtelung zu pm/pm/7_Assets zu vermeiden.
  • Der Legacy-, Titel-basierte Fallback-Pfad — direkt oben als vom pm/-Wrapper unbetroffen beschrieben — löst jetzt ebenfalls unter pm/3_Cases/Assets/<TestCase-Titel>/<refId>.png auf, womit sich diese Asymmetrie schließt. Das ist ein Seiteneffekt der Repo-Root-Änderung, keine Umschreibung des Fallback-Pfads selbst — er war schon immer relativ zum “Playwright-Config-Root deines Projekts”, und diese Root liegt jetzt innerhalb von pm/.

Das ist ein breaking, harter Schnitt ohne Backward-Compat-Shim.


Setup: den Writeback-Reporter aktivieren

Discovery funktioniert, sobald eine Referenz gebunden ist — ohne weitere Konfiguration. Writeback — das Persistieren einer erfolgreichen Heilung in deiner Control-Quelldatei — braucht noch eine Sache: Opt-in per Umgebungs-Variable.

export SELF_HEALING_WRITEBACK=true

Spreadest du in deiner playwright.config.ts das baseConfig des Frameworks (siehe API-Referenz — baseConfig), ist das schon das komplette Setup. baseConfig hängt SelfHealingWritebackReporter automatisch an sein reporter-Array an, sobald SELF_HEALING_WRITEBACK oder SELF_HEALING_WRITEBACK_DISCOVERY auf true gesetzt ist — kein Edit an playwright.config.ts nötig:

// playwright.config.ts
import { defineConfig } from '@playwright/test';
import { baseConfig } from '@meintest/cc-testframework';

export default defineConfig({
    ...baseConfig,
    // kein reporter[]-Override nötig — SELF_HEALING_WRITEBACK wird gelesen, wenn diese Datei lädt
});

Der bereits vorhandene ['html']-Default-Reporter bleibt dabei immer erhalten.

Den Reporter manuell registrieren

Überspring den reinen Env-Var-Weg von oben und registriere SelfHealingWritebackReporter selbst, wenn deine playwright.config.ts kein baseConfig spreadet, oder dein Projekt bereits ein eigenes reporter[]-Array hat und du es lieber explizit ergänzt, statt dich auf Auto-Wiring zu verlassen:

// playwright.config.ts
import { defineConfig } from '@playwright/test';
import { SelfHealingWritebackReporter } from '@meintest/cc-testframework';

export default defineConfig({
    reporter: [
        ['list'],
        [SelfHealingWritebackReporter],
    ],
});

Baust du dein eigenes reporter[]-Array ohne Import des kompletten Paket-Barrels auf, referenziere den Reporter stattdessen über seinen dedizierten Sub-Pfad-Export — er löst zur exakt selben Klasse auf:

reporter: [
    ['list'],
    ['@meintest/cc-testframework/reporter/self-healing-writeback'],
],

Der ältere, wildcard-abgeleitete Pfad (@meintest/cc-testframework/Reporter/SelfHealingWritebackReporter, großgeschrieben) löst für bestehende Configs weiterhin auf — der kleingeschriebene Sub-Pfad oben ist ab jetzt die kanonische Form für neue Configs.

Eine manuelle Registrierung, die du schon vor dem baseConfig-Auto-Wiring eingerichtet hattest, funktioniert unverändert weiter — daran ist nichts deprecated, es ist nur nicht mehr die einzige Option.

Danach wählst du den Modus per Umgebungs-Variable:

Env-var Effekt
(nicht gesetzt) Discovery-Modus. Heilungen werden versucht und in .self-healing-report.json festgehalten; nichts wird in Quelldateien geschrieben.
SELF_HEALING_WRITEBACK_DISCOVERY=true Gleicher Effekt wie nicht gesetzt — expliziter Opt-in für CI-Jobs, die den Report wollen, ohne sich darauf zu verlassen, dass eine Env-Var einfach nicht gesetzt ist.
SELF_HEALING_WRITEBACK=true Apply-Modus. Jede erfolgreiche Heilung dieses Laufs wird in die Control-Quelldatei geschrieben, die den betroffenen Locator besitzt.
SELF_HEALING_WRITEBACK_EMIT_ONLY=true Emit-Only-Modus. Jede erfolgreiche Heilung wird erfasst und in eine dauerhafte, maschinenlesbare Datei geschrieben — nichts wird angewendet. Siehe Emit-Only-Modus weiter unten.

Apply-Modus ist standardmäßig aus, damit ein CI-Lauf dein Repository nie unerwartet verändert. Aktiviere ihn für einen lokalen Lauf, oder für einen dedizierten CI-Job, der dir einen Review-Diff / Pull-Request zur Prüfung öffnet.

Wie weit der Apply-Modus geht: writebackMode

Apply-Modus schreibt eine Heilung — aber “die Datei schreiben”, “sie committen” und “sie pushen” sind drei unterschiedliche Vertrauens-Stufen, und das Framework lässt dich unabhängig wählen, wie weit es geht:

SELF_HEALING_WRITEBACK_MODE Was passiert
working-tree (Default) Die geheilte(n) Datei(en) — und ein eventueller Pro-Instanz-Override-Store-Eintrag — werden geschrieben und bleiben dirty in deinem Working Tree. Nichts wird committet, nichts gepusht; du reviewst und committest selbst.
commit Ein koordiniertes Schreiben, plus ein lokaler git commit — kein Push.
commit-push Der volle Zyklus: koordiniertes Schreiben, Commit und Push zu origin — das bisherige, immer-veröffentlichende Verhalten, jetzt ein bewusstes Opt-in.

Setze es genauso wie jede andere Self-Healing-Wahl — eine Umgebungs-Variable, oder selfHealing.writebackMode in der persistierten Config-Datei (siehe unten):

export SELF_HEALING_WRITEBACK_MODE=commit-push

Diese Einstellung gilt für BEIDE Writeback-Ziele — eine gewöhnliche Control-Quelldatei-Bearbeitung und einen Pro-Instanz-Override-Store-Write — nach exakt derselben Regel. Sie hat keine Wirkung im Emit-Only-Modus, der unabhängig von writebackMode nie etwas anwendet.

Verhaltensänderung: SELF_HEALING_WRITEBACK=true allein committet oder pusht nicht mehr. Bevor diese Einstellung existierte, hat Apply-Modus ohne weitere Konfiguration koordiniert, committet UND zu origin gepusht — in deinem Namen. Der Default ist jetzt working-tree — “die Heilung persistieren” und “sie veröffentlichen” sind jetzt getrennte, bewusst gewählte Vertrauens-Stufen. Betreibst du Self-Healing in CI und hast dich auf das alte, immer-pushende Verhalten verlassen, ergänze SELF_HEALING_WRITEBACK_MODE=commit-push (oder selfHealing.writebackMode: 'commit-push' in deiner persistierten Config), damit diese Pipeline exakt wie zuvor weiterläuft — sonst landen ihre Heilungen jetzt als uncommittete Working-Tree-Änderung, die niemand ansieht.

Nicht zu verwechseln mit dem writebackMode-Feld der Report-Datei selbst ("applied" / "discovery" / "emitted", das beschreibt, welcher Top-Level-Modus den Report erzeugt hat — siehe Die Report-Datei weiter unten): das hier ist eine andere Einstellung, die steuert, wie weit eine bereits angewendete Heilung Richtung Git geht.

Setup dauerhaft speichern

SELF_HEALING_WRITEBACK (und SELF_HEALING_AGENT_IDENTITY, siehe Abschnitt „Commit-Identität” weiter unten) als reine Umgebungs-Variable zu setzen, geht leicht verloren — ein neues Terminal, der Wechsel von einer Windows-Shell zu einer Linux-Shell, oder ein Neustart des integrierten Editor-Terminals setzen sie alle zurück. Als Alternative persistiert npx cc-testframework config self-healing <action> dieselben Einstellungen in einer kleinen, projekt-lokalen .cc-testframework.local.json-Datei innerhalb von pm/ (dem Repo-Root deines Projekts, wie vom Framework aufgelöst — siehe Konzepte — Migration auf v0.25.0):

npx cc-testframework config self-healing enable                                     # selfHealing.enabled = true
npx cc-testframework config self-healing disable                                    # selfHealing.enabled = false
npx cc-testframework config self-healing discovery                                  # selfHealing.discoveryMode = true
npx cc-testframework config self-healing set-identity "Jane Doe-agent <jane+agent@example.com>"
npx cc-testframework config self-healing show                                       # den effektiven, gemergten Stand ausgeben

Jede Einstellung löst über dieselbe Priority-Chain auf: eine gesetzte Umgebungs-Variable gewinnt immer gegen die Config-Datei, die wiederum gegen den eingebauten Default gewinnt. Ein bestehendes export SELF_HEALING_WRITEBACK=...-Setup funktioniert also unverändert weiter, und ein einmaliger Override (z.B. SELF_HEALING_WRITEBACK=false für einen einzelnen CI-Lauf) hat immer Vorrang vor dem, was in der Datei persistiert ist — sogar der wörtliche Wert "false" zählt als “gesetzt” und gewinnt.

$ npx cc-testframework config self-healing show
Self-Healing configuration (merged: env-var > config-file > default):
  enabled:        true  (source: config)
  discoveryMode:  false  (source: default)
  agentIdentity:  cc-testframework-agent <agent@your-domain>  (source: config)
  config-file:    /pfad/zu/deinem-projekt/.cc-testframework.local.json

Die Datei enthält keine Geheimnisse — nur die drei Booleans/Strings oben —, ist aber eine persönliche/lokale Laufzeit-Präferenz und keine Einstellung, die ein ganzes Team identisch teilen sollte; trag sie in dein Projekt-.gitignore ein.

Die Config-Datei allein genügt nicht, wenn du dich auf baseConfigs automatische Reporter-Registrierung verlässt. baseConfig entscheidet anhand der Umgebungs-Variable direkt, ob SelfHealingWritebackReporter an sein reporter-Array angehängt wird — das selfHealing.enabled der Config-Datei fließt in diese konkrete Entscheidung (noch) nicht ein. Spreadet deine playwright.config.ts baseConfig und hast du den Reporter nicht manuell registriert, lass SELF_HEALING_WRITEBACK=true zusätzlich als Umgebungs-Variable gesetzt für jeden Lauf, der den Reporter braucht. Registrierst du den Reporter manuell, entfällt diese Einschränkung komplett — einmal registriert (egal wie), liest er die gemergte Config (Env-Var oder Datei) bei jedem Lauf korrekt. npx cc-testframework self-healing status zeigt genau das an — siehe Pipeline-Probleme diagnostizieren weiter unten.

Damit Writeback das richtige Array-Element zum Aktualisieren findet, müssen Locators das Core.xpath-Tagged-Template statt eines rohen Template-Literal-Strings nutzen:

// 1_Controls/Control_DataGrid.ts
import * as Core from '@meintest/cc-testframework';

export async function findRow(page: Core.Page, rowIndex: number) {
    return Core.SearchEngine.findLocators(page, [
        Core.xpath`(//*[@ControlType='ControlType.DataItem'])[${rowIndex}]`,
    ]);
}

Core.xpath erzeugt exakt denselben String wie ein äquivalentes rohes Template-Literal — es ist rein additiv und erfordert kein Umschreiben von Controls, die kein Self-Healing brauchen. Tagge nur die Locators, die du writeback-fähig machen willst.

Wird eine Heilung angewendet, wird der geheilte Locator als erstes Element des Arrays eingefügt; dein(e) ursprüngliche(r) Locator(s) bleiben weiter unten als Fallbacks erhalten. Sonst wird nichts in der Datei angefasst — Einrückung, Kommentare und umgebender Code bleiben exakt wie vorher.


Emit-Only-Modus: externe Orchestrierung

Für einen Workflow, in dem ein separates Automatisierungs-Tool — ein externer Orchestrator, eine CI-Pipeline, oder ein beliebiger Prozess, der an dieses Framework andockt — Heilungen unter seinem eigenen Versionskontroll-Ablauf prüft und anwendet, statt das Framework direkt committen zu lassen, aktivierst du den Emit-Only-Modus:

export SELF_HEALING_WRITEBACK_EMIT_ONLY=true

Dieses Flag ist eigenständig: Es allein zu setzen erfasst Heilungen während des Laufs, ohne dass zusätzlich SELF_HEALING_WRITEBACK oder SELF_HEALING_WRITEBACK_DISCOVERY gesetzt sein müsste. Das Framework erfasst jede erfolgreiche Heilung, wendet aber nichts an — keine Control-Quelldatei wird umgeschrieben, und auch kein Override-Store-Eintrag wird geschrieben. Stattdessen schreibt es eine dauerhafte .self-healing-emit.jsonl — ein JSON-Datensatz pro Heilung, bei jedem Lauf frisch überschrieben — neben dem gewohnten .self-healing-report.json (dessen Einträge status: "emitted" tragen). Jeder Datensatz identifiziert, wohin die Heilung gehört (target: "source" für einen gewöhnlichen Control-Locator, target: "override-store" für eine Shared-Template-Heilung), plus den deklarierten Locator-Block, den geheilten XPath, den interpolierten Wert und die gebundene refId — alles, was ein externer Konsument braucht, um das Schreiben selbst auszuführen, entweder als Quelldatei-Edit oder als Override-Store-Eintrag, unter seinem eigenen Review-/Commit-Ablauf. Die Pending-Queue-Datei bleibt dabei erhalten statt gelöscht zu werden, damit nichts verloren geht, falls der konsumierende Prozess später läuft als der Testlauf selbst.

Modus Wendet Heilungen an? Dauerhafter Datensatz pro Heilung? Wer wendet an
Discovery (Default, nicht gesetzt) Nein Nein — nur Report; Pending-Queue wird am Lauf-Ende gelöscht Niemand — nur menschlicher Review
Apply (SELF_HEALING_WRITEBACK=true) Ja Entfällt — das Schreiben selbst ist der Datensatz Das Framework, direkt
Emit-Only (SELF_HEALING_WRITEBACK_EMIT_ONLY=true) Nein Ja — .self-healing-emit.jsonl, ein Datensatz pro Heilung Ein externes Tool, unter eigenem Ablauf

Persistiere die Einstellung genauso wie jede andere Self-Healing-Wahl — siehe Setup dauerhaft speichern — und überschreibe den Speicherort der Emit-Datei mit SELF_HEALING_EMIT_FILE, falls .self-healing-emit.jsonl im Projekt-Root nicht zu deiner Pipeline passt.

Eine Emit-Datei anwenden: self-healing apply

Der Emit-Only-Modus schreibt die Datei; npx cc-testframework self-healing apply ist es, was sie tatsächlich anwendet:

npx cc-testframework self-healing apply

Das liest .self-healing-emit.jsonl und wendet jede erfasste Heilung auf den Working Tree an — dieselbe Control-Quelldatei-Bearbeitung oder denselben Override-Store-Write, den der Apply-Modus selbst vornehmen würde, nur separat aufrufbar, nach dem Lauf. Das vervollständigt den zweistufigen, entkoppelten Ablauf: Lauf mit SELF_HEALING_WRITEBACK_EMIT_ONLY=true (nichts wird angewendet, alles wird in die Emit-Datei erfasst) — danach, eventuell nachdem ein externer Orchestrator die betroffenen Controls unter seinem eigenen Ablauf gelockt hat, self-healing apply ausführen, um die Änderungen tatsächlich zu schreiben.

Es ist auch für sich genommen ein echter lokaler Einzelentwickler-Workflow: lokal emitten, apply ausführen (der Default-Modus working-tree fasst Git nie an), den entstandenen Diff mit deinem eigenen git diff reviewen, und selbst committen, wenn du zufrieden bist.

Flag Default Effekt
--emit <pfad> .self-healing-emit.jsonl im aktuellen Verzeichnis, oder SELF_HEALING_EMIT_FILE Die anzuwendende Emit-Datei.
--mode working-tree\|commit\|commit-push working-tree Wie weit angewendet wird — exakt dieselben drei Stufen wie writebackMode: nur schreiben (Aufrufer committet), nur lokal committen, oder der volle Commit-und-Push-Zyklus.
--report <pfad> (keine) Schreibt die maschinenlesbare Zusammenfassung (unten) in eine Datei.
--json (aus) Gibt die Zusammenfassung auf stdout aus statt des menschenlesbaren Texts.

Es wendet beide Datensatz-Arten aus der Emit-Datei genauso an wie der Apply-Modus: target: "source"-Datensätze werden in ihrer Control-Datei umgeschrieben, target: "override-store"-Datensätze bekommen einen Eintrag im Pro-Instanz-Override-Store der besitzenden App.

Die Zusammenfassung — standardmäßig als Text ausgegeben, oder als JSON via --report/--json — sieht so aus:

{
  "records": [
    {
      "file": "pm/2_Apps/<DeineApp>/1_Controls/Control_DataGrid.ts",
      "line": 12,
      "control": "Control_DataGrid",
      "value": "",
      "target": "source",
      "originalXPath": "//*[@AutomationId='row-3']",
      "healedXPath": "(//*[@ControlType='ControlType.DataItem'])[3]",
      "status": "applied"
    }
  ],
  "appliedCount": 1,
  "skippedCount": 0,
  "errorCount": 0,
  "affectedFiles": ["pm/2_Apps/<DeineApp>/1_Controls/Control_DataGrid.ts"],
  "mode": "working-tree"
}

💡 skippedCount vs. errorCount richtig lesen. skippedCount zählt jeden Datensatz, der nicht angewendet wurde, aus egal welchem Grund — auch solche, die zusätzlich in errorCount auftauchen (ein Fehler beim koordinierten Schreiben, z.B. ein abgelehnter Push, zählt jeden Datensatz dieser Gruppe sowohl als skipped als auch als errored). appliedCount + skippedCount ergibt immer die Gesamtzahl der Datensätze; behandle errorCount als Drill-down dazu, warum manche davon übersprungen wurden, nicht als dritten, zusätzlichen Bucket.

apply ist bewusst idempotent und tolerant: Eine fehlende Emit-Datei, oder eine ohne verbleibende Datensätze, ist ein sauberer No-Op — Exit-Code 0, eine Meldung, dass nichts anzuwenden war. Das ist wichtig für einen Orchestrator, der apply defensiv erneut aufruft — er soll Erfolg sehen, nicht Fehlschlag, wenn es tatsächlich nichts mehr zu tun gibt. Exit-Code 1 ist einem echten, unerwarteten Fehler in der Apply-Logik selbst vorbehalten; Exit-Code 2 bedeutet einen --mode-Wert, der keiner der drei oben genannten ist.


Multi-User-Koordination

Apply-Modus ist sicher in einem geteilten Repository aktivierbar. Hat dein Projekt einen origin-Git-Remote, wird jedes Writeback über einen Lock koordiniert, sodass ein Kollege, der an derselben Control-Datei arbeitet — in seiner eigenen IDE oder über ein Tool, das am selben Koordinations-Protokoll teilnimmt — nie einen konfligierenden Commit gegen die Änderung des Agents bekommt:

  1. Vor dem Schreiben versucht das Framework, für jede betroffene Datei einen Lock zu erwerben.
  2. Ist eine Datei bereits von jemand anderem gelockt, wird sie für diesen Lauf übersprungen — nichts wird geschrieben, nichts committet — und der Report hält fest, wer den Lock hält und seit wann (siehe Report-Schema unten).
  3. Ist der Lock frei, zieht das Framework den aktuellen Remote-Stand, wendet das Writeback an, committet, pusht und gibt den Lock danach wieder frei.
  4. Ein Lock läuft nach 2 Stunden Inaktivität automatisch ab, sodass ein abgestürzter Prozess eine Datei nie dauerhaft blockieren kann.

Projekte ohne origin-Remote (rein lokaler Checkout) sind unbetroffen — Writeback läuft exakt wie in einem Einzel-Nutzer-Setup, ohne jeden Koordinations-Overhead.

💡 Was, wenn ein Kollege dieselbe Datei ohne jedes Koordinations-Tool bearbeitet? Der Lock schützt nur gegen einen anderen Teilnehmer, der ihn aktiv hält (ein weiterer Agent-Lauf, kompatibles Tooling eines Teammitglieds, oder ein CI-Job). Eine reine, unkoordinierte lokale Bearbeitung im Editor eines Kollegen ist für den Lock unsichtbar — die übliche Working-Tree-Hygiene gilt weiter: committe oder stashe, bevor du einen Lauf mit SELF_HEALING_WRITEBACK=true startest, und beachte die Warnung des Reporters, falls er einen unsauberen Working-Tree erkennt (siehe unten).

Commit-Identität

Der Agent committet und pusht in eigenem Namen, nicht stillschweigend in deinem. Setze die Umgebungs-Variable SELF_HEALING_AGENT_IDENTITY, um zu steuern, wie das in deinem Git-Log erscheint:

Env-var Effekt
(nicht gesetzt) Die Identität wird aus deinem eigenen git config user.name / user.email abgeleitet, mit -agent an den Namen angehängt und +agent in die E-Mail eingefügt — z.B. Jane Doe-agent <jane+agent@example.com> — klar von deinen eigenen Commits unterscheidbar.
SELF_HEALING_AGENT_IDENTITY="Name <email>" Nutzt exakt diesen Namen und diese E-Mail als Commit-Autor.
SELF_HEALING_AGENT_IDENTITY="bare-email@example.com" Nutzt die E-Mail unverändert; der Name wird aus dem Teil vor dem @ abgeleitet.

Jeder Auto-Commit trägt außerdem einen Co-Authored-By: <dein Name> <deine E-Mail>-Trailer, mit deiner eigenen, unveränderten Git-Identität — der Log zeigt also immer sowohl, wer den Job ausgeführt hat, als auch, für wen die Änderung eigentlich ist.

💡 Empfehlung für CI: Setze SELF_HEALING_AGENT_IDENTITY auf etwas Erkennbares, z.B. "cc-testframework-agent <agent@your-domain>", damit automatisierte Commits leicht aus git blame / Autorenschafts-Suchen herausgefiltert werden können.


Stabile Locators: was eine Heilung nicht schreibt

Wenn eine Heilung einen Ersatz-Locator vorschlägt, wird er auf Stabilität geprüft — nicht nur darauf, ob er das Element gerade jetzt findet:

  • Bevorzugt wird ein aussagekräftiges, stabiles Attribut, das das Element bereits selbst trägt — aria-label, eine sprechende id, oder ein data-testid-artiges Attribut auf Web; Name/AutomationId auf Desktop.
  • Vermieden werden Attributwerte, die maschinengeneriert oder sitzungsvolatil wirken — eine id mit einer angehängten Ziffernfolge oder einem hash-artigen Suffix, oder eine CSS-Klasse, die nur einen flüchtigen UI-Zustand kodiert (open, active, expanded, selected und Ähnliches) statt einer Identität.
  • Trägt das Element selbst kein stabiles Attribut, verankert sich der geheilte Locator am nächstgelegenen Vorfahren, der eines hat, und steigt von dort strukturell ab — z.B. .//div[@aria-label='Settings']/div/div für ein Ziel zwei Ebenen unter einem stabilen Container, der selbst kein verankerungswürdiges Attribut trägt.

Das ist relevant, weil ein Locator, der auf einem volatilen Wert aufbaut, heute funktioniert und beim nächsten Wechsel dieses Werts erneut bricht — derselbe Fehler, den die Heilung eigentlich beheben sollte, nur aufgeschoben. Es ist auch das, was ein namenloses, individuell gestyltes Cookie-Consent-Banner (siehe oben) auf einen stabilen Klassen-Selektor statt auf eine seitengenerierte Element-ID heilen lässt.


Geheilte Locators auf deinem XPath-Style halten

Wird eine Heilung angewendet, wird der geheilte Locator als erstes Element des Locator-Arrays der Control eingefügt (siehe oben) — eine semantische, nicht-destruktive Bearbeitung, die deine bestehenden Fallbacks bewahrt. Dieser geheilte XPath selbst kommt direkt aus einem Vision-Aufruf und kann gelegentlich @id-, [data-testid]- oder anderweitig nicht-strukturell zurückkommen, selbst wenn der Rest deiner Control dem strukturellen Style des Frameworks folgt. Standardmäßig wird das nicht geprüft — die XPath-Style-Regel (R4 in Skeleton-Konventionen) läuft nur gegen das Skeleton, sodass ein abdriftender geheilter Locator weiterhin funktioniert, aber sein Style über die Zeit vom Rest deiner Controls abweichen kann.

Aktiviere strictConventions (siehe Skeleton-Konventionen — Alle 6 Regeln auf deinen eigenen Apps durchsetzen), um diese Lücke zu schließen: Ein Pre-Commit-Hook oder CI-Step, der mit --strict / STRICT_CONVENTIONS=true läuft, validiert dann jede geänderte Datei, auch die, die ein Writeback gerade angefasst hat, und blockiert den Commit, falls der geheilte Locator zu einem verbotenen XPath-Muster passt, statt ihn still landen zu lassen. Du greifst dann manuell ein — den Befund akzeptieren und den XPath von Hand anpassen, eine weitere Heilung in der Hoffnung auf einen strukturellen Kandidaten anstoßen, oder Discovery-Modus erneut laufen lassen und vor dem Anwenden prüfen.


Erkennen, wenn ein geheilter Wert bereits zu einem parametrischen Locator passt

Manche Controls nutzen ein parametrisches Core.xpath-Template — eines, das einen Laufzeitwert interpoliert, statt ihn hartzucodieren:

// 1_Controls/Control_DataGrid.ts
Core.xpath`.//*[@class='${identifier}']`

Kommt eine Heilung für eine dieser Controls zurück, prüft der Writeback, ob der geheilte Wert schlicht eine Instanz des Templates ist, das du bereits hast. Ist das so, wird nichts geschrieben — der Eintrag landet im Report mit status: "parametric-instantiation" (siehe unten), da dein bestehender parametrischer Locator ihn bereits abdeckt. Nutzt der geheilte XPath ein anderes Attribut oder eine andere Struktur, taucht der Wert darin aber weiterhin eindeutig auf, erzeugt der Writeback ihn neu als parametrisches Tagged-Template statt als Einmal-Literal, sodass die neue Locator-Strategie jeden Wert abdeckt, den deine Testsuite jemals durch diese Control schickt — nicht nur den, der in diesem Lauf gescheitert ist.

💡 Wann du trotzdem ein Literal siehst. Bei einem kurzen Wert, einem Wert, der im geheilten XPath mehrfach vorkommt, oder einem Wert, der Substring eines anderen interpolierten Werts ist, fällt der Writeback zurück auf das Schreiben des konkreten Werts als reines String-Literal — dasselbe Verhalten wie bei einer Control ohne parametrisches Template. Das ist eine bewusste Sicherheits-Entscheidung: Ein mehrdeutiger Treffer wird übersprungen statt riskiert. Gelegentlich trotzdem einen angehängten Literal-Eintrag zu sehen, obwohl ein parametrischer Locator im Spiel war, bedeutet, dass der Writeback lieber abgelehnt als falsch geraten hat.


Pro-Instanz-Override-Store: geteilte, parametrisierte Locators heilen

Manche Controls — häufig ein View/Section-Wrapper — nutzen einen parametrischen Locator nicht, weil eine einzelne Control-Instanz im Zeitverlauf unterschiedliche Werte braucht, sondern weil DERSELBE Control-Code über viele verschiedene Screens hinweg wiederverwendet wird, z.B.:

// 1_Controls/Section.ts
Core.xpath`.//*[@aria-label='${viewName}']`

Eine Heilung gegen eine dieser Instanzen kann nie in diese geteilte Zeile geschrieben werden — das würde jede ANDERE Instanz verändern, die auf demselben Template aufbaut, und Screens kaputtmachen, gegen die die Heilung nie lief. Der Writeback erkennt diesen Fall und überspringt den Quelldatei-Edit — aber ohne einen anderen Ort, um die Reparatur zu persistieren, ginge der geheilte Locator beim nächsten Lauf verloren, was jedes Mal einen frischen Vision-Aufruf erzwingen würde. Stattdessen persistiert das Framework den vollständigen Ersatz-Locator-Block für GENAU DIESE Instanz in einer separaten, eingecheckten Datei: 2_Apps/<App>/.self-healing-overrides.json.

Bei einem späteren Lauf prüft das Framework vor der Suche diese Datei auf die exakte Instanz — abgeglichen über die Origin-Aufrufstelle, den interpolierten Wert und die gebundene refId. Existiert ein Eintrag, läuft die Suche gegen SEINEN Locator-Block statt gegen den in deiner Control deklarierten — ein vollständiger Ersatz, nie vermischt oder mit dem deklarierten Block zusammengeführt. Der geheilte Locator sitzt am Ende dieses Blocks, sodass ein davor stehender, weiterhin korrekter deklarierter Locator die Priorität behält; bei einem Wiederholungs-Lauf mit bestehendem Eintrag findet kein Vision-/API-Aufruf mehr statt.

Die Datei ist ein gewöhnliches JSON-Array, ein Eintrag pro geheilter Instanz:

[
  {
    "kind": "override",
    "control": "Section",
    "origin": "1_Controls/Section.ts:12",
    "value": "Settings",
    "refId": "settings-panel",
    "xpathBlock": [
      ".//*[@aria-label='Settings']",
      "//div[contains(@class,'panel-settings')]"
    ],
    "confidence": "high",
    "healedAt": "2026-08-14T09:12:00.000Z"
  }
]

Committe diese Datei wie jede andere Quelldatei — so teilt dein Team geheilte Locators für geteilte Templates, genau wie In-Source-Writeback sie für alles andere teilt. Eine spätere Heilung gegen dieselbe Instanz überschreibt ihren Eintrag (Last-Writer-Wins); jeder andere Eintrag in der Datei bleibt unangetastet.

Das ist ein separater Mechanismus zum oben beschriebenen In-Source-Writeback — eine Control mit einem nicht-parametrisierten oder wirklich einzigartigen Locator bekommt ihre Heilung weiterhin direkt in die Quelldatei geschrieben; nur der Shared-Template-Fall landet in dieser Datei.


Die Report-Datei

Jeder Lauf mit SelfHealingWritebackReporter schreibt .self-healing-report.json innerhalb von pm/, egal ob tatsächlich eine Heilung stattfand:

{
  "schemaVersion": 3,
  "runTimestamp": "2026-07-14T10:03:21.000Z",
  "writebackMode": "applied",
  "totalHeals": 1,
  "appliedCount": 1,
  "skippedCount": 0,
  "commitSha": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
  "entries": [
    {
      "file": "pm/2_Apps/<DeineApp>/1_Controls/Control_DataGrid.ts",
      "line": 12,
      "originalXPath": "//*[@AutomationId='row-3']",
      "healedXPath": "(//*[@ControlType='ControlType.DataItem'])[3]",
      "confidence": "high",
      "status": "applied"
    }
  ]
}
  • writebackMode spiegelt den Top-Level-Modus des Laufs — applied, discovery oder emitted (siehe Emit-Only-Modus) — nicht zu verwechseln mit der oben beschriebenen SELF_HEALING_WRITEBACK_MODE-Einstellung, die steuert, wie weit ein applied-Lauf Richtung Git geht.
  • status pro Eintrag ist eins von applied, would-apply (Discovery-Modus), skipped (das Schreiben war nicht möglich — z.B. weil sich die Datei-Struktur seit der Heilung geändert hat, oder weil die Datei von jemand anderem gelockt war; das Feld skipReason erklärt welcher Fall vorliegt), parametric-instantiation (der geheilte Wert passt bereits eins-zu-eins zu einem bestehenden parametrischen Core.xpath-Template, es musste also nichts in deine Control-Datei geschrieben werden — siehe oben), oder r4-rejected (der geheilte Locator hat die im Abschnitt Geheilte Locators auf deinem XPath-Style halten beschriebene XPath-Style-Prüfung nicht bestanden und wurde nie geschrieben — das diagnostics-Feld an diesem Eintrag nennt die verletzte Regel).
  • inputTokens / outputTokens / apiCalls / model / estimatedUsd — pro Eintrag vorhanden, wenn diese Heilung Vision aufgerufen und deren Token-Nutzung erfasst wurde, siehe Was ein Heal wirklich kostet weiter unten. estimatedUsd ist null, wenn die Tokens bekannt sind, model aber nicht in der aufgelösten Preis-Tabelle steht (nur Tokens); alle fünf Felder fehlen komplett, wenn gar keine Vision-Nutzung erfasst wurde (z.B. ein älterer Report, oder eine Heilung, die Vision nie erreicht hat).
  • commitSha — vorhanden, sobald mindestens eine Datei tatsächlich über die Koordinations-Schicht committet wurde, d.h. writebackMode zu commit oder commit-push aufgelöst hat (siehe oben) und der Commit erfolgreich war. Fehlt bei working-tree-Läufen (es wird nichts committet), bei rein lokalen Projekten und bei Läufen, in denen nichts angewendet wurde.
  • lockedByHumans — nur vorhanden, wenn der Lauf eine oder mehrere Dateien überspringen musste, weil ein anderer Lock-Halter bereits ein Writeback für sie koordinierte:

    "lockedByHumans": [
      { "file": "pm/2_Apps/<DeineApp>/1_Controls/Control_DataGrid.ts", "owner": "Jane Doe-agent (jane+agent@example.com)" }
    ]
    
  • diagnostics — eine maschinell auswertbare Zusammenfassung des gesamten Laufs, in jedem Report vorhanden, damit ein Skript oder ein externes Automatisierungs-Tool sie prüfen kann, ohne sie selbst aus entries[] neu abzuleiten:

    "diagnostics": {
      "reporterInvoked": true,
      "pendingJsonlPath": ".self-healing-pending.jsonl",
      "entriesRead": 1,
      "applied": 1,
      "skipped": 0,
      "errors": 0,
      "gitStatus": { "branch": "main", "remoteReachable": true, "uncommittedCount": 0 },
      "refLockStatus": { "activeLocks": [] }
    }
    

    diagnostics.errors zählt einen eigenständigen Fehlschlag-Typ, getrennt von einem gewöhnlichen Locked-File-Skip: er erhöht sich, wenn der koordinierte Commit/Push selbst fehlschlug, nachdem die Control-Datei bereits lokal umgeschrieben wurde (z.B. weil der Push abgelehnt wurde oder das Netzwerk abbrach). Der status dieses Eintrags bleibt trotzdem skipped — Writeback ist für die Dateien eines Laufs alles-oder-nichts —, aber sein skipReason trägt den zugrundeliegenden Git-Fehlertext statt einer Lock-Owner-Beschreibung. Behandle ein von Null verschiedenes diagnostics.errors als “prüfe deinen Working-Tree auf eine bereits lokal angewendete Änderung, bevor du erneut läufst”, nicht als “es ist nichts passiert”.

  • schemaVersion: 1-Reports (Läufe vor dieser Koordinations-Schicht) enthalten commitSha und lockedByHumans nie; Reports von vor Einführung des diagnostics-Felds enthalten auch dieses nie — behandle alle drei als optional, wenn du diese Datei in einem CI-Skript parst (z.B. um einen PR-Kommentar zu posten).
  • Prüfe diese Datei zusammen mit den Control-Diffs, die sie erzeugt hat, wenn sie in einem Pull-Request landet, oder trage sie in .gitignore ein, wenn du den transienten Report nicht versionieren willst.

💡 Working-Tree-Hygiene: Der Reporter warnt — blockiert aber nie —, wenn dein Git-Working-Tree vor dem Schreiben uncommittete Änderungen hat. Automatische Writebacks mit deinen eigenen In-Progress-Edits zu mischen macht den späteren Diff schwerer zu reviewen; committe oder stashe zuerst, wenn du einen sauberen, writeback-only Diff willst.


Pipeline-Probleme diagnostizieren

Jeder Lauf mit dem Reporter druckt eigene Status-Zeilen auf die Konsole — [SelfHealingReporter] Enabled — pending JSONL: <path> zu Beginn, wenn ein Writeback-Modus aktiv ist, und eine einzeilige applied/skipped/errors-Zusammenfassung am Ende. Für eine tiefere, bei Bedarf abrufbare Prüfung — auch außerhalb eines Test-Laufs — führt npx cc-testframework self-healing status sechs Prüfungen gegen deine aktuelle Umgebung durch:

Prüfung Was sie verifiziert
Writeback-Modus SELF_HEALING_WRITEBACK oder SELF_HEALING_WRITEBACK_DISCOVERY ist auf true gesetzt — als Umgebungs-Variable oder in der Config-Datei persistiert
Anthropic-API-Key Auflösbar per Env-Var oder OS-Credential-Store
Agent-Identität SELF_HEALING_AGENT_IDENTITY ist gesetzt — Umgebungs-Variable oder die persistierte Config-Datei, siehe Abschnitt „Commit-Identität“ weiter oben
Reporter-Registrierung SelfHealingWritebackReporter ist registriert — explizit, oder über baseConfig-Auto-Wiring (das aktuell nur auf die Umgebungs-Variable reagiert, nicht auf die Config-Datei allein — siehe die Anmerkung im Abschnitt Setup dauerhaft speichern)
Git-Origin Ein origin-Remote ist konfiguriert und erreichbar (ein rein lokales Projekt ohne einen besteht die Prüfung trivial)
Ref-Lock-Status Aktive Koordinations-Locks auf dem Remote, zur Sichtbarkeit

💡 Nutzt du nur AI_API_KEY oder set-ai-key? Die Prüfung Anthropic-API-Key oben löst genauso auf wie die Priority-Chain des OS-Credential-Stores — sie erkennt ANTHROPIC_API_KEY (Env-Var oder OS-Credential-Store), schaut aber noch nicht auf AI_API_KEY oder einen mit set-ai-key persistierten Key. Funktioniert Vision selbst (siehe Kosten und BYOK oben), meldet diese eine Prüfung den Key aber trotzdem als fehlend, ist das erwartet — führe npx cc-testframework set-ai-key für sich aus, um zu sehen, was Vision tatsächlich auflöst.

Ein gesundes Setup — hier komplett über die persistierte Config-Datei plus einen manuell registrierten Reporter, ganz ohne Umgebungs-Variablen:

$ npx cc-testframework self-healing status
Self-Healing Pipeline Status

  ✔ Writeback mode:        ACTIVE (source: config)
  ✔ ANTHROPIC_API_KEY:     available (source: store)
  ✔ Agent identity:        "cc-testframework-agent <agent@your-domain>" (source: config)
  ✔ Reporter registration: registered (manual-registration)
  ✔ Git origin:            reachable (https://github.com/<you>/<repo>.git)
  ✔ Ref-lock status:       no active locks

  ℹ Pending queue:         empty (no unpersisted heals)
  ℹ Last report:           .self-healing-report.json (1 applied, 0 skipped, 0 errors, 42s ago)

Status: HEALTHY — ready for self-healing runs

Ein fehlkonfiguriertes scheitert an einer oder mehreren Prüfungen und erklärt warum — hier wurde Self-Healing rein über die persistierte Config-Datei aktiviert (npx cc-testframework config self-healing enable, keine Umgebungs-Variable gesetzt), was für die Prüfungen Writeback-Modus und Agent-Identität genügt, aber nicht für Reporter-Registrierung: Die playwright.config.ts dieses Projekts spreadet baseConfig, ohne den Reporter manuell zu registrieren, und baseConfigs Auto-Wiring reagiert weiterhin nur auf die Umgebungs-Variable:

$ npx cc-testframework self-healing status
Self-Healing Pipeline Status

  ✔ Writeback mode:        ACTIVE (source: config)
  ✔ ANTHROPIC_API_KEY:     available (source: store)
  ✔ Agent identity:        "cc-testframework-agent <agent@your-domain>" (source: config)
  ✖ Reporter registration: playwright.config.ts spreads baseConfig, but neither SELF_HEALING_WRITEBACK nor _DISCOVERY is set to "true" — auto-wiring will not append the reporter.
  ✔ Git origin:            reachable (https://github.com/<you>/<repo>.git)
  ✔ Ref-lock status:       no active locks

  ℹ Pending queue:         empty (no unpersisted heals)
  ℹ Last report:           none yet

Status: MISCONFIGURED — see failing checks above

Lass entweder SELF_HEALING_WRITEBACK=true zusätzlich als Umgebungs-Variable gesetzt für Läufe, die den Reporter brauchen, oder registriere den Reporter einmalig manuell — beides schließt genau diese Lücke.

Übergib --format json für dieselben sechs Prüfungen plus info.pendingQueue / info.lastReport als ein einzelnes JSON-Objekt — geeignet für ein Skript, oder ein externes Test-Management-/Automatisierungs-Tool, das die Pipeline-Gesundheit vor und nach einem Lauf abfragt, ohne menschlich formatierten Text zu parsen.

Exit-Code Bedeutung
0 Gesund — jede Prüfung bestanden.
1 Fehlkonfiguriert — siehe die gescheiterten Prüfungen in der Ausgabe.
2 User-Fehler — z.B. ein ungültiger --format-Wert.

Die Pending-Heals-Queue leeren

Wurden Heilungen eingereiht, aber nie angewendet (Writeback war aus, oder der Lauf ist abgestürzt, bevor onEnd des Reporters lief), und du möchtest sie beim nächsten Writeback-aktivierten Lauf nicht erneut versuchen lassen, leere die Queue:

npx cc-testframework self-healing clear

Das fragt vor dem Löschen von .self-healing-pending.jsonl interaktiv nach Bestätigung. In einem nicht-interaktiven Kontext (CI, ein Skript) übergib --force — lässt du es dort weg, endet der Befehl mit Exit-Code 2, statt irgendetwas zu löschen, sodass eine unbeaufsichtigte Pipeline nie still ausstehende Heilungen verwirft.

Exit-Code Bedeutung
0 Geleert — oder du hast die interaktive Bestätigung abgelehnt, dann hat sich nichts geändert.
1 Keine Pending-Heals-Datei gefunden — nichts zu leeren.
2 User-Fehler — ungültige Argumente, oder ein nicht-interaktiver Kontext ohne --force.

Kosten und BYOK

Der Vision-Schritt ruft die Anthropic-API direkt von deiner Maschine oder deinem CI-Runner aus auf, mit deinem eigenen API-Key (BYOK) — es gibt keinen Proxy und keinen gehosteten Dienst dazwischen; das Framework sieht oder leitet deinen Key nirgendwo sonst weiter. Der Key wird in dieser Reihenfolge aufgelöst:

  1. AI_API_KEY — eine provider-agnostische Umgebungs-Variable, zuerst geprüft. Praktisch, um ein Secret pro Lauf zu injizieren (ein CI-Job, ein Wrapper-Skript), ganz ohne lokalen Zustand.
  2. Ein mit npx cc-testframework set-ai-key persistierter Key — siehe unten.
  3. ANTHROPIC_API_KEY — die ursprüngliche, Anthropic-spezifische Umgebungs-Variable. Funktioniert weiterhin vollständig als Fallback; ein bestehendes export ANTHROPIC_API_KEY=...-Setup läuft unverändert weiter.

Der OS-Credential-Store (npx cc-testframework config set anthropic-api-key) bleibt ebenfalls verfügbar und erreicht Vision indirekt — sein Wert wird vor dem Start eines Testlaufs automatisch nach ANTHROPIC_API_KEY gebridgt (siehe Credential Management — Priority-Chain).

Einen Key mit set-ai-key persistieren

npx cc-testframework set-ai-key sk-ant-<dein-key>
# ✔ Set aiApiKey (sk-ant-1****...**890) in /pfad/zu/deinem-projekt/.cc-testframework.local.json

npx cc-testframework set-ai-key
# aiApiKey = sk-ant-1****...**890  (source: config)

npx cc-testframework set-ai-key --clear
# ✔ Cleared aiApiKey from /pfad/zu/deinem-projekt/.cc-testframework.local.json

Der Key wird in die per-Maschine, gitignorete .cc-testframework.local.json geschrieben — er wird nie committet. Führe den Befehl ohne Argument aus, um den aktuell aufgelösten Wert (maskiert) und dessen Quelle zu sehen, ohne je den rohen Key auszugeben; --json liefert dieselbe Information als {"key":"aiApiKey","value":"sk-ant-****...**890","source":"config"}, geeignet für Skripte. --clear entfernt den persistierten Wert — die Umgebungs-Variable AI_API_KEY hat weiterhin Vorrang, sobald sie gesetzt ist, sowohl vor als auch nach dem Löschen.

💡 AI_API_KEY oder set-ai-key? Nutze AI_API_KEY, wenn du Secrets bereits pro Lauf injizierst (CI, ein Wrapper-Skript) — braucht keinen lokalen Zustand und gewinnt immer. Nutze set-ai-key für ein einmaliges Setup pro Maschine, das du nicht in jeder Shell-Session wiederholen willst. Beide speisen dieselbe Vision-Pipeline; ANTHROPIC_API_KEY funktioniert unter beiden Optionen weiterhin.

Kein Befehl und keine Log-Zeile gibt den rohen Key je vollständig aus — es erscheint nur eine maskierte Form (sk-ant-1****...**xyz), sowohl in set-ai-keys eigener Ausgabe als auch überall sonst, wo das Framework darauf verweisen könnte.

Jeder Vision-Aufruf kostet grob 1–3 US-Cent, vergleichbar mit einer einzelnen Bild-Analyse-Anfrage gegen Claude. Weil Writeback den geheilten Locator nach seiner ersten erfolgreichen Anwendung persistiert, löst ein gegebener Locator höchstens einen einzigen kostenpflichtigen Vision-Aufruf in seiner gesamten Lebenszeit aus — jeder Folge-Lauf findet den geheilten Locator direkt in der Control-Datei und ruft Vision dafür nie wieder auf.

Was ein Heal wirklich kostet

Nach jedem Vision-basierten Heal-Versuch — egal ob erfolgreich oder nicht — gibt die Konsole eine Zeile mit dem tatsächlichen Token-Verbrauch dieser Heilung aus:

[SelfHealing] Heal cost: 1842 in + 96 out tokens (~$0.0350 est., claude-opus-4-7).

Die Token-Zahlen (in/out) sind exakt — sie stammen direkt aus der Nutzungs-Angabe der Anthropic-API, summiert über alle internen Retry-Versuche dieser einen Heilung (Self-Healing versucht einen Vorschlag intern mehrfach, bevor es aufgibt). Der Dollar-Betrag ist immer eine Schätzung (~... est.) — nie als exakte Zahl dargestellt —, abgeleitet aus einer kleinen eingebauten Preis-Tabelle für das Standard-Vision-Modell (claude-opus-4-7). Dieser eingebaute Wert ist ein Beispiel-Wert zum Zeitpunkt der Erstellung, kein Live-Abgleich mit Anthropics eigener Preisseite, und kann veralten.

Um eine eigene Rate zu verwenden — deine verhandelte Preisstufe, oder ein anderes Vision-Modell —, setze CC_AI_PRICE_JSON auf ein JSON-Objekt, das Modellname auf USD pro Million Tokens abbildet:

export CC_AI_PRICE_JSON='{"claude-opus-4-7":{"inputPerMTok":16,"outputPerMTok":80}}'

Ein Eintrag braucht sowohl inputPerMTok als auch outputPerMTok; alles Fehlerhafte wird (mit einer Konsolen-Warnung) ignoriert, statt die Schätzung stillschweigend zu verfälschen. Lief eine Heilung mit einem Modell, das weder in der eingebauten Tabelle noch in CC_AI_PRICE_JSON steht, zeigt die Zeile nur Tokens — nie wird ein Dollar-Betrag geraten.

Dieselben Felder — inputTokens, outputTokens, apiCalls, model, estimatedUsd — landen im jeweiligen Eintrag in der Report-Datei, für Skripte und Dashboards. Am Ende eines Laufs mit mindestens einer Heilung gibt der Reporter zusätzlich eine Summenzeile aus:

[SelfHealingReporter] 3 heals — 5210 in + 288 out tokens (~$0.1010 est.)

💡 Setze CC_AI_PRICE_JSON einmalig (z.B. in deiner CI-Umgebung oder im Shell-Profil) statt pro Lauf — jede Heilung, in jedem Folge-Lauf, übernimmt sie automatisch.


Wo es weitergeht

  • API-ReferenzCore.xpath, SelfHealingWritebackReporter und die restlichen kuratierten Exports
  • CI-IntegrationSELF_HEALING_WRITEBACK, AI_API_KEY und den Rest in eine Pipeline verkabeln
  • Custom Steps — die @custom-Konvention für Steps ohne bisherige automatisierte Implementierung
  • Credential ManagementANTHROPIC_API_KEY im OS-Credential-Store speichern statt in einer reinen Env-Var
  • FAQ — Troubleshooting für Self-Healing-Setup, Referenz-Screenshots und API-Key-Konfiguration

📧 Fragen? Kontakt: jens.szelag@itsbusiness.ch

itsbusiness AG · Bern · Schweiz