Testing in CI
The goldens as the gate before a deploy, in GitHub Actions, and what to run nightly.
Your goldens are the gate before production. Run them in CI on every pull request, and a red run stops the deploy. A second, scheduled job spends more: several models, spoken callers, and the drift of the judges over real calls.
How it works#
The CLI needs two things in CI: the project, and a key. It reads PINECALL_KEY and PINECALL_URL from the
environment and nothing else. PINECALL_URL defaults to https://cloud.pinecall.io, so on Pinecall Cloud
the key is enough.
Use a sandbox server token: make one in the console's Settings ▸ Tokens, in the sandbox, and keep it
in the repository's secrets as PINECALL_KEY. It starts with pc_test_ and acts only in the sandbox, so
a test run never touches production's calls.
| Job | When | What it runs | Costs |
|---|---|---|---|
| Unit tests | every push | npm test |
nothing |
| Goldens | every pull request, before a deploy | pinecall test, pinecall docs eval, pinecall memory eval |
real model calls; embeddings |
| Nightly | weekday nights | every approved case, two models, pinecall remember, spoken personas, pinecall runs drift |
real calls |
pinecall test serves the agent from the CI machine itself, the way it does on your laptop, so your tools
run there against whatever they reach. The gateway plays and judges the conversations.
The gate#
Make the token#
A sandbox server token, made on the sandbox's console the way
Run it on your own server shows. It is shown once, as
PINECALL_KEY=pc_test_….
Keep it in GitHub#
gh secret set PINECALL_KEY
Paste the value when asked. It never goes on a command line.
Add the workflow#
name: agent
on:
pull_request:
push:
branches: [main]
# The gateway runs one suite per agent at a time: queue, never overlap.
concurrency:
group: goldens
cancel-in-progress: false
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
PINECALL_KEY: ${{ secrets.PINECALL_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- name: unit tests
run: npm test
- name: goldens
run: npx pinecall test
- name: retrieval goldens
run: |
npx pinecall docs eval
npx pinecall memory eval
- name: keep what broke
if: failure()
uses: actions/upload-artifact@v4
with:
name: broken-goldens
path: .pinecall/evals/
npx pinecall is the CLI your project pins in package.json, which pinecall new puts there. Each verb
exits 1 when something it measures did not hold and 2 when it cannot run, so a step fails on either.
The retrieval goldens are optional. Leave out
docs evalwhen the agent reads no base, andmemory evalwhen the project has notest/<name>/goldens/memory.json.
The artifact holds one file per broken golden, with the requests the model answered. Download it and read
asked before you change anything.
The nightly#
A scheduled job is where the expensive checks go. This one plays every approved case, runs the goldens under two models, the extraction cases, one spoken caller, and the drift of the judges on the calls the sandbox took:
name: nightly
on:
schedule:
- cron: "17 2 * * 1-5"
workflow_dispatch:
concurrency:
group: goldens
cancel-in-progress: false
jobs:
nightly:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
PINECALL_KEY: ${{ secrets.PINECALL_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- name: every approved case
run: npx pinecall test --dataset
- name: goldens, two models
if: always()
run: npx pinecall test --model haiku --model openai/gpt-4.1-mini
- name: what memory writes down
if: always()
run: npx pinecall remember
- name: a caller out loud
if: always()
run: npx pinecall simulate --persona hurried --voice --judge
- name: drift
if: always()
run: npx pinecall runs drift --agent front-desk --window 7d --baseline 30d --threshold 10
if: always() keeps a red matrix from hiding a judge that has been sliding for a week: the night fails on
any of them, and every step still runs.
To fail only when the baseline model breaks and report the second as a finding, run the two-model step with
--jsonand gate onrun.matrix.failures(Comparing models).
runs drift above reads the sandbox's calls. To watch production's, give a production server token to a
job of its own and add --prod.
CI's sandbox token has no corner of its own, so a run there reads the team's settings. A fix you saved only in your corner holds on your laptop and breaks here until
pinecall agent push … --team.