API test infrastructure for StoneScriptPHP projects — coverage validation and test execution.
Zero npm dependencies. Uses only Node.js built-ins.
npm install @progalaxyelabs/stonescriptphp-api-testerOr use directly via npx:
npx @progalaxyelabs/stonescriptphp-api-tester validate
npx @progalaxyelabs/stonescriptphp-api-tester run --flow=smokeStatic analysis — cross-references your testplan.json against routes.php and the generated TypeScript api-client to find coverage gaps.
api-tester validate [options]Options:
| Option | Default | Description |
|---|---|---|
--testplan=<path> |
./api/tests/testplan.json |
Path to testplan.json |
--client=<path> |
./api/client/src/index.ts |
Path to api-client index.ts |
--routes=<path> |
./api/src/config/routes.php |
Path to routes.php |
--verbose |
Show all matches, not just failures | |
--json |
Output as JSON (for CI pipelines) |
Coverage checks performed:
- API Client -> Testplan (MUST be 100%): Every endpoint in the TypeScript client must appear in testplan.json
- Routes.php -> Testplan (MUST be 100%): Every route in routes.php must appear in testplan.json
- Testplan -> API Client (informational): Shows testplan endpoints not in api-client (expected for untyped routes)
- Graph Coverage (MUST be 100%): Every endpoint ID must appear in at least one graph edge
- Flow Coverage (SHOULD be 100%): Every endpoint ID should appear in at least one flow
Exit codes: 0 = all mandatory checks pass, 1 = coverage gaps found.
Runtime test execution — sends real HTTP requests following flows defined in testplan.json.
api-tester run [options]Options:
| Option | Default | Description |
|---|---|---|
--testplan=<path> |
./api/tests/testplan.json |
Path to testplan.json |
--base-url=<url> |
http://localhost:3000 |
Base URL for API |
--flow=<name> |
Flow name to run, or all for all flows |
|
--endpoint=<id> |
Run a single endpoint by ID (for debugging) | |
--dry-run |
Print what would happen without making HTTP calls | |
--verbose |
Show request/response bodies | |
--token=<token> |
Pre-obtained auth token (skip login) | |
--email=<email> |
Email for login step | |
--password=<pass> |
Password for login step | |
--platform=<name> |
Platform name for auth login |
Examples:
# Run the smoke flow
api-tester run --flow=smoke --base-url=http://localhost:3011
# Run all flows with auth credentials
api-tester run --flow=all --email=test@example.com --password=TestPass1 --platform=myapp
# Debug a single endpoint
api-tester run --endpoint=15 --verbose --base-url=http://localhost:3011
# Dry run — see what would execute without making requests
api-tester run --flow=mvp --dry-run
# Use a pre-obtained token
api-tester run --flow=smoke --token=eyJhbG...The testplan is a JSON file that describes your API surface, how endpoints relate to each other, and how to test them.
{
"meta": {
"version": "1.0",
"total_endpoints": 128,
"api_base": "http://localhost:3011"
},
"endpoints": [
{
"id": 1,
"method": "GET",
"path": "/health",
"name": "system.health",
"group": "system",
"auth_required": false,
"in_api_client": false,
"implementation_status": "implemented-untyped",
"sample_request": null,
"expected_status": 200,
"response_shape": ["status", "service", "timestamp"]
},
{
"id": 89,
"method": "POST",
"path": "/invoices",
"name": "invoices.create",
"group": "invoices",
"auth_required": true,
"in_api_client": true,
"implementation_status": "implemented-typed",
"sample_request": {
"invoice_number": "INV-{{timestamp}}",
"invoice_date": "2026-01-15",
"distributor_id": "{{distributor_id}}",
"items": []
},
"expected_status": 200,
"response_shape": ["invoice_id"]
}
],
"graph": [
{
"from": 89,
"to": 29,
"carry": {
"invoice_id": "response.data.invoice_id"
},
"note": "Create invoice, then fetch it by ID"
}
],
"flows": {
"smoke": [1, 2, 3],
"mvp": [14, 107, 105, 89, 91]
}
}| Field | Type | Description |
|---|---|---|
id |
number | Unique endpoint identifier (referenced in graph/flows) |
method |
string | HTTP method: GET, POST, PUT, DELETE |
path |
string | URL path with :param placeholders |
name |
string | Dot-notation name (e.g., invoices.create) |
group |
string | Logical group for organization |
auth_required |
boolean | Whether the endpoint requires a JWT |
in_api_client |
boolean | Whether this endpoint is in the TypeScript api-client |
implementation_status |
string | implemented-typed, implemented-untyped, or missing |
sample_request |
object/null | Request body template with {{placeholder}} support |
expected_status |
number | Expected HTTP status code |
response_shape |
string[] | Array of field names expected in response data |
Graph edges define data dependencies between endpoints. When endpoint from succeeds, values are extracted from its response and carried to subsequent steps.
The carry object maps variable names to dot-notation paths into the response:
{
"from": 89,
"to": 29,
"carry": {
"invoice_id": "response.data.invoice_id"
}
}After step 89 (POST /invoices), the invoice_id is extracted from the response and used to resolve :id in step 29 (GET /invoices/:id).
Flows are named sequences of endpoint IDs executed in order. Each flow represents a business workflow (e.g., create distributor -> create item -> create invoice -> create bill).
sample_request fields support {{placeholder}} syntax:
| Placeholder | Resolves to |
|---|---|
{{timestamp}} |
Current Unix timestamp |
{{email}} |
Generated test email |
{{any_carried_key}} |
Value carried from a previous step |
Path parameters (:id, :slug) are resolved from carried data automatically.
The runner understands the StoneScriptPHP response format:
{"status": "ok", "message": "", "data": { ... }}When validating response_shape, the runner checks body.data (not the wrapper) if the response has status: "ok". This matches how StoneScriptPHP wraps all responses via res_ok() and res_error().
- Node.js >= 18.0.0 (uses native
fetch) - Zero npm dependencies
MIT