fix: harden engine, enrich failure diagnostics, close adoption gaps

- P0: CLI verify now honors  test budget with seeded multi-sample
- P0: Observe sampling enforced via Math.random() gate in hook-validator
- P1: Remove misleading undici-mock-agent isolation option
- P1: Qualify reuses shared discoverRouteDetails() with warnings
- P1: Chaos/scenario config exposed via preset schema
- P1: README/docs limitations updated to current state
- P2: Nested response annotations prefer 2xx deterministically
- P2: --changed documented as heuristic in verify.md

- Add observe sink tests (sampling 0/1, sink failure non-interference)
- Add verify runs regression tests (scale, determinism, variants)
- Add configured-scenario qualify test (independent of OAuth fixture)
- Add coverageBreakdown to qualify artifacts (per-gate route coverage)
- Add production-style observe example with real sink in docs/observe.md
- Add nightly/staging vs PR gating guidance to docs/qualify.md

- Enrich VerifyFailure with formula-aware diagnostics:
  status:201 => 'HTTP 200', body field checks => actual values
- Remove stale observe CLI activation message
- Document outbound mocks as process-global in getting-started.md
- Refresh APOPHIS_ADOPTION_AUDIT.md with current state

903 tests pass, build clean, typecheck clean.
This commit is contained in:
John Dvorak
2026-05-21 20:39:36 -07:00
parent 55b0262799
commit d0523fcc2d
128 changed files with 4004 additions and 3631 deletions
+2 -2
View File
@@ -89,7 +89,7 @@ type PredicateResolver = (context: PredicateContext) =>
## 4. Example: Arbiter Extension
```typescript
import type { ApophisExtension, PredicateContext } from 'apophis-fastify'
import type { ApophisExtension, PredicateContext } from '@apophis/fastify'
import { createArbiter } from 'arbiter-sdk'
const arbiterExtension: ApophisExtension = {
@@ -263,7 +263,7 @@ const arbiterExtension: ApophisExtension = {
```typescript
import fastify from 'fastify'
import apophis from 'apophis-fastify'
import apophis from '@apophis/fastify'
import { arbiterExtension } from './arbiter-extension.js'
const app = fastify()
+29 -38
View File
@@ -46,23 +46,26 @@ await fastify.apophis.contract({
})
```
### wrapFetch for Outbound Interception
### Outbound Mocking
Use `fastify.apophis.test.enableOutboundMocks()` in test code to mock HTTP dependencies:
```typescript
import { wrapFetch, createOutboundInterceptor } from 'apophis-fastify'
const interceptor = createOutboundInterceptor([
{
target: 'api.stripe.com',
delay: { probability: 0.1, minMs: 1000, maxMs: 5000 },
error: {
probability: 0.05,
responses: [{ statusCode: 429, headers: { 'retry-after': '60' } }]
}
fastify.apophis.test.registerOutboundContracts({
'payment-api': {
target: 'https://api.stripe.com/v1',
method: 'POST',
response: { 200: { type: 'object', properties: { id: { type: 'string' } } } }
}
], 42)
})
fastify.apophis.test.enableOutboundMocks({ mode: 'example' })
const calls = fastify.apophis.test.getOutboundCalls('payment-api')
```
const interceptedFetch = wrapFetch(globalThis.fetch, interceptor)
Programmatic access via `createOutboundMockRuntime`:
```typescript
import { createOutboundMockRuntime } from '@apophis/fastify'
```
### Mutation Testing
@@ -70,7 +73,7 @@ const interceptedFetch = wrapFetch(globalThis.fetch, interceptor)
Measure contract strength by injecting synthetic bugs:
```typescript
import { runMutationTesting } from 'apophis-fastify/quality/mutation'
import { runMutationTesting } from '@apophis/fastify/quality/mutation'
const report = await runMutationTesting(fastify)
console.log(`Score: ${report.score}%`) // 0-100
@@ -190,7 +193,7 @@ Extensions register custom APOSTL predicates that can be used in `x-ensures` and
**Register via `extensions: [sseExtension]`**
```typescript
import { sseExtension } from 'apophis-fastify/extensions/sse'
import { sseExtension } from '@apophis/fastify/extensions/sse'
await fastify.register(apophis, {
extensions: [sseExtension]
@@ -236,7 +239,7 @@ sse_events(this).0.retry // number (ms)
**Register via `extensions: [createSerializerExtension(registry)]`**
```typescript
import { createSerializerExtension, createSerializerRegistry } from 'apophis-fastify/extensions/serializers'
import { createSerializerExtension, createSerializerRegistry } from '@apophis/fastify/extensions/serializers'
const registry = createSerializerRegistry()
registry.register('protobuf', {
@@ -273,7 +276,7 @@ fastify.post('/users', {
**Register via `extensions: [websocketExtension]`**
```typescript
import { websocketExtension } from 'apophis-fastify/extensions/websocket'
import { websocketExtension } from '@apophis/fastify/extensions/websocket'
await fastify.register(apophis, {
extensions: [websocketExtension]
@@ -320,7 +323,7 @@ ws_state(this) // string
**Register via `extensions: [jwtExtension(config)]`**
```typescript
import { jwtExtension } from 'apophis-fastify/extensions'
import { jwtExtension } from '@apophis/fastify/extensions'
await fastify.register(apophis, {
extensions: [
@@ -348,7 +351,7 @@ jwt_format(this) == "compact"
**Register via `extensions: [x509Extension(config)]`**
```typescript
import { x509Extension } from 'apophis-fastify/extensions'
import { x509Extension } from '@apophis/fastify/extensions'
await fastify.register(apophis, {
extensions: [x509Extension()]
@@ -370,7 +373,7 @@ x509_self_signed(this) == false
**Register via `extensions: [spiffeExtension(config)]`**
```typescript
import { spiffeExtension } from 'apophis-fastify/extensions'
import { spiffeExtension } from '@apophis/fastify/extensions'
await fastify.register(apophis, {
extensions: [spiffeExtension()]
@@ -391,7 +394,7 @@ spiffe_validate(this) == true
**Register via `extensions: [tokenHashExtension(config)]`**
```typescript
import { tokenHashExtension } from 'apophis-fastify/extensions'
import { tokenHashExtension } from '@apophis/fastify/extensions'
await fastify.register(apophis, {
extensions: [tokenHashExtension()]
@@ -412,7 +415,7 @@ token_hash(this, "sha256") == jwt_claims(this).ath
**Register via `extensions: [httpSignatureExtension(config)]`**
```typescript
import { httpSignatureExtension } from 'apophis-fastify/extensions'
import { httpSignatureExtension } from '@apophis/fastify/extensions'
await fastify.register(apophis, {
extensions: [httpSignatureExtension()]
@@ -433,7 +436,7 @@ signature_valid(this) == true
**Register via `extensions: [timeExtension(config)]`**
```typescript
import { timeExtension } from 'apophis-fastify/extensions'
import { timeExtension } from '@apophis/fastify/extensions'
await fastify.register(apophis, {
extensions: [timeExtension()]
@@ -453,7 +456,7 @@ jwt_claims(this).exp <= now() + 30000
**Register via `extensions: [statefulExtension()]`**
```typescript
import { statefulExtension } from 'apophis-fastify/extensions'
import { statefulExtension } from '@apophis/fastify/extensions'
await fastify.register(apophis, {
extensions: [statefulExtension()]
@@ -521,7 +524,7 @@ fastify.get('/tenants/:id', {
**Register via `extensions: [requestContextExtension(config)]`**
```typescript
import { requestContextExtension } from 'apophis-fastify/extensions'
import { requestContextExtension } from '@apophis/fastify/extensions'
await fastify.register(apophis, {
extensions: [requestContextExtension()]
@@ -555,19 +558,7 @@ await fastify.apophis.contract({
### Outbound Interception
```typescript
import { wrapFetch, createOutboundInterceptor } from 'apophis-fastify'
const interceptor = createOutboundInterceptor([{
target: 'api.stripe.com',
error: {
probability: 0.05,
responses: [{ statusCode: 429, headers: { 'retry-after': '60' } }]
}
}], 42)
const interceptedFetch = wrapFetch(globalThis.fetch, interceptor)
```
Outbound interception works through `fastify.apophis.test.enableOutboundMocks()` in test code. See the [Outbound Mocking](#outbound-mocking) section for the supported API.
### Per-Route Overrides