Observed since 21 Oct, 10:34 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
LangGraph-based AI-driven E2E test self-healing engine (CLI core + CI integration)
Using LLM APIs? This project optionally supports OrcaRouter: one API for 200+ models with zero markup and cost-aware routing. Using this referral link supports the project with a 5% commission at no additional cost to you.
OrcaRouter is an optional LLM provider for this project. See Using OrcaRouter for the configuration.
A self-healing engine that automatically repairs broken Playwright E2E tests with an AI agent built on LangGraph. When a UI change renames or restructures an element and a test's selector breaks, the engine diagnoses the failure, patches the broken selector/wait, verifies the new selector against the live DOM, then re-runs the test until it passes (or a retry cap is hit) and writes the fix back — as a local CLI or a CI GitHub Action that opens a patch PR.
Scope guardrail: the engine only fixes failing locators and wait conditions. It never touches assertions or test logic, and every patch stays human-reviewable.
Auto-healing patches the test. That is fast, but on its own it can look like papering over a real problem — the source that broke the selector. So the engine offers a second mode:
heal (default) — patch the broken selector/wait and re-run until green. Best when the
UI change is intentional and the test simply needs to catch up.review — diagnose why the selector broke and post source-level suggestions as
inline PR comments (e.g. "this className rename broke #cta; add a stable
data-testid or use getByRole"). It never edits the test — it advises the fix at the
source and pushes teams toward resilient, accessibility-first selectors.Same diagnosis engine, two outputs: a patch, or a review. Pick per-project or per-PR.

Four layers drive a LangGraph repair loop:
e2e-healer); everything, including CI, calls it.git diff into compact,
hallucination-resistant context (the failing selector + the DOM attribute that changed).Diagnoser → Patch Generator → Selector Verifier → Test Runner,
looping via a conditional Router until the test passes or max_loops is reached.npx playwright test via subprocess to validate each attempt. ┌──────────┐ ┌─────────────────┐ ┌───────────────────┐ ┌─────────────┐
──▶│ Diagnoser│──▶ │ Patch Generator │──▶ │ Selector Verifier │─┬─▶│ Test Runner │──┐
└──────────┘ └─────────────────┘ └───────────────────┘ │ └─────────────┘ │
▲ ▲ verify fail (0/2+ match) → repatch ┘ │
│ └───────────────────────────────────────────────────────┘│
│ fail & loop_count < max │
└─────────────────────────────── Router ◀───────────────────────────────────┘
│ pass or loop cap
▼
[End]
The Selector Verifier skips gracefully (loop proceeds unverified) when
E2E_HEALER_APP_URLis empty or the page is unreachable (e.g. Node/Playwright not installed) — tooling problems never block a heal.
See docs/design.md for the full design, and
docs/shadow-testing.md for the planned Shadow Testing pipeline.
The flagship workflow: run your suite and auto-heal on failure, opening a patch PR for review:
- name: E2E self-heal
id: heal
uses: Lee-Dongwook/E2E-Self-Heal@v0.4.0
with:
test-path: tests/example.spec.ts
nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }}
diff-base: ${{ github.event.pull_request.base.sha }}
app-url: http://localhost:4173 # optional: enables live selector verification
- name: Open patch PR
if: steps.heal.outputs.outcome == 'healed'
uses: peter-evans/create-pull-request@v6
with:
body-path: ${{ steps.heal.outputs.summary-path }}
branch: e2e-self-heal/${{ github.run_id }}
The action's outcome output is passed | healed | unhealed (heal mode) or reviewed
(review mode). For a Playwright suite in a subdirectory, pass working-directory:. A
runnable self-demo that heals this repo's own examples/ project lives in
ci/github-workflow.example.yml.
To run as a PR review bot instead, pass mode: review and post the findings as inline PR
comments — a ready-to-copy workflow lives in
ci/github-review-bot.example.yml:
- name: E2E review
id: review
uses: Lee-Dongwook/E2E-Self-Heal@v0.4.0
with:
mode: review
test-path: tests/example.spec.ts
nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }}
diff-base: ${{ github.event.pull_request.base.sha }}
# then read steps.review.outputs.review-path and post inline comments (see the example)
The examples/ project is a runnable React + Vite app the engine heals
end to end. It ships green — on a clean checkout every spec passes against the real app.
Applying a scenario's real git diff breaks it: the
id-rename scenario renames the button id
submit-btn → submit, so scenarios/id-rename/spec.ts times out on #submit-btn.
Running the healer against it (with a live NVIDIA key) produces:
diagnoser_finished
patch_generator_finished instruction_count=1
selector_verify_started selector_count=1 url=http://localhost:4173
selector_verify_passed counts={'#submit': 1}
test_runner_passed loop_count=0
fixed after 0 loop(s)
- await page.click("#submit-btn");
+ await page.click("#submit"); # assertion on "Thanks!" left untouched
Reproduce it yourself (the demo uses pnpm): see
examples/README.md.
A real run against a landing-page CTA test. A UI refactor renamed the button id
#enter-demo-btn → #demo-cta-btn, so demo-cta.spec.ts started failing on
locator.click. Pointed at the file, the engine diagnosed the break against the git diff,
patched only the broken selector (leaving the toHaveURL assertion untouched), re-ran
the suite, and passed on the first attempt — end to end on NVIDIA NIM
(integrate.api.nvidia.com, openai/gpt-oss-120b):

playwright_run_finished passed=False # original selector times out
diagnoser_started loop_count=0
diagnoser_finished
patch_generator_finished instruction_count=1
test_runner_started
playwright_run_finished passed=True
repair_run_finished is_success=True loop_count=0
fixed after 0 loop(s)
test('guest enters the demo workspace from the landing CTA', async ({ page }) => {
await page.goto('/')
- await page.click('#enter-demo-btn')
+ await page.click('#demo-cta-btn')
await expect(page).toHaveURL(/\/w\//) // assertion left untouched
})
Requires Python 3.13+ and a Playwright project (Node) in your repo.
Recommended — one-line global install:
pipx install ai-driven-e2e
# or, before PyPI release:
uv tool install git+https://github.com/Lee-Dongwook/E2E-Self-Heal.git
Then in any Playwright project:
cp .env.example .env # set E2E_HEALER_LLM_API_KEY (or your provider's key)
e2e-healer tests/login.spec.ts
Works with NVIDIA NIM (default), OpenAI, Anthropic (Claude), DeepSeek, OrcaRouter, or a local Ollama model —
see Configuration to pick one. For the default, get a free NVIDIA NIM API
key at build.nvidia.com (default model openai/gpt-oss-120b).
git clone https://github.com/Lee-Dongwook/E2E-Self-Heal.git
cd E2E-Self-Heal
uv sync --extra dev
uv tool install --force . # global `e2e-healer` from this checkout
Re-run uv tool install --force . after pulling changes.
# Heal the WHOLE suite — run every test, then repair each failing file (aggregate summary):
uv run e2e-healer
# Heal a single failing test (with no --log, the tool runs it to capture the failure):
uv run e2e-healer tests/example.spec.ts
# Preview only — run the loop but write nothing:
uv run e2e-healer tests/example.spec.ts --dry-run
# Disable local healing-history lookup and storage for this run:
uv run e2e-healer tests/example.spec.ts --no-memory
# Feed a pre-captured log and a PR-scoped diff (the CI path):
uv run e2e-healer tests/example.spec.ts --log playwright.log --diff-base origin/main --json
# Enable live-DOM selector verification against a running app:
uv run e2e-healer tests/example.spec.ts --app-url http://localhost:4173
# Review mode — suggest source-level fixes instead of patching (never edits the test):
uv run e2e-healer review tests/example.spec.ts --log playwright.log --diff-base origin/main --json
# Compare the Diagnoser's full-file and semantic-context prompt-token estimates for shipped examples:
uv run e2e-healer benchmark
Pass --root <dir> to anchor all relative paths (the spec, --diff, test-results/, and the
git diff/Playwright subprocess cwd) to a project root — useful when invoking the CLI from a
directory other than the project root (e.g. from a Playwright Reporter).
Exit code is 0 when the test is healed, non-zero otherwise. --json prints a
machine-readable summary to stdout (human output goes to stderr) so CI can branch on it:
a RepairSummary for a successful single-file heal, a RefusalReport when it declines a
single-file repair, a SuiteSummary for suite mode, and a ReviewReport in review mode.
Every emitted summary is self-describing: it carries a schema_version (currently "2.0")
and a kind discriminator ("repair" / "refusal" / "suite" / "review") so the CI
wrapper can dispatch without guessing on keys. e2e-healer <path>
is shorthand for e2e-healer heal <path>; review is a separate subcommand that emits a
ReviewReport (findings anchored to the changed source line) and always exits 0 — the
CI wrapper branches on has_findings.
Healing history is enabled by default. Pass --no-memory to fully bypass local history lookup
and verified-repair storage for a run; pass --memory explicitly to enable the default behavior.
Run uv run e2e-healer benchmark to render a rich table comparing the full-file and
semantic-context Diagnoser prompt sizes for each checked-in breakage scenario. It needs neither
provider credentials nor a running Playwright app. Counts include the Diagnoser system and user
prompt content, estimated with the fixed cl100k_base tokenizer; they are a stable comparison
metric, not a provider-billed-token statement. tiktoken may download this tokenizer on first
use, then serves it from its local cache. A scenario that cannot yield an enclosing JSX node is
labeled whole-file fallback and correctly reports zero savings.
All settings use the E2E_HEALER_ prefix (see .env.example). LLM
configuration is provider-neutral: pick a backend with E2E_HEALER_LLM_PROVIDER and
configure it via the generic LLM_* vars. The legacy NVIDIA_* vars still work — when
the provider is nvidia they are folded into the generic ones, so existing setups need no
changes.
| Provider | Install | Required env | Structured outputs | Notes |
|---|---|---|---|---|
nvidia | built-in (default) | E2E_HEALER_LLM_API_KEY (or legacy NVIDIA_API_KEY) | strict json_schema | OpenAI-compatible NIM endpoint; default openai/gpt-oss-120b. |
openai | built-in | E2E_HEALER_LLM_API_KEY or OPENAI_API_KEY | strict json_schema (native) | Set LLM_BASE_URL for Azure / OpenAI-compatible endpoints. |
anthropic | ai-driven-e2e[anthropic] | E2E_HEALER_LLM_API_KEY or ANTHROPIC_API_KEY | tool-use | No OpenAI response_format; schema enforced via Claude tool-use. |
ollama | ai-driven-e2e[ollama] | none (local) | native JSON-schema format | Fully offline; smaller models are less reliable at strict JSON. |
deepseek | built-in | E2E_HEALER_LLM_API_KEY or DEEPSEEK_API_KEY | Responses API json_schema | Stateless Responses API; default deepseek-v4-flash. |
orcarouter | built-in | E2E_HEALER_LLM_API_KEY or ORCAROUTER_API_KEY | LangChain default | OpenAI-compatible endpoint; default base URL is https://api.orcarouter.ai/v1. |
All providers read the generic E2E_HEALER_LLM_MODEL / E2E_HEALER_LLM_MAX_TOKENS /
E2E_HEALER_LLM_BASE_URL. On a structured-output parse failure the engine retries
(tenacity) and then falls back to the Patch Generator feedback loop, so a flaky JSON
response never crashes the run.
Set the provider to openai and supply a key — either the generic E2E_HEALER_LLM_API_KEY
or an existing standard OPENAI_API_KEY (used as a fallback). Patch/review use OpenAI's
native strict Structured Outputs. Point E2E_HEALER_LLM_BASE_URL at an Azure or
OpenAI-compatible endpoint to override the default.
E2E_HEALER_LLM_PROVIDER=openai
OPENAI_API_KEY=sk-... # or E2E_HEALER_LLM_API_KEY=sk-...
E2E_HEALER_LLM_MODEL=gpt-4o-mini
OrcaRouter is an optional OpenAI-compatible provider. Select it explicitly; it never
changes the default NVIDIA configuration. Supply either the generic
E2E_HEALER_LLM_API_KEY or ORCAROUTER_API_KEY. ORCAROUTER_BASE_URL defaults to
https://api.orcarouter.ai/v1, and can be overridden without changing application code.
E2E_HEALER_LLM_PROVIDER=orcarouter
ORCAROUTER_API_KEY=your_orcarouter_api_key_here
E2E_HEALER_LLM_MODEL=your_structured_output_model
# ORCAROUTER_BASE_URL=https://api.orcarouter.ai/v1
Anthropic is an optional dependency — install the extra so non-Anthropic users don't pull it in:
pip install "ai-driven-e2e[anthropic]" # or: uv sync --extra anthropic
Then set the provider and a key — either E2E_HEALER_LLM_API_KEY or an existing standard
ANTHROPIC_API_KEY (fallback). Claude has no OpenAI-style response_format, so
PatchOutput/ReviewOutput are enforced via tool-use.
E2E_HEALER_LLM_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-... # or E2E_HEALER_LLM_API_KEY=sk-ant-...
E2E_HEALER_LLM_MODEL=claude-sonnet-4-6
Run fully offline with no API key. Ollama is an optional dependency:
pip install "ai-driven-e2e[ollama]" # or: uv sync --extra ollama
Start Ollama and pull a model, then point the provider at it. Structured outputs use
Ollama's native JSON-schema format, so pick a model that handles structured output
well — recommended: llama3.1, qwen2.5-coder, or mistral-nemo. Smaller/older models are
less reliable at strict JSON; on a parse failure the engine retries and falls back to the
Patch Generator feedback loop.
E2E_HEALER_LLM_PROVIDER=ollama
E2E_HEALER_LLM_MODEL=llama3.1
# E2E_HEALER_LLM_BASE_URL=http://localhost:11434 # default; override for a remote host
DeepSeek is exposed through its Responses API (client.responses.create() at
https://api.deepseek.com) rather than the LangChain chat path, so the provider is a small
built-in adapter that speaks that API directly. Supply a key — either
E2E_HEALER_LLM_API_KEY or an existing standard DEEPSEEK_API_KEY (fallback). The default
model is deepseek-v4-flash (the Responses API currently supports only this model;
deepseek-v4-pro is pending). Setting E2E_HEALER_LLM_MODEL to any other model is
rejected unless E2E_HEALER_LLM_BASE_URL points at a custom OpenAI-compatible endpoint
that may serve it. Structured output is requested with the Responses API text.format
json_schema parameter, and the returned JSON is validated against
PatchOutput/ReviewOutput, so schema enforcement holds without LangChain.
E2E_HEALER_LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=sk-... # or E2E_HEALER_LLM_API_KEY=sk-...
# E2E_HEALER_LLM_MODEL=deepseek-v4-flash # default; set only to override
Try it live against the API (set DEEPSEEK_API_KEY first):
uv run e2e-healer --help >/dev/null # import smoke check
E2E_HEALER_LLM_PROVIDER=deepseek uv run python - <<'PY'
from app.llm import generate_diagnosis
print(generate_diagnosis("You are a terse assistant.", "Say OK"))
PY
| Variable | Default | Purpose |
|---|---|---|
E2E_HEALER_LLM_PROVIDER | nvidia | LLM backend: nvidia, openai, anthropic, ollama, deepseek, orcarouter |
E2E_HEALER_LLM_API_KEY | — | API key for the selected provider |
E2E_HEALER_LLM_BASE_URL | — | OpenAI-compatible endpoint (empty = SDK default) |
E2E_HEALER_LLM_MODEL | — | Structured-Outputs-capable model |
E2E_HEALER_LLM_MAX_TOKENS | 4096 | Completion token cap (headroom for reasoning) |
E2E_HEALER_NVIDIA_API_KEY | — | NVIDIA NIM API key (legacy; maps to LLM_API_KEY) |
E2E_HEALER_NVIDIA_BASE_URL | https://integrate.api.nvidia.com/v1 | OpenAI-compatible endpoint (legacy) |
E2E_HEALER_NVIDIA_MODEL | openai/gpt-oss-120b | Structured-Outputs-capable model (legacy) |
E2E_HEALER_NVIDIA_MAX_TOKENS | 4096 | Completion token cap (legacy) |
ORCAROUTER_API_KEY | — | OrcaRouter API key (used when provider is orcarouter) |
ORCAROUTER_BASE_URL | https://api.orcarouter.ai/v1 | OrcaRouter OpenAI-compatible endpoint |
E2E_HEALER_MAX_LOOPS | 3 | Repair loop cap |
E2E_HEALER_PLAYWRIGHT_CMD | npx playwright test | Playwright invocation |
E2E_HEALER_VERIFY_SELECTORS | true | Toggle live-DOM selector verification |
E2E_HEALER_APP_URL | — | URL the Selector Verifier loads (empty = skip) |
E2E_HEALER_NODE_CMD | node | Node executable for the verifier |
E2E_HEALER_SANDBOX_MODE | relaxed | strict, relaxed, or off |
E2E_HEALER_WORKSPACE_ROOT | . | Root for strict path checks |
E2E_HEALER_WRITE_GLOBS | *.spec.js,... | Writable test-file globs |
E2E_HEALER_DENY_GLOBS | .env,.git/**,... | Paths blocked by the sandbox |
E2E_HEALER_ALLOW_TEMP_HELPER | true | Permit selector verifier helper file |
The
--app-urlCLI flag overridesE2E_HEALER_APP_URL. To actually run selector verification locally, the Playwright project needs browsers installed (npm install && npx playwright install).
make install # uv sync --extra dev
make check # ruff lint + format check + pyright
make coverage # pytest with a 90% coverage floor
make test # pytest
See CONTRIBUTING.md.
The docs-site/ directory contains our Docusaurus documentation site — this is where frontend contributors can help.
To run it locally:
cd docs-site
npm install
npm run start # dev server with hot reload
npm run build # production build
Picking up a docs-site component issue? Read docs-site/DESIGN.md first — it's the locked design spec (tokens, layout, dark-mode rules) all components must follow.
Contributions of every size are welcome — bug reports, docs, tests, or code. Start with
CONTRIBUTING.md, then browse
good first issues
and help wanted.
APP_URL in v1. Elements that
only appear after clicks/navigation aren't verified here; the Test Runner remains the final
arbiter (failure-time snapshot capture is planned).git diff.Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.5.0 | 22 Sept 2026 | 19 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 21 Oct, 10:34 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:817220e7e0eef203fae07359a3d3c41f1652e94d18d4e42df8b3803f08005cce