# Write Tests for a Custom GitHub Action

Yes. An agent writes Jest tests for every input and failure path of your TypeScript action, rebuilds dist with npm run all and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/write-action-tests/github-actions/), in a Project that has your repository.

## What the agent does

The agent clones the action's repository into its own cloud sandbox and reads the contract before the code. In an action built from actions/typescript-action, `action.yml` declares one required input, `milliseconds` (default `'1000'`), one output, `time`, and `runs.using: node24` with `main: dist/index.js`. `src/index.ts` only calls `run()`, `src/main.ts` holds the logic, and `src/wait.ts` is the one helper. `.node-version` pins Node 24.4.0 and `package.json` requires `>=24.0.0`, so the agent installs that Node before `npm ci`.

Then it maps every path through `run()`. It reads `core.getInput('milliseconds')`, writes three `core.debug` lines, calls `wait(parseInt(ms, 10))` and sets the `time` output. `wait` throws `milliseconds is not a number` inside its promise when `parseInt` returns `NaN`, and `run()` catches it and calls `core.setFailed` with the message. One branch is easy to miss: the catch only calls `setFailed` when `error instanceof Error`, so anything else thrown is swallowed and the step passes. The existing `__tests__/main.test.ts` covers two cases, the output is set and a rejected `wait` fails the action; the agent's list of missing cases starts from there.

It writes the tests the way the template already does them. The repository mocks `@actions/core` and `../src/wait.js` with `jest.unstable_mockModule` from fixtures in `__fixtures__/core.ts` and `__fixtures__/wait.ts`, then imports `../src/main.js` dynamically, because the project is ESM (`"type": "module"`, `ts-jest` with `useESM`, and the test script runs under `NODE_OPTIONS=--experimental-vm-modules`). The new cases cover an empty input, a negative number, a value like `"500ms"` that `parseInt` partly accepts, the `debug` calls in order, `setOutput` never being called after a failure, a non-`Error` rejection, and `wait` itself with real timers swapped for Jest's fake ones so the suite does not sleep.

Last, it runs `npm run all`, the project's own script: `format:write`, `lint`, `test`, `coverage` (which redraws `badges/coverage.svg` with `make-coverage-badge`) and `package`, which rebuilds `dist/` with Rollup. `jest.config.js` already collects coverage from `./src/**` with a `json-summary` reporter and has a `coverageThreshold` block commented out; the agent reports the coverage it reached and leaves turning the threshold on as a choice for the maintainer.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Write Jest tests for this TypeScript GitHub Action. Read action.yml for its inputs and outputs, then src/main.ts and every helper it calls, and list every input combination and failure path, including each core.setFailed call and any catch branch that does not fail the step. Follow the mocking pattern the existing tests use (fixtures and jest.unstable_mockModule for ESM) and use fake timers instead of real waits. Install the Node version in .node-version, run npm ci, then run npm run all so formatting, lint, tests, the coverage badge and dist are all regenerated. Do not change the action's behaviour; if a path looks like a bug, add a test that documents today's behaviour and say so in the pull request. Open a pull request that lists each new test case and the coverage before and after.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **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.
2. **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.
3. **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. For an action shaped like the template, the diff adds cases to `__tests__/main.test.ts` and `__tests__/wait.test.ts`, may extend the fixtures in `__fixtures__/`, and carries the regenerated `badges/coverage.svg` and `dist/` from `npm run all`. The description lists each case by the path it covers and gives the coverage percentages from the `json-summary` report before and after.

The pull request's own checks are the ones the template ships: `ci.yml` runs `npm run format:check`, `npm run lint` and `npm run ci-test`, then runs the action itself with `uses: ./`, and `check-dist.yml` rebuilds `dist/` with `npm run bundle` and fails if it differs from what was committed. Because the agent ran `npm run all` before pushing, that comparison passes only if the committed `dist/` is the one the sources produce.

Before merging, a reviewer checks the cases the agent flagged as current behaviour rather than intended behaviour, such as a non-`Error` throw that leaves the step green, and decides whether to fix the action in a separate change.

## Run it on every pull request that changes src/

To keep tests in step with the action, start one run when a pull request touches `src/`: a small workflow step or your own server sends `POST /o/{org}/sessions` with the pull request URL, `https://github.com/<owner>/<repo>/pull/<n>`, in `resources`, so the agent works on that pull request's branch and adds the missing cases there.

Give the API key a `webhook_url` and Rush posts `run.completed` or `run.failed` when the turn ends, with the `session_url` that reads the run's result and changed files.

## Why run it on Rush?

An action's tests only count once `dist/` is rebuilt, the badge is redrawn and the formatting passes, which is the whole `npm run all` chain, and the action's own CI rejects a pull request whose `dist/` was built differently. In a Rush sandbox the agent installs the exact Node from `.node-version` on a clean machine and runs that chain end to end, so the committed bundle is not the product of a laptop's different Node or a stale `node_modules`.

The transcript keeps every command with its output, including the Jest run with its coverage table, so the reviewer sees which lines the new cases reach without checking out the branch. Several actions in one organization can each get a run at once, each in its own sandbox.

## Frequently asked questions

### Will it change my action's behaviour to make tests pass?

No. The prompt says to leave `src/` behaviour alone and to document anything that looks like a bug with a test of today's behaviour. The pull request names those cases so you can fix them deliberately.

### Does it commit the rebuilt dist folder?

Yes, because the action runs from `dist/index.js` and the template's `check-dist.yml` fails when the committed bundle differs from a fresh build. Running `npm run all` regenerates it with the same Rollup config CI uses.

### My action is ESM. Can Jest mock its imports?

Yes, the template already does it: fixtures export `jest.fn()` stand-ins, `jest.unstable_mockModule` registers them, and the module under test is imported with `await import()` after that. The agent follows the same pattern for any new dependency it has to mock.

### Does it turn on a coverage threshold?

Not by default. `jest.config.js` has the `coverageThreshold` block commented out, and the agent reports the coverage it reached instead of enforcing it. Ask for a threshold in `input` and it uncomments the block with the measured numbers.
