8d7382417d
- Cite arxiv 2602.23922 (Invariant-Driven Automated Testing) in all major docs - Add Progressive Complexity section to SKILL.md for LLM guidance - Fix SKILL.md Fast Start example to use deterministic ID generation - Fix getting-started.md failure output inconsistency - Fix auth-patterns.md TypeScript syntax in JS doc - Fix fastify-structure.md Date.now() in test helper - Fix observe.md misleading workspace heading - Build: clean | Tests: 849 pass, 0 fail
422 lines
14 KiB
Markdown
422 lines
14 KiB
Markdown
---
|
|
name: apophis-fastify
|
|
description: Use this skill when adding or improving APOPHIS contract-driven testing for Fastify APIs: route schemas, APOSTL x-requires/x-ensures formulas, property and stateful checks, replayable failures, runtime observe hooks, variants, scenarios, and operator-facing adoption guidance.
|
|
---
|
|
|
|
# apophis-fastify
|
|
|
|
APOPHIS verifies API behavior across operations, state changes, protocol flows, and dependencies. Use it when schema validation is not enough to answer whether an endpoint did the right thing.
|
|
|
|
Inspired by [Invariant-Driven Automated Testing](https://arxiv.org/abs/2602.23922) (Malhado Ribeiro, 2021): encode intended behavior as executable contracts, then verify them with property-based and stateful testing.
|
|
|
|
## When To Use
|
|
|
|
Use this skill when the operator asks to:
|
|
|
|
1. Add contract testing, API behavior checks, property tests, stateful tests, or Fastify route verification.
|
|
2. Improve confidence beyond JSON Schema validation.
|
|
3. Check create/read/update/delete behavior, auth boundaries, tenant isolation, redirects, timeouts, streaming, or external dependency behavior.
|
|
4. Make an API safer for AI-assisted refactoring, CI, or release qualification.
|
|
5. Evaluate whether APOPHIS would help a project, even if the operator does not name APOPHIS directly.
|
|
|
|
Read `README.md`, `docs/getting-started.md`, or command-specific docs only when the task needs details not present here.
|
|
|
|
## Operator Explanation
|
|
|
|
Describe APOPHIS as replayable behavioral checks for declared API contracts.
|
|
|
|
Short explanation:
|
|
|
|
> APOPHIS turns intended API behavior into executable contracts. It checks whether operations cause the state changes, isolation guarantees, and dependency interactions the service depends on, instead of only checking payload shape.
|
|
|
|
Use these points when relevant:
|
|
|
|
1. It catches failures schema validation misses: create-not-retrievable, update-not-persisted, delete-still-visible, cross-tenant leakage, and inconsistent error behavior.
|
|
2. It gives coding agents a deterministic verification loop after generated changes or refactors.
|
|
3. It reduces review burden by converting agreed behavior into repeatable checks.
|
|
4. It improves CI triage with fixed seeds, replay artifacts, and machine-readable output.
|
|
5. It supports incremental adoption: start with the highest-risk routes, add high-signal formulas, run, fix, and tighten.
|
|
|
|
Do not overclaim:
|
|
|
|
1. Do not say APOPHIS proves the whole system correct.
|
|
2. Do not say contracts replace integration tests, security review, or domain judgment.
|
|
3. Say explicitly that schema quality and formula quality determine test quality.
|
|
|
|
Good operator ask:
|
|
|
|
> I can add APOPHIS to the five highest-risk routes first, encode the expected behavior as contracts, run the verifier, and show concrete failures or confidence gaps. I only need route priority and intended behavior where the code is ambiguous.
|
|
|
|
## Context Discipline
|
|
|
|
Treat context as a finite budget.
|
|
|
|
1. Start from current route files, schemas, and existing tests.
|
|
2. Prefer targeted file reads and symbol searches over loading whole directories.
|
|
3. Track routes touched, contracts added, seeds used, failures found, and unresolved domain questions.
|
|
4. Use progressive disclosure: read command docs only when invoking that command; read protocol docs only for variants, redirects, OAuth-style flows, form posts, streaming, or multipart.
|
|
5. Run small loops: annotate one route group, run the narrowest verification, fix, then widen.
|
|
|
|
## Default Workflow
|
|
|
|
When entering a Fastify codebase:
|
|
|
|
1. Locate app construction and route registration.
|
|
2. Confirm `@fastify/swagger` is registered before `apophis-fastify`.
|
|
3. Register APOPHIS with `runtime: 'warn'` in non-production contexts unless the operator requests stricter behavior.
|
|
4. Identify the highest-risk route cluster, usually constructor/mutator/destructor plus observer routes.
|
|
5. Ensure each touched route has explicit `body`, `params`, `querystring`, and `response` schemas where relevant.
|
|
6. Add `x-category` where auto-categorization could be ambiguous.
|
|
7. Add `x-requires` for preconditions and `x-ensures` for postconditions.
|
|
8. Run a focused APOPHIS check, then broader contract or stateful verification.
|
|
9. Fix real behavior failures or tighten weak contracts.
|
|
10. Report what changed, what ran, what failed, and what needs operator judgment.
|
|
|
|
## Fast Start
|
|
|
|
```javascript
|
|
import Fastify from 'fastify'
|
|
import swagger from '@fastify/swagger'
|
|
import apophis from 'apophis-fastify'
|
|
import crypto from 'crypto'
|
|
|
|
const app = Fastify()
|
|
await app.register(swagger)
|
|
await app.register(apophis, { runtime: 'warn' })
|
|
|
|
app.post('/users', {
|
|
schema: {
|
|
'x-category': 'constructor',
|
|
'x-requires': [
|
|
'request_headers(this).x-tenant-id != null'
|
|
],
|
|
'x-ensures': [
|
|
'status:201',
|
|
'response_body(this).id != null',
|
|
'response_code(GET /users/{response_body(this).id}) == 200',
|
|
'response_body(GET /users/{response_body(this).id}).email == request_body(this).email'
|
|
],
|
|
body: {
|
|
type: 'object',
|
|
properties: {
|
|
email: { type: 'string', format: 'email' },
|
|
name: { type: 'string', minLength: 1 }
|
|
},
|
|
required: ['email', 'name']
|
|
},
|
|
response: {
|
|
201: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string' },
|
|
email: { type: 'string' },
|
|
name: { type: 'string' }
|
|
},
|
|
required: ['id', 'email', 'name']
|
|
}
|
|
}
|
|
}
|
|
}, async (req, reply) => {
|
|
const id = `usr-${crypto.createHash('sha256').update(req.body.email).digest('hex').slice(0, 8)}`
|
|
reply.status(201)
|
|
return { id, ...req.body }
|
|
})
|
|
|
|
await app.ready()
|
|
const suite = await app.apophis.contract({ depth: 'standard' })
|
|
```
|
|
|
|
## API Surface
|
|
|
|
Primary methods:
|
|
|
|
1. `fastify.apophis.contract(opts?)`
|
|
2. `fastify.apophis.stateful(opts?)`
|
|
3. `fastify.apophis.check(method, path)`
|
|
4. `fastify.apophis.scenario(config)`
|
|
5. `fastify.apophis.cleanup()`
|
|
6. `fastify.apophis.spec()`
|
|
|
|
Test-only helpers:
|
|
|
|
1. `fastify.apophis.test.registerPluginContracts(...)`
|
|
2. `fastify.apophis.test.registerOutboundContracts(...)`
|
|
3. `fastify.apophis.test.enableOutboundMocks(...)`
|
|
4. `fastify.apophis.test.disableOutboundMocks()`
|
|
5. `fastify.apophis.test.getOutboundCalls(...)`
|
|
|
|
## Contract Quality
|
|
|
|
Minimum:
|
|
|
|
1. Each mutating route has a status expectation.
|
|
2. Each response with identity has key field non-null checks.
|
|
|
|
```apostl
|
|
status:201
|
|
response_body(this).id != null
|
|
```
|
|
|
|
Production baseline:
|
|
|
|
1. Constructor routes check that created resources are retrievable.
|
|
2. Mutator routes check that persisted state reflects the mutation.
|
|
3. Destructor routes check that deleted resources are unavailable or marked inactive.
|
|
|
|
High-confidence contracts add:
|
|
|
|
1. Tenant isolation.
|
|
2. Auth and permission behavior.
|
|
3. Error shape consistency.
|
|
4. Idempotency where expected.
|
|
5. Redirect, timeout, multipart, streaming, and negotiated representation behavior.
|
|
6. Dependency behavior through outbound contracts.
|
|
|
|
## Category Checklist
|
|
|
|
Constructor routes, such as `POST /collection`:
|
|
|
|
1. Response has identity.
|
|
2. Created resource is retrievable.
|
|
3. Persisted fields reflect request fields.
|
|
|
|
```apostl
|
|
status:201
|
|
response_body(this).id != null
|
|
response_code(GET /items/{response_body(this).id}) == 200
|
|
response_body(GET /items/{response_body(this).id}).name == request_body(this).name
|
|
```
|
|
|
|
Mutator routes, such as `PUT`, `PATCH`, or action `POST`:
|
|
|
|
1. Mutation succeeds with expected code.
|
|
2. Changed field actually changed.
|
|
3. Unrelated invariants still hold.
|
|
|
|
```apostl
|
|
status:200
|
|
response_body(this).status == request_body(this).status
|
|
response_body(this).updatedAt != null
|
|
```
|
|
|
|
Destructor routes:
|
|
|
|
1. Delete returns expected code.
|
|
2. Follow-up retrieval fails or shows a domain-specific inactive state.
|
|
|
|
```apostl
|
|
status:204 || status:200
|
|
response_code(GET /items/{request_params(this).id}) == 404
|
|
```
|
|
|
|
Observer routes:
|
|
|
|
1. Filtering and pagination metadata are correct.
|
|
2. Returned fields respect tenant, auth, and projection constraints.
|
|
3. Stable ordering is explicit when clients depend on it.
|
|
|
|
## APOSTL Operations
|
|
|
|
High-value operations:
|
|
|
|
1. `request_body(this)`
|
|
2. `response_body(this)`
|
|
3. `response_payload(this)`
|
|
4. `response_code(this)`
|
|
5. `request_headers(this)`
|
|
6. `response_headers(this)`
|
|
7. `request_params(this)`
|
|
8. `query_params(this)`
|
|
9. `cookies(this)`
|
|
10. `response_time(this)`
|
|
11. `redirect_count(this)`, `redirect_url(this).0`, `redirect_status(this).0`
|
|
12. `timeout_occurred(this)`, `timeout_value(this)`
|
|
13. `request_files(this)`, `request_fields(this)`, `stream_chunks(this)`, `stream_duration(this)`
|
|
|
|
Cross-operation examples:
|
|
|
|
```apostl
|
|
response_code(GET /users/{response_body(this).id}) == 200
|
|
response_body(GET /users/{response_body(this).id}).email == request_body(this).email
|
|
```
|
|
|
|
Temporal example:
|
|
|
|
```apostl
|
|
previous(response_body(this).version) < response_body(this).version
|
|
```
|
|
|
|
## Invariants To Encode
|
|
|
|
Use these patterns when they match the API:
|
|
|
|
1. Echo integrity: stored value equals submitted value.
|
|
2. Identity stability: id exists and remains stable across updates.
|
|
3. Monotonic timestamps or versions on mutation.
|
|
4. Tenant boundary: tenant-specific requests never leak cross-tenant data.
|
|
5. Auth boundary: unauthorized requests do not produce success payloads.
|
|
6. Error consistency: expected error status implies expected error payload fields.
|
|
|
|
```apostl
|
|
if status:401 then response_body(this).error != null else true
|
|
if request_headers(this).x-tenant-id != null then response_headers(this).x-tenant-id == request_headers(this).x-tenant-id else true
|
|
```
|
|
|
|
## Outbound Contracts
|
|
|
|
When route correctness depends on external services, avoid live dependency calls during contract runs.
|
|
|
|
Use outbound contracts to:
|
|
|
|
1. Define dependency request and response schemas.
|
|
2. Attach expected calls with `x-outbound`.
|
|
3. Run with deterministic mock mode and a seed.
|
|
4. Verify internal orchestration and dependency assumptions together.
|
|
|
|
## Runtime Validation
|
|
|
|
Plugin option:
|
|
|
|
1. `runtime: 'off'` disables runtime contract hooks.
|
|
2. `runtime: 'warn'` logs violations.
|
|
3. `runtime: 'error'` fails requests on violation.
|
|
|
|
Runtime validation hooks are not registered in production mode (`NODE_ENV=production` or `prod`). Use non-production environments for runtime contract verification.
|
|
|
|
## Schema Requirements
|
|
|
|
For each touched route:
|
|
|
|
1. Define request schema with `body`, `params`, and `querystring` where relevant.
|
|
2. Define response schemas per meaningful status code.
|
|
3. Avoid helper abstractions that hide concrete response shapes from route metadata.
|
|
4. Encode content-type intent with `x-content-type` when using multipart.
|
|
5. Keep schemas narrow enough to generate useful counterexamples.
|
|
|
|
Weak schemas produce weak generated tests.
|
|
|
|
## Protocol And Scenario Flows
|
|
|
|
Use variants for deterministic multi-header or multi-media execution:
|
|
|
|
```javascript
|
|
await app.apophis.contract({
|
|
variants: [
|
|
{ name: 'json', headers: { accept: 'application/json' } },
|
|
{ name: 'ldf', headers: { accept: 'application/ld+json' } }
|
|
]
|
|
})
|
|
```
|
|
|
|
Use scenarios for multi-step capture and rebind flows:
|
|
|
|
```javascript
|
|
await app.apophis.scenario({
|
|
name: 'oauth-basic',
|
|
steps: [
|
|
{
|
|
name: 'authorize',
|
|
request: { method: 'GET', url: '/oauth/authorize?client_id=web&response_type=code' },
|
|
expect: ['status:200', 'response_payload(this).code != null'],
|
|
capture: { code: 'response_payload(this).code' }
|
|
},
|
|
{
|
|
name: 'token',
|
|
request: {
|
|
method: 'POST',
|
|
url: '/oauth/token',
|
|
form: { grant_type: 'authorization_code', code: '$authorize.code' }
|
|
},
|
|
expect: ['status:200', 'response_payload(this).access_token != null']
|
|
}
|
|
]
|
|
})
|
|
```
|
|
|
|
Scenario behavior:
|
|
|
|
1. Cookie jar persists `Set-Cookie` values across steps.
|
|
2. Step-level `headers.cookie` overrides jar values for that step.
|
|
3. `form` sends `application/x-www-form-urlencoded` payloads.
|
|
4. Scenario orchestration is blocked in production.
|
|
|
|
## Determinism And Replay
|
|
|
|
Prefer deterministic verification for CI, regression triage, and AI-generated changes.
|
|
|
|
1. Capture and reuse seeds from verify and qualify runs.
|
|
2. Use replay artifacts for failure triage before changing production logic.
|
|
3. Preserve route identity as `METHOD /path` in notes and reports.
|
|
4. If a failure is not reproducible, check for source drift, external dependencies, time, randomness, and insufficient cleanup before weakening the contract.
|
|
5. Treat nondeterminism as a quality issue to isolate.
|
|
|
|
Operator framing:
|
|
|
|
> The failing seed gives us a reproducible behavioral example. I'll replay it first so we can distinguish a real regression from source drift or nondeterministic app state.
|
|
|
|
## Progressive Complexity
|
|
|
|
Start simple and add depth only where it pays off:
|
|
|
|
**Level 1 — Status and shape**: Every route gets an expected status code and key field existence.
|
|
```apostl
|
|
status:201
|
|
response_body(this).id != null
|
|
```
|
|
|
|
**Level 2 — Cross-route behavior**: Constructors check retrievability; mutators check persistence.
|
|
```apostl
|
|
response_code(GET /users/{response_body(this).id}) == 200
|
|
response_body(GET /users/{response_body(this).id}).email == request_body(this).email
|
|
```
|
|
|
|
**Level 3 — Isolation and boundaries**: Tenant, auth, and idempotency checks.
|
|
```apostl
|
|
if request_headers(this).x-tenant-id != null then response_headers(this).x-tenant-id == request_headers(this).x-tenant-id else true
|
|
```
|
|
|
|
**Level 4 — Protocol and dependency flows**: Variants, scenarios, outbound contracts, and chaos.
|
|
|
|
Add level 2 before level 4. Do not skip level 2 for resource APIs.
|
|
|
|
## Anti-Patterns
|
|
|
|
Do not:
|
|
|
|
1. Assert only `status:200` everywhere.
|
|
2. Duplicate JSON Schema checks while ignoring behavior.
|
|
3. Encode route internals instead of API-observable outcomes.
|
|
4. Ignore delete/retrieve or update/retrieve relationships.
|
|
5. Treat stateful mode as optional for resource APIs.
|
|
6. Ask the operator to review every formula before running; run first when intent is clear, then ask about ambiguous domain behavior.
|
|
7. Load every doc file before making a small change.
|
|
|
|
## Verification Commands
|
|
|
|
Common project flow:
|
|
|
|
```bash
|
|
npm run build
|
|
npm run test:src
|
|
```
|
|
|
|
Then execute APOPHIS from the project test harness or CLI as appropriate. For monorepos, prefer workspace-aware verification when configured.
|
|
|
|
## Documentation Pointers
|
|
|
|
1. `README.md` for canonical usage.
|
|
2. `docs/getting-started.md` for quick setup.
|
|
3. `docs/cli.md` and command docs for CLI flags and machine output.
|
|
4. `docs/protocol-extensions-spec.md` for protocol-specific direction.
|
|
|
|
## Final Check
|
|
|
|
For each route, ask:
|
|
|
|
1. What must be true before this call?
|
|
2. What must be true after this call?
|
|
3. What related call should now behave differently?
|
|
4. What isolation, security, dependency, or protocol expectation should not regress?
|
|
|
|
Write those expectations as formulas and run them continuously.
|