What the agent does
The agent reads how the app runs and what it already tests. In a Symfony app like symfony/demo, composer.json requires PHP 8.4, Symfony 8, Doctrine ORM 3 and ext-pdo_sqlite; .env points DATABASE_URL at data/database.sqlite, and data/ ships that file beside a database_test.sqlite. There is no package.json: assets go through AssetMapper and importmap.php, with Stimulus controllers under assets/controllers. phpunit.dist.xml runs the suite under tests/ with the DAMA transaction bundle, and tests/Controller/BlogControllerTest.php already covers the blog index, the RSS feed, a new comment and search through WebTestCase, which drives the kernel without a browser. The README starts the app with symfony serve or php -S localhost:8000 -t public/, and .github/workflows/tests.yaml installs PHP 8.4 and 8.5 with intl, mbstring, pdo_sqlite and zip and runs vendor/bin/phpunit.
The flows come from the routes and fixtures. src/Controller/BlogController.php serves /{_locale}/blog/, paginated pages with ten posts each from Paginator::PAGE_SIZE, a post at /blog/posts/{slug}, comments at POST /blog/comment/{postSlug}/new, and /blog/search. src/Controller/SecurityController.php serves /login, whose template has username, password and remember_me inputs, and src/Controller/UserController.php guards /profile/edit and /profile/change-password with ROLE_USER. src/DataFixtures/AppFixtures.php creates jane_admin and tom_admin with ROLE_ADMIN and john_user with ROLE_USER, all with the password kitten, and gives each post five comments. Those are the accounts and the numbers the specs assert.
The agent adds a tests/e2e/ directory with its own package.json for @playwright/test, so the PHP project gains no root Node dependency, and a playwright.config.ts whose webServer copies data/database.sqlite to a scratch path, exports DATABASE_URL for it, and starts php -S 127.0.0.1:8000 -t public/ with APP_ENV=test. The specs: the blog index shows ten posts and the second page has different ones; a search for a word from the fixtures returns results; a visitor cannot open /en/profile/edit and is sent to /en/login; john_user signs in with kitten, opens a post, submits the #post-add-comment form and sees the comment appear; jane_admin reaches the admin blog controller and a plain user does not. Each spec uses the locale prefix the routes require, and the sign-in step is a shared fixture so it is written once.
It runs the suite headless in the sandbox, with the browser Playwright installs, until it passes, then runs vendor/bin/phpunit to show the PHP suite is unaffected. A tests/e2e/README.md gives the two commands to run locally, and a new job in .github/workflows/tests.yaml installs PHP the same way the existing job does, adds Node, installs the browser and runs the specs.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Write Playwright end-to-end tests for this Symfony app. Read composer.json, .env, phpunit.dist.xml, tests/Controller/BlogControllerTest.php, src/Controller/BlogController.php, src/Controller/SecurityController.php, src/Controller/UserController.php, src/DataFixtures/AppFixtures.php, the login template and .github/workflows/tests.yaml first. Put the tests in tests/e2e/ with their own package.json and a playwright.config.ts whose webServer copies data/database.sqlite to a scratch file, sets DATABASE_URL to it and starts php -S 127.0.0.1:8000 -t public/ with APP_ENV=test. Cover the paginated blog index, search, the redirect to login on a protected profile page, signing in as the fixture user and posting a comment, and the admin area being open to an admin and closed to a plain user. Share sign-in as a fixture. Run the specs headless until they pass, run vendor/bin/phpunit to show the PHP suite still passes, add a CI job for the specs, and open a pull request. If a spec exposes a real bug, mark it expected to fail and explain it in the description.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is a pull request on its own branch with tests/e2e/package.json, tests/e2e/package-lock.json, tests/e2e/playwright.config.ts, the spec files and the sign-in fixture, a short README, and the new job in .github/workflows/tests.yaml. The description lists each flow, the fixture accounts it uses, and any spec marked as expected to fail with the reason.
The transcript holds the browser install, each Playwright run with its failures until the passing one, and the vendor/bin/phpunit run afterward, so a reviewer can see the specs were run against the PHP server rather than written blind. The repository's existing Tests workflow runs on the pull request as before, and the new job runs the specs beside it.
Before merging, a reviewer checks that the specs run against a copy of the SQLite file and never the committed one, that the comment spec cleans up or uses a copy so it stays repeatable, and that the selectors come from ids and labels in the templates rather than generated class names. A follow-up message to the same session adds the /es/ locale or the password change flow, and the sandbox already has PHP and the browser installed.
Run it on every Symfony major upgrade
Before an upgrade branch merges, start this run against it: a POST /o/{org}/sessions with the branch in resources as https://github.com/<owner>/<repo>/tree/<branch> and this prompt, so the flows a user sees are covered by the time the upgrade lands. An Idempotency-Key per branch makes a retried request return the run already started.
The same prompt runs in an automation with a schedule trigger to keep the suite in step with the templates, or from your own server with a webhook_url on the API key, which receives run.completed with the session_url for the pull request.
The use case this job follows, with its automation and trigger: Write the missing tests.
Why run it on Rush?
Writing browser tests is a loop of running the server, running the spec, reading the failure and adjusting a selector, with a browser install and a PHP runtime to set up first. In a Rush sandbox the agent has PHP, the SQLite file, Node and a browser on its own machine, runs the loop as many times as it needs, and the sandbox parks when the turn ends.
The pull request carries the passing run and the PHPUnit run in its transcript, and the specs run on your repository's workflow from then on. The same prompt runs against several Symfony apps at once, each in its own sandbox, each ending in its own pull request with its own evidence.
Frequently asked questions
Why not use Symfony Panther instead of Playwright?
Panther is the PHP route and phpunit.dist.xml already has its extension commented out; it drives a browser through WebDriver from PHPUnit. The prompt asks for Playwright because that is what the team asked for; if you prefer Panther, say so in the input and the agent writes the same flows as Panther tests inside the PHPUnit suite instead.
Does this add Node to my PHP project?
Only under tests/e2e/, which has its own package.json, so the root of the repository and AssetMapper are unchanged. CI installs Node for that job alone.
Which database do the specs use?
A copy of data/database.sqlite made by the Playwright webServer command before the PHP server starts, with DATABASE_URL pointing at the copy. The committed file is never written to, and each run starts from the same fixtures.
Will the existing PHPUnit tests change?
No. tests/Controller keeps covering the controllers through WebTestCase, and the agent runs vendor/bin/phpunit after the specs to show nothing there moved. The Playwright specs add the browser-level checks, such as the Stimulus login controller and the comment form, that kernel tests cannot see.