When a Playwright test breaks overnight, a Claude agent diagnoses the failure, classifies it as test-issue or website-issue, and — if it's a fix the agent is allowed to make — opens a pull request before anyone gets to their desk. The workflow is in our public repo.
Autonomous test-healing for Drupal teams.
Four things that happen when a test fails.
The agent doesn't “fix your tests.” It reads the failure, decides what kind of failure it is, and takes the action its operating mode allows. Sometimes that's a PR. Sometimes it's an issue with a diagnosis. Sometimes it's nothing.
A Views block label changes.
The site is actually broken.
It's just flake.
The guardrails are in the repo.
Every night, Playwright tests run against this site in CI. When one fails, a GitHub Action invokes a Claude agent with the failure artifacts and the constraints it's allowed to operate under. Here's the top of heal-tests-claude.yml — the file that decides what the AI is permitted to touch:
# Only fix test code in CI. Site-code changes require a human.
operating_mode: ci
permissions:
modify_test_code: true # selectors, assertions, waits
modify_site_code: false # PHP, Twig, JS, CSS - never in CI
open_pull_request: true
auto_merge: false # a human reviews every PR
classification:
test_issue: [selector_outdated, assertion_outdated, timing, url_change]
website_issue:[element_missing, server_error, broken_form, empty_content]
Locally, the same agent runs with operating_mode: local — the engineer at the terminal can let it fix site code too, because changes can be verified against a running DDEV instance before commit. The mode is explicit. The permission flags are explicit. Nothing is implicit.
The toolkit we run AI on top of.
The agent isn't a testing framework. It's a layer that sits on top of the frameworks your team already runs — or should be running.
Playwright.
Cypress.
ATK — Automated Testing Kit.
Testor.
This site is the first dogfood.
Every night, Playwright tests run against the very site you're reading. When a test breaks, the workflow above fires. For the full narrative — architecture choices, rasterization strategy, mobile-spacing reconciliation, and every other decision that shaped this site — read how we built this site.
- await expect(page.getByRole('heading', { name: 'Latest articles' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Recent articles' })).toBeVisible();
One of the commits the agent opened on this repo. A config-rename ripple — Views UI changed the block label; the Playwright assertion had to catch up. The agent read the failure output, read the views.view.*.yml diff, decided it was a coordinated rename, and shipped the PR. Closed the loop in about eleven minutes, start to merge.
Over the last 90 days, [N] tests have been auto-healed on this site without human intervention.