All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Write the missing tests.

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.