Install
$ agentstack add skill-graysurf-agent-kit-api-test-runner ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
API Test Runner (REST + GraphQL)
Contract
Prereqs:
api-test,api-rest, andapi-gqlavailable onPATH(install viabrew install nils-cli).jqrecommended for ad-hoc assertions/formatting (optional).- For focused protocol calls, use the same
setup/restandsetup/graphqlscaffold conventions documented here.
Inputs:
- Suite selection:
--suiteor--suite-file. - Optional filters/flags:
--tag,--allow-writes,--out,--junit. - Optional auth secret env (when configured in suite):
API_TEST_AUTH_JSON(or customsecretEnv).
Outputs:
- JSON results always emitted to stdout.
- Optional files:
--out(results JSON) and--junit(JUnit XML). - Per-run logs/artifacts under
out/api-test-runner//when executed.
Exit codes:
0: all selected cases passed2: one or more cases failed1: invalid inputs / schema / missing files
Failure modes:
- Missing/invalid suite or case files (JSON schema errors, missing request/op files).
- Auth missing/invalid (401/403) or write-capable case blocked by safety defaults.
Entrypoints
- External CLI commands only:
api-test runapi-test summary- This skill intentionally has no repo-local
scripts/entrypoint.
Goal
Provide one canonical API testing entrypoint for both REST and GraphQL. Prefer api-test run for repeatable suites and CI. Use api-rest or api-gql directly only for quick focused calls, then graduate repeated checks into an api-test suite.
Run a suite of API checks in CI (and locally) via a single manifest file, reusing existing protocol callers:
- REST:
api-rest - GraphQL:
api-gql
The runner:
- Executes selected cases deterministically
- Produces machine-readable JSON results (and optional JUnit XML)
- Applies safe defaults (no secret leakage; guardrails for write-capable cases)
TL;DR
Bootstrap a minimal setup/api/ (when your repo already has setup/rest and/or setup/graphql):
mkdir -p setup
cp -R "$AGENT_HOME/skills/tools/testing/api-test-runner/assets/scaffold/setup/api" setup/
Bootstrap a runnable local-fixture smoke suite (includes setup/api, setup/rest, setup/graphql, protocol presets, plus a tiny REST + GraphQL fixture):
cp -R "$AGENT_HOME/skills/tools/testing/api-test-runner/assets/scaffold/setup" .
Start the local fixture (REST + GraphQL; required by smoke-demo):
python3 setup/fixtures/httpbin/server.py --port 43127
Run it in a separate terminal (or background it with &) so the suite can connect.
Run a canonical suite:
api-test run --suite smoke-demo --out out/api-test-runner/results.json
Emit JUnit for CI reporters:
api-test run --suite smoke-demo --junit out/api-test-runner/junit.xml
Generate a human-friendly summary (CI logs + $GITHUB_STEP_SUMMARY), based on the results JSON:
api-test summary \
--in out/api-test-runner/results.json \
--out out/api-test-runner/summary.md \
--slow 5
Hide skipped cases (optional):
api-test summary \
--in out/api-test-runner/results.json \
--hide-skipped
Suite Manifests
Canonical locations searched by --suite:
tests/api/suites/*.suite.json- (fallback)
setup/api/suites/*.suite.json(used by the bundled template)
Runner entrypoints:
--suite→ resolves totests/api/suites/.suite.json(fallback:setup/api/suites/...); override the search dir with
API_TEST_SUITES_DIR
--suite-file→ explicit suite file path (use when the suite manifest is not in a canonical suites dir)
Notes:
- REST cases point at
*.request.json(same inputs used byapi-rest call). - GraphQL cases point at
*.graphql+ variables*.json(same inputs used byapi-gql call). - Suite manifest location is independent from REST/GraphQL
configDir(those can live undertests/rest,tests/graphql, etc). - GraphQL write safety: if an operation file contains a
mutationdefinition, the runner treats it as write-capable and requires
allowWrite=true on the case.
- GraphQL default validation: when
allowErrors=falseandexpect.jqis omitted, the runner requires.datato be a non-null object. - Auth safety:
.auth.*.credentialsJqmust yield exactly one object (multiple matches fail fast).
Focused REST Calls
Use api-rest for one-off REST calls when a full suite is unnecessary:
api-rest call \
--env local \
setup/rest/requests/.request.json \
| jq .
When the call becomes a repeated smoke/regression check, add it to setup/api/suites/*.suite.json and run it with api-test run.
Focused GraphQL Calls
Use api-gql for one-off GraphQL calls when a full suite is unnecessary:
api-gql call \
--env local \
--jwt default \
setup/graphql/operations/.graphql \
setup/graphql/operations/.json \
| jq .
When the operation becomes a repeated smoke/regression check, add it to setup/api/suites/*.suite.json and run it with api-test run.
Suite schema v1
{
"version": 1,
"name": "smoke",
"defaults": {
"env": "staging",
"noHistory": true,
"rest": { "configDir": "setup/rest", "url": "", "token": "" },
"graphql": { "configDir": "setup/graphql", "url": "", "jwt": "" }
},
"cases": [
{
"id": "rest.health",
"type": "rest",
"tags": ["smoke"],
"env": "",
"noHistory": true,
"allowWrite": false,
"configDir": "",
"url": "",
"token": "",
"request": "setup/rest/requests/health.request.json"
},
{
"id": "rest.auth.login_then_me",
"type": "rest-flow",
"tags": ["smoke"],
"env": "",
"noHistory": true,
"allowWrite": true,
"configDir": "",
"url": "",
"loginRequest": "setup/rest/requests/login.request.json",
"tokenJq": ".accessToken",
"request": "setup/rest/requests/me.request.json"
},
{
"id": "graphql.countries",
"type": "graphql",
"tags": ["smoke"],
"env": "",
"noHistory": true,
"allowWrite": false,
"allowErrors": false,
"configDir": "",
"url": "",
"jwt": "",
"op": "setup/graphql/operations/countries.graphql",
"vars": "setup/graphql/operations/countries.variables.json",
"expect": { "jq": "(.errors? | length // 0) == 0" }
}
]
}
Notes:
defaults.*are optional; per-case fields overridedefaults.defaults.noHistory(and casenoHistory) map to--no-historyon underlyingapi-rest call/api-gql call.defaults.rest.configDir/defaults.graphql.configDirdefault tosetup/rest/setup/graphql.- For REST and GraphQL endpoint selection, prefer
urlfor CI determinism (avoids env preset drift). rest-flowrunsloginRequestfirst, extracts a token (viatokenJq), then runsrequestwithACCESS_TOKEN=(token is not
printed in command snippets).
Case cleanup (optional)
For write cases that create persistent resources (DB rows, uploaded files, etc), attach a cleanup block so the runner removes test artifacts after the case runs.
cleanupcan be an object (single step) or an array of steps.- Each cleanup step must include
type:restorgraphql. - Cleanup steps run only when writes are allowed:
API_TEST_ALLOW_WRITES_ENABLED=true(or--allow-writes) or when the effectiveenvis
local.
REST cleanup step fields:
method(default:DELETE)pathTemplate(required; supports{{var}})vars(optional):{ "var": "" }evaluated against the main response JSONexpectStatus(orexpect.status) optional; defaults to204forDELETE, otherwise200
GraphQL cleanup step fields:
op(required)- Variables (pick one):
varsJq: jq expression producing the variables object (evaluated against the main response JSON)varsTemplate+vars(object mapping placeholders):varsTemplateis a JSON file that may contain{{var}}placeholdersvars: a variables JSON file path (static)allowErrors/expect.jq: same semantics as normal GraphQL cases (whenallowErrors=true,expect.jqis required)
GraphQL create + delete example (cleanup uses the create response):
{
"id": "graphql.admin.createThing",
"type": "graphql",
"tags": ["write"],
"allowWrite": true,
"jwt": "admin",
"op": "setup/graphql/operations/things.create.graphql",
"vars": "setup/graphql/operations/things.create.variables.json",
"expect": { "jq": ".data.createThing.thing.id != null" },
"cleanup": {
"type": "graphql",
"op": "setup/graphql/operations/things.delete.graphql",
"varsJq": "{ input: { id: .data.createThing.thing.id } }",
"expect": { "jq": ".data.deleteThing.success == true" }
}
}
REST cleanup example (delete by key extracted from the response):
{
"id": "rest.files.images.upload",
"type": "rest",
"tags": ["write"],
"allowWrite": true,
"token": "admin",
"request": "setup/rest/requests/files.images.upload.request.json",
"cleanup": {
"type": "rest",
"method": "DELETE",
"pathTemplate": "/files/images/{{key}}",
"vars": { "key": ".key" },
"expectStatus": 204
}
}
Note: api-rest also supports per-request cleanup blocks; case-level cleanup is an alternative that can cover both REST + GraphQL in one suite.
CI auth (GitHub Secrets / JWT login)
If your JWT expires, prefer logging in at runtime in CI using a suite-level auth block + a single JSON GitHub Secret.
How it works:
- You provide credentials via a JSON env var (default:
API_TEST_AUTH_JSON). - The runner logs in once per referenced profile (cached for the run) using either a REST or GraphQL login provider.
- For cases that specify
token(REST) orjwt(GraphQL), the runner injectsACCESS_TOKENfor that case and does not rely on
tokens(.local).env / jwts(.local).env.
Recommended secret schema (example):
{
"profiles": {
"admin": { "username": "admin@example.com", "password": "..." },
"member": { "username": "member@example.com", "password": "..." }
}
}
Suite example (REST provider):
{
"version": 1,
"name": "auth-smoke",
"auth": {
"provider": "rest",
"secretEnv": "API_TEST_AUTH_JSON",
"required": true,
"rest": {
"loginRequestTemplate": "setup/rest/requests/login.request.json",
"credentialsJq": ".profiles[$profile] | select(.) | { username, password }",
"tokenJq": ".accessToken"
}
},
"defaults": {
"noHistory": true,
"rest": { "url": "https://", "token": "member" },
"graphql": { "url": "https:///graphql", "jwt": "member" }
},
"cases": [
{ "id": "rest.me.member", "type": "rest", "token": "member", "request": "setup/rest/requests/me.request.json" },
{ "id": "graphql.me.admin", "type": "graphql", "jwt": "admin", "op": "setup/graphql/operations/me.graphql" }
]
}
Suite example (GraphQL provider):
{
"version": 1,
"name": "auth-smoke",
"auth": {
"provider": "graphql",
"secretEnv": "API_TEST_AUTH_JSON",
"required": true,
"graphql": {
"loginOp": "setup/graphql/operations/login.ci.graphql",
"loginVarsTemplate": "setup/graphql/operations/login.ci.variables.json",
"credentialsJq": ".profiles[$profile] | select(.) | { email, password }",
"tokenJq": ".. | objects | (.accessToken? // .access_token? // .token? // .jwt? // empty) | select(type==\"string\" and length>0) | ."
}
}
}
Defaults:
- Fail fast: if
authis configured but the secret env var is missing/empty, the runner exits1with a clear error. - Optional override: set
auth.required=falseto disable suite auth when the secret is missing (useful for forks / local runs).
Assertions
- REST: assertions live in the request file:
expect.status(required whenexpectis present)expect.jq(optional; evaluated withjq -eagainst the JSON response)- GraphQL:
- Default: the runner enforces
.errorsis empty, plus optional per-caseexpect.jq. allowErrors: true: skip the default no-errors check and rely onexpect.jq(required) to assert the expected error(s), e.g.
.errors[0].extensions.code == "PHONE_VERIFICATION_INCOMPLETE".
CLI flags
Core:
--suite/--suite-file--out(optional; JSON results file)--junit(optional; JUnit XML)
Selection:
--only--skip--tag(repeatable; all tags must match)
Control:
--fail-fast(stop after first failure)--continue(continue after failures; default)
CI examples
Generic shell (write JSON + JUnit as CI artifacts):
api-test run \
--suite smoke \
--out out/api-test-runner/results.json \
--junit out/api-test-runner/junit.xml
GitHub Actions (runs the bundled smoke suite; local REST + GraphQL fixture):
- Example workflow file:
.github/workflows/api-test-runner.yml
The bundled smoke suite expects a local fixture on http://127.0.0.1:43127:
python3 setup/fixtures/httpbin/server.py --port 43127
If you want to run your own suite in CI, replace the bootstrap step with your repo’s committed setup/api/.
Notes:
- Keep
out/api-test-runner/results.jsonas the primary machine-readable artifact. - Only upload per-case response files as artifacts if they are known to be non-sensitive.
GitHub Actions: matrix sharding by tags
For large suites, you can split a single suite into multiple CI jobs by tagging cases and running the runner in a matrix.
Suite tagging pattern (make shard tags mutually exclusive):
{
"cases": [
{ "id": "graphql.health", "type": "graphql", "tags": ["staging", "shard:0"], "op": "..." },
{ "id": "graphql.notifications", "type": "graphql", "tags": ["staging", "shard:1"], "op": "..." }
]
}
Workflow example:
strategy:
fail-fast: false
matrix:
shard: ["0", "1"]
steps:
- name: Run suite shard
env:
AGENT_HOME: ${{ github.workspace }}
API_TEST_AUTH_JSON: ${{ secrets.API_TEST_AUTH_JSON }}
run: |
api-test run \
--suite my-suite \
--tag staging \
--tag "shard:${{ matrix.shard }}" \
--out "out/api-test-runner/results.shard-${{ matrix.shard }}.json" \
--junit "out/api-test-runner/junit.shard-${{ matrix.shard }}.xml"
Notes:
--tagis repeatable and uses AND semantics (a case must include all tag filters to run).- Make shard tags mutually exclusive to avoid duplicate coverage across jobs.
- Use per-shard output filenames to avoid artifact collisions.
Safety defaults
Write-capable cases are denied by default.
- REST write detection: HTTP methods other than
GET/HEAD/OPTIONS. - GraphQL write detection: operation type
mutation(best-effort).
Allow writes only when:
- The case has
allowWrite: true, AND - Either:
- effective
envislocal, OR - the runner is invoked with
--allow-writes(orAPI_TEST_ALLOW_WRITES_ENABLED=true).
Results
- JSON results are always emitted to stdout (single JSON object). A one-line summary is emitted to stderr.
- Use
--outto also write the JSON results to a file. - Use
--junitto write a JUnit XML report.
Result JSON schema (v1)
Top-level fields:
version: integersuite: suite namesuiteFile: path to suite file (relative to repo root)runId: timestamp idstartedAt/finishedAt: UTC timestampsoutputDir: output directory (relative to repo root)summary:{ total, passed, failed, skipped }cases[]: per-case status objects
Per-case fields:
id,type,status(passed|failed|skipped),durationMstags(array)command(replayable snippet; no secrets)message(reason for fail/skip; stable-ish tokens)assertions(GraphQL only; includesdefaultNoErrorsand optionaljq)stdoutFile/stderrFile(paths underout/api-test-runner//when executed)
Exit codes:
0: all selected cases passed2: one or more cases failed1: invalid inputs / schema / missing
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: graysurf
- Source: graysurf/agent-kit
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.