What the agent does
The agent starts by mapping lib/ to test/, because in a mature Phoenix app the gaps are specific modules rather than whole layers. In hexpm/hexpm, the app behind hex.pm, test/ holds 280 test files and mirrors lib/hexpm_web/ closely: every controller under controllers/ and controllers/api/ except two has a _test.exs, every LiveView under live/ has one, and so do six of the sixteen plugs. The modules without a test file of their own are the plugs Status, Vary, ReadOnly, DashboardAuth, SCIMAuth, VarselAuth, RequestLog, CacheRawBody, ReadmeContentSecurityPolicy and OrganizationSSO; the controllers OrganizationAuthController and API.SSOAuthorizationController; the helpers RepositoryAccess, ReadmeToken and Session.TTL; and twenty of the twenty-five function components under components/, from flash.ex and input.ex to table.ex and navbar.ex.
Then it reads how the project writes tests, so the new ones look like the old ones. mix.exs requires Elixir ~> 1.20, adds test/support to elixirc_paths in the test env, and aliases test to ecto.create --quiet, ecto.load --skip-if-loaded, ecto.migrate and then test. test/support/conn_case.ex imports Phoenix.ConnTest, Mox, the factory and Hexpm.TestHelpers, enables verified routes, checks out the SQL sandbox and the in-memory store, stubs Hexpm.Geo.Mock, and offers test_login/3 to sign a conn in as a user with an optional sudo timestamp. A plug test like test/hexpm_web/plugs/sudo_test.exs uses build_conn() |> Plug.Test.init_test_session(...) and calls the plug's functions directly; attack_test.exs defines a tiny inline controller that mounts the plug. A component test like components/confirmation_modal_test.exs is a plain ExUnit.Case, async: true that calls Phoenix.LiveViewTest.render_component/2 and queries the HTML with LazyHTML. config/test.exs swaps billing, pwned-password, geolocation and HTTP for Mox mocks, uses in-memory buckets and the Swoosh test adapter, and runs the endpoint with server: false.
With that, each gap gets the test its shape calls for. Plugs.Status answers 200 and halts on path_info: ["status"] and passes every other conn through; Plugs.Vary joins its option onto an existing vary header in a register_before_send callback; Plugs.ReadOnly lets GET, HEAD and OPTIONS through, refuses writes with a Retry-After of 60 unless the route is in allowed_routes, and write_routes turns a GET into a write; Plugs.CacheRawBody reads up to a million bytes into conn.assigns[:raw_body] only for the secret-scanning path; ReadmeToken.sign/3 and verify/4 round-trip through Phoenix.Token with a 30-minute max_age, so the test also checks a tampered token and an expired one; Session.TTL.within?/2 accepts an ISO 8601 timestamp inside the duration and rejects a malformed string. The two controllers go through ConnCase: GET /organizations/:organization/authenticate redirects to the return path when no requirement is outstanding, to /dashboard/security with a flash when two-factor is required, to the SSO route otherwise, and renders not found for a non-member; POST /oauth/organization_authorization answers 201 with a user session token and 422 for a client_credentials token, built with insert(:oauth_token, ...) from the factory. Components get a render_component test each for their attributes and slots.
The agent runs the suite the way .github/workflows/main.yml does, with OTP 29.1 and Elixir 1.20.4 against a postgres:18 service and MIX_ENV=test: mix deps.get, mix compile --warnings-as-errors, mix format --check-formatted and mix test. Production code under lib/ stays as it is unless a test exposes a bug, in which case the fix is its own commit and the description says so.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Write ExUnit tests for the modules in this Phoenix app that have no test file of their own. Start by listing every module under lib/<app>_web/ (plugs, controllers, live views, components and helpers) and checking for a matching _test.exs under test/; in an app shaped like hexpm/hexpm the gaps are plugs such as Status, Vary, ReadOnly and CacheRawBody, the OrganizationAuthController and the API SSOAuthorizationController, helpers such as ReadmeToken and Session.TTL, and most function components. Read test/support/conn_case.ex, data_case.ex, the factory and two existing tests of each shape first, and match their style: ConnCase with build_conn, test_login and the factory for controllers and request-level plugs; direct calls on a conn from Plug.Test for small plugs; ExUnit.Case with Phoenix.LiveViewTest.render_component and LazyHTML for components. Test behaviour through public functions and every branch the module has, not implementation details, and never stub the module under test. Change nothing under lib/ unless a test reveals a bug; if it does, fix it in a separate commit and say so. Start Postgres as .github/workflows/main.yml does, run mix compile --warnings-as-errors, mix format --check-formatted and mix test, and open a pull request listing each module now covered.",
"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 a new branch that adds test files beside the modules it covered, such as test/hexpm_web/plugs/status_test.exs, test/hexpm_web/plugs/read_only_test.exs, test/hexpm_web/readme_token_test.exs, test/hexpm_web/session/ttl_test.exs, test/hexpm_web/controllers/organization_auth_controller_test.exs, test/hexpm_web/controllers/api/sso_authorization_controller_test.exs and one components/<name>_test.exs per component taken. Nothing under lib/ changes unless a test found a bug, and mix.lock does not move.
The description lists each module with the branches its tests cover, quotes the mix test summary and the results of the compile and format checks, and names any module it left out, such as VarselAuth, when a test would need a signing key the config does not provide and says what config the test would need.
Before merging, a reviewer reads the ReadOnly and OrganizationSSO tests first, since those plugs decide what a request may do, checks that controller tests use test_login and the factory rather than inserting rows by hand, and that component tests assert on rendered attributes and text instead of on CSS class lists that change with a redesign.
Run it on every new plug, controller or component
Create an automation in the Project with this prompt and a schedule trigger (cron 0 2 * * * in your timezone), so each night the agent diffs lib/ against test/ and writes tests for whatever was added that day without a test file. A night with no gap ends with no pull request and a one-line result. Keep max_runs_per_hour at 1 so a retried fire does not start two runs on the same tree.
To start it from a pull request instead, give the automation a github trigger on pull_request.opened and pull_request.synchronize; the event follows the prompt, and the agent limits itself to the modules that pull request added. Its runs are not tied to an API key, so read the results from the automation's recent_runs or the Project's sessions rather than from a key's webhook.
The use case this job follows, with its automation and trigger: Write the missing tests.
Why run it on Rush?
A Phoenix app this size needs Postgres, the sandbox, Mox stubs and the factory before a single controller test runs, and the first mix test compiles 158 dependencies; on a laptop that is an afternoon before the first assertion. On Rush the agent takes that setup in its own sandbox with the OTP and Elixir versions CI pins, writes tests in the shapes the project already uses, and iterates until mix compile --warnings-as-errors and mix format --check-formatted are clean.
The pull request carries the list of modules and branches covered and the exact mix test output, so a reviewer reads tests rather than a coverage percentage. The same request runs against every Phoenix app you maintain at once, each in its own sandbox.
Frequently asked questions
The suite already has 280 test files. What is left?
Modules with no test file of their own: in hexpm, ten of sixteen plugs, two controllers, three helpers and twenty function components. Some of their lines are exercised indirectly through controller tests, but a plug like ReadOnly has branches (allowed_routes, write_routes, Retry-After) that only a direct test pins down.
Does it need Postgres and the other services?
Postgres, yes: ConnCase and DataCase check out the Ecto sandbox, and the test alias creates and migrates the database. Everything else is swapped in config/test.exs: billing, pwned passwords, geolocation and HTTP are Mox mocks, storage is in-memory, and mail goes to the Swoosh test adapter.
Will it use Mox to stub the module it is testing?
No. Mox is for the behaviours config/test.exs already mocks, such as Hexpm.Billing.Mock. A plug or controller is tested by sending it a conn, and a component by rendering it; stubbing the module under test would prove nothing.
How does it test a plug that reads runtime config?
With the project's own app_env/3 helper from test/support/test_helpers.ex, which sets an application environment value for the test and restores it afterwards. DashboardAuth reads :dashboard_user and :dashboard_password that way, and ReadmeContentSecurityPolicy reads :host and :img_url.
Which Elixir and OTP does it run?
The ones .github/workflows/main.yml pins, Elixir 1.20.4 on OTP 29.1, which also match the Dockerfile's build arguments. The description names the versions and the Postgres image used.