What the agent does
The agent starts from the route config, not from guesses about file names. In an app built from remix-run/react-router-templates it reads app/routes.ts, which in the default template declares one route, index("routes/home.tsx"), and then opens each module that config points at. home.tsx there exports meta and a default component but no loader and no action; the node-custom-server template's home.tsx does export a loader that returns { message: context.get(valueFromExpressContext) }. The agent lists every exported loader, action, clientLoader and clientAction it finds this way, and the pull request says which routes had none, so a reviewer can see nothing was skipped.
Then it reads how the project is built, because a test runner has to fit it. The templates repository is a pnpm workspace (pnpm-workspace.yaml lists default, node-custom-server, the two unstable_rsc templates and .tests) with a pnpm-lock.yaml at lockfileVersion: '9.0' that resolves react-router to 8.4.0, Vite to 8.0.3 and TypeScript to 5.9.3. The only tests are Playwright files under .tests/, such as test.default.ts, which run pnpm typecheck, pnpm dev and pnpm build and then load the page in a browser; nothing calls a loader directly. vite.config.ts registers tailwindcss() and reactRouter() and sets resolve.tsconfigPaths, so ~/ imports resolve through tsconfig.json.
It adds Vitest as a dev dependency with pnpm, respecting the workspace's minimumReleaseAge: 10800 setting, and writes a vitest.config.ts that keeps resolve.tsconfigPaths but leaves out the reactRouter() plugin, which expects to drive a dev server or a build rather than a test run. Each test imports the route module and calls its function the way the framework does: a new Request("http://localhost/...") for a loader, a POST Request with a FormData body for an action, params for dynamic segments, and a context built the way the server builds it. For the node-custom-server loader that means a RouterContextProvider with context.set(valueFromExpressContext, ...), mirroring server/app.ts, so the test checks the real wiring instead of a stub.
Assertions target what the server decides. A loader test checks the returned data; an action test checks the redirect Response, its status and its Location header, or the validation errors it returns for a bad form; a thrown Response is caught and its status checked, the way ErrorBoundary in root.tsx would see it through isRouteErrorResponse. The agent runs the new test script and pnpm typecheck (react-router typegen && tsc) in its sandbox, because the +types imports in each route only exist after typegen and tsconfig.json includes every file, tests included.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Add Vitest to this React Router framework-mode app and write tests for every route loader and action. Read app/routes.ts to find every route module, and list each exported loader, action, clientLoader and clientAction. Add a vitest.config.ts that keeps the tsconfig path aliases but does not load the reactRouter() Vite plugin, and a test script in package.json, using the package manager the lockfile shows. Call each loader and action directly with real Request objects (FormData bodies for actions), real params, and the same context the server sets up, and assert the returned data, redirects (status and Location) and thrown responses. Do not mock the route modules themselves. Run the tests and the typecheck script, fix anything that fails, and open a pull request whose description lists every loader and action, the test that covers it, and any route that has neither.",
"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
The pull request adds vitest to devDependencies and the lockfile, a vitest.config.ts, a test script in package.json, and one test file beside each route module that has a loader or an action, such as app/routes/home.test.ts. Each file holds a describe block per exported function with cases for the normal path, the redirect or thrown response, and bad input where the action validates a form. When a route has no server function, as in the default template, the description says so instead of adding a test that checks nothing.
The proof is the commands and their output. The trajectory keeps the pnpm install, the Vitest run and the react-router typegen && tsc run from the sandbox with their full output, and the pull request's own checks run on the branch if the repository has CI. A reviewer reads the assertions rather than the count: each loader test should pin data the UI depends on, each action test should pin where a successful submit redirects, and a context or session the server builds should be built the same way in the test, not replaced by a hand-written object.
Run it on every new route module
Listen for GitHub's push webhook on your default branch and, when a commit adds a file under app/routes/ with no .test.ts beside it, have your server send this request with the repository in resources and the new file's path added to input. Each route gets its own run in its own sandbox and its own pull request, so the tests arrive while the author still remembers what the loader is meant to return.
With a webhook URL set on the API key, Rush POSTs run.completed or run.failed with the session URL when each run's turn ends, so your server can link the pull request in the original author's thread. Keep the commit SHA with the execution_id the request returns, so a redelivered GitHub event finds the run already started instead of starting a second one.
The use case this job follows, with its automation and trigger: Write the missing tests.
Why run it on Rush?
Writing the first tests for a framework-mode app is mostly setup: finding a Vitest config that works beside the React Router Vite plugin, getting path aliases and typegen right, and learning how to build a context the way the server does. On Rush the agent does that trial and error in its own sandbox with its own clone and its own node_modules, so your checkout, your dev server and your pnpm store are untouched while it works.
What comes back can be checked rather than trusted. Every install, test run and typecheck the agent ran is in the trajectory with its output, so a reviewer can see the tests failed before a fix and passed after it, and the pull request is an ordinary diff a person reviews before anything merges. Because each run is independent, the same request can cover several apps in a monorepo at once, one pull request per app.
Frequently asked questions
Does it test loaders through the browser or directly?
Directly. It imports the route module and calls loader or action with a real Request, params and context, which is fast and pins the server's decisions. Your existing Playwright tests, like the ones under .tests/ in the templates, stay as they are for the full page flow.
Will the React Router Vite plugin break Vitest?
It can, which is why the agent writes a separate vitest.config.ts without reactRouter() and keeps only what the tests need, such as resolve.tsconfigPaths for ~/ imports. If your vite.config.ts already skips the plugin under process.env.VITEST, it reuses that file instead.
What about loaders that read a session cookie or a database?
It builds the same inputs the server would: a Request with a Cookie header from your own session storage, or a test database when the repository has one configured. It does not replace your session or data modules with mocks, and when a dependency cannot run in the sandbox it says so in the pull request rather than faking it.
Does it work with npm, Yarn or Bun instead of pnpm?
Yes. It uses whichever lockfile the repository has and adds Vitest with that package manager, so the lockfile in the pull request matches what your CI installs.
My app was built with Remix v2. Does that count?
React Router framework mode is what Remix v2 became, so the same loader and action exports apply. If the app still imports from @remix-run/node, the agent writes the tests against those imports and does not migrate the app as part of this job.