Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Meza-dev/Ghostly/llms.txt

Use this file to discover all available pages before exploring further.

CSS selectors and test IDs are brittle. A component rename, a library upgrade, or a design refresh can change data-testid="submit-btn" to data-testid="login-submit" overnight, turning a perfectly healthy test suite into a wall of red. Traditional test frameworks require a developer to diagnose the failure, hunt down the new selector, and push a fix — even when the underlying application behavior hasn’t changed at all. Ghostly’s self-healing system addresses this at runtime. When a step fails because its selector can’t be found on the page, the healer agent captures the current state of the DOM, asks the LLM to identify an alternative selector for the intended element, and retries the step — all without stopping the run or requiring any human input.
Self-healing only activates in assisted mode (v2). Runs using runFlow directly (scripted steps without assist.v2: true) fail immediately on selector errors and do not attempt recovery.

The Problem in Detail

Consider a login form. Your test step reads:
{ "action": "click", "selector": "[data-testid=\"login-btn\"]" }
After a refactor the button is now [data-testid="submit-login"]. Playwright throws a timeout error because the original selector is nowhere on the page. Without self-healing, the run fails and a developer has to intervene.

How the Healer Works

When a step fails inside the assisted v2 loop, the following sequence fires up to maxHealingAttemptsPerStep times (0–3):
1

Detect Failure

The pipeline catches the step error and emits a step_failure event. If maxHealingAttemptsPerStep is greater than zero, healing begins immediately.
2

Capture Current Accessibility Tree

The Observer calls captureObserverSnapshot(), which uses Playwright’s ariaSnapshot({ mode: "ai" }) on the live page. This produces a structured markdown of every visible interactive element — buttons, links, textboxes, headings — without any CSS selector dependencies.
3

Ask the LLM for an Alternative

The Healer function (HealerFn) receives a HealerContext containing the goal, the failed step, the error message, the current snapshot, the execution history, and any codeHints from the Scanner package. The LLM is asked to propose replacement steps that accomplish the same intent.
4

Sanitize and Validate

sanitizeHealerSteps() filters out ambiguous selectors (bare button, input, etc.), rejects [ref=eN] accessibility reference tokens that aren’t valid CSS, and caps the response at 3 steps. Invalid proposals are discarded silently.
5

Apply the Healed Steps

Each sanitized replacement step is executed in order. A heal_action event is emitted for each. If the healer’s steps succeed and the original step is no longer needed (because the healer replaced it), it is marked dropped in the plan progress.
6

Retry the Original (if needed)

If the healer’s steps only unblocked the path rather than replacing the original action, the original step is retried once more. Both outcomes are recorded in the plan progress.

Healing Events

The entire healing lifecycle is observable through the event stream:
EventPayload highlights
heal_startattempt, maxAttempts, nodeCount (snapshot size)
heal_actionstep (the replacement), rationale (LLM explanation)
heal_successstep, replacedByHealer, skippedOriginal
heal_failureerror or reason: "no-valid-steps"

CodeHints: Source-Code Context for the Healer

The Ghostly Scanner package performs static AST analysis of your application’s source code to extract component metadata. This metadata is passed to the healer as codeHints, giving the LLM accurate knowledge of what test IDs and aria labels actually exist in the codebase rather than guessing from the DOM snapshot alone. The codeHints object is validated by codeHintsSchema in apps/api/src/routes/run.ts:
// From packages/runner/src/assist/types.ts

export type CodeHints = {
  components?: Array<{
    name: string;
    file?: string;
    testIds?: string[];
    ariaLabels?: string[];
    roles?: string[];
  }>;
  forms?: Array<{
    name: string;
    file?: string;
    inputs?: Array<{
      testId?: string;
      ariaLabel?: string;
      id?: string;
      name?: string;
      placeholder?: string;
      type?: string;
    }>;
    submitTestId?: string;
    submitLabel?: string;
  }>;
  routes?: Array<{
    path: string;
    component?: string;
  }>;
  selectors?: {
    byTestId?: Record<string, string>;
    byAriaLabel?: Record<string, string>;
  };
};

Example CodeHints Payload

Here is what a typical codeHints payload looks like when the Scanner processes a React login form component:
{
  "components": [
    {
      "name": "LoginForm",
      "file": "src/features/auth/LoginForm.tsx",
      "testIds": ["email-input", "password-input", "submit-login"],
      "ariaLabels": ["Email address", "Password"],
      "roles": ["form", "button"]
    },
    {
      "name": "DashboardLayout",
      "file": "src/layouts/DashboardLayout.tsx",
      "testIds": ["sidebar-nav", "user-menu"],
      "ariaLabels": ["Main navigation", "User account menu"]
    }
  ],
  "forms": [
    {
      "name": "LoginForm",
      "file": "src/features/auth/LoginForm.tsx",
      "inputs": [
        { "testId": "email-input", "type": "email", "ariaLabel": "Email address" },
        { "testId": "password-input", "type": "password", "ariaLabel": "Password" }
      ],
      "submitTestId": "submit-login",
      "submitLabel": "Sign in"
    }
  ],
  "routes": [
    { "path": "/", "component": "LoginPage" },
    { "path": "/dashboard", "component": "DashboardLayout" }
  ],
  "selectors": {
    "byTestId": {
      "email-input": "input[data-testid=\"email-input\"]",
      "submit-login": "button[data-testid=\"submit-login\"]"
    },
    "byAriaLabel": {
      "Email address": "input[aria-label=\"Email address\"]"
    }
  }
}
Passing codeHints significantly improves healer accuracy on well-typed React/Vue/Svelte codebases. The Scanner CLI generates this payload automatically — run it as part of your CI pre-test step and pass the output in the codeHints field of the run request.

Selector Sanity Rules

The healer applies strict validation before accepting any LLM-proposed selector. Proposals are rejected if they:
  • Are bare tag names: button, input, textarea, select, a
  • Are overly generic type selectors: button[type="submit"], input[type="text"], input[type="password"], input[type="email"], [type=submit], form button, form input
  • Contain accessibility ref tokens: [ref=e42] (internal to Playwright’s aria engine, not valid CSS)
This prevents the healer from replacing a broken specific selector with an equally-broken generic one that matches dozens of elements on the page.
Setting maxHealingAttemptsPerStep: 0 disables self-healing entirely. The run will fail immediately on any unresolvable selector, which is the same behavior as a non-assisted runFlow call. Use 0 only when you want strict, zero-tolerance selector enforcement.

Build docs developers (and LLMs) love