18 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[2.5.0] - 2026-04-29
Added
CLI Lazy Plugin Loading
The CLI now works with Fastify apps that don't pre-register the APOPHIS plugin.
Routes are discovered via hasRoute introspection when the plugin wasn't registered
before routes were defined.
- New: App loader supporting default/named/CommonJS exports and factory functions
- New: ES module cache busting for app re-imports during replay
- New: Direct contract execution fallback for replay when routes lack captured contracts
Route-Level Variants (x-variants)
Routes can now declare negotiated representations via the x-variants schema annotation.
Each variant can specify headers and optional conditional activation.
const schema = {
'x-variants': [
{ name: 'json', headers: { 'accept': 'application/json' } },
{ name: 'ldf', headers: { 'accept': 'application/ld+json' } }
],
'x-ensures': ['response_body(this).id != null']
}
- New:
RouteContract.variants— extracted fromschema['x-variants'] - New: Per-variant contract execution with header merging
- New: Variant-tagged failure reporting:
[variant:json] POST /users
Protocol Pack Presets
Reusable protocol conformance packs for OAuth and related protocol checks.
- New:
oauth21ProfilePack()— OAuth 2.1 with PKCE - New:
rfc8628DeviceAuthorizationPack()— Device Authorization Grant - New:
rfc8693TokenExchangePack()— Token Exchange - New:
composePacks()— merge multiple packs - New:
applyPack()— apply pack to existing config
Fixed
- Config validation errors now return exit code 2 (usage error) instead of 3 (internal error)
- Replay correctly handles apps without pre-registered APOPHIS plugin
- Empty body with content-type header no longer causes Fastify 400 errors
[2.4.0] - 2026-04-27
Added
Contract-Driven Outbound Mocking
Routes can now declare the contracts and expectations of their outbound dependencies. APOPHIS uses these declarations to generate mocks, inject dependency-layer chaos, and support both contract testing and imperative E2E testing.
- New:
ApophisOptions.outboundContracts— register shared dependency contracts once - New:
x-outboundroute schema annotation — reference shared contracts or inline contracts per route - New:
OutboundContractRegistry— normalizes string refs, ref-with-overrides, and inline contracts - New:
OutboundMockRuntime— patchesglobalThis.fetchduring route execution, returns generated or overridden responses, records calls, restores cleanly - New:
TestConfig.outboundMocks— control mode (example/property), overrides, and unmatched behavior - New: Imperative E2E helpers:
enableOutboundMocks(),disableOutboundMocks(),getOutboundCalls() - New: Built-in outbound extension exposing
outbound_calls(this)andoutbound_last(this)to APOSTL formulas - New:
registerOutboundContracts()decoration for runtime registration
await fastify.register(apophis, {
outboundContracts: {
'stripe.paymentIntents.create': {
target: 'https://api.stripe.com/v1/payment_intents',
method: 'POST',
response: {
200: { type: 'object', properties: { id: { type: 'string' } } },
402: { type: 'object', properties: { error: { type: 'object' } } }
}
}
}
})
// Routes reference contracts via x-outbound
const schema = {
'x-outbound': ['stripe.paymentIntents.create'],
'x-ensures': [
'if response_code == 200 then outbound_last(this).stripe.paymentIntents.create.response.statusCode == 200 else true'
]
}
// Imperative E2E
await fastify.apophis.enableOutboundMocks({
overrides: {
'stripe.paymentIntents.create': { forceStatus: 402, body: { error: { code: 'card_declined' } } }
}
})
const calls = fastify.apophis.getOutboundCalls('stripe.paymentIntents.create')
await fastify.apophis.disableOutboundMocks()
See Outbound Contract Mocking Spec for full documentation.
Changed
- Migrated:
runStatefulTestsnow usesEnhancedChaosEnginefromchaos-v2.ts(was using deprecatedChaosEnginefromchaos.ts). Stateful and contract runners now share a single chaos stack. - Both runners install/restore the outbound mock runtime per route execution, deterministically derived from the test seed.
[2.3.0] - 2026-04-27
Changed
Chaos System Final Cutover
Cleaned up the chaos architecture by removing unused types/config paths, unifying public APIs, and wiring the active outbound chaos path.
- Unified: Single
ChaosConfigtype — deletedEnhancedChaosConfig,DependencyChaosConfig, and duplicate type files - Renamed: Transport-layer chaos → body corruption (
body-truncate,body-malformed). Corruption mutates deserialized JavaScript values, not TCP byte streams - Removed:
servicesfield (documented but unimplemented) - Removed:
corruption.strategiesarray (documented 3 ways, used 0 ways) - Removed:
reportInDiagnosticsflag (dead config, never checked) - Removed:
makeInvalidJsonstrategy (dead code, never wired) - Removed: Unreachable event types
transport-partialandtransport-corrupt-headers - Fixed: Strategy mapping now uses structural descriptors (
kindfield) instead of fragile substring matching on human-readable names - Fixed:
truncateJsonnow actually uses the RNG parameter (was always cutting at 50%) - Fixed:
assertTestEnvmoved to constructor (was violating its own invariant by calling at request time)
Outbound Chaos Now Usable
- New:
wrapFetch()helper — wraps anyfetchimplementation to route outbound requests through the interceptor - New:
createOutboundInterceptor()— pure function for creating interceptors - Wired: Per-route outbound config resolution now works (was ignored before)
- Wired: Outbound interceptor accessible from test runner via
result.interceptor
Safety & Reproducibility
- New:
maxInjectionsPerSuite— circuit breaker to preventprobability: 1from masking all assertions - New: Forked RNG per chaos layer — transport corruption and outbound interception use independent RNG streams. Adding outbound config no longer shifts transport corruption sequence
Added
Dependency-Aware Chaos Testing (v2)
- New:
ChaosConfig.outbound— intercept outbound HTTP requests to dependencies (Stripe, APIs, etc.) - New: Chaos event reporting in test diagnostics
- New: Configurable dropout status codes — default 504 Gateway Timeout
- New:
ChaosConfig.skipResilienceFor— skip resilience retries for non-idempotent routes
// Simulate Stripe failures
await fastify.apophis.contract({
depth: 'quick',
chaos: {
probability: 0.1,
outbound: [
{
target: 'api.stripe.com',
error: {
probability: 0.05,
responses: [
{ statusCode: 429, headers: { 'retry-after': '60' } },
{ statusCode: 503, body: { error: 'stripe_unavailable' } }
]
}
}
],
// Skip retries for routes that create side effects
skipResilienceFor: ['constructor', 'mutator']
}
})
See Dependency-Aware Chaos Guide for full documentation.
Route Targeting for Chaos Testing
- New:
TestConfig.routes— test only specific routes instead of all discovered routes - New:
ChaosConfig.include/ChaosConfig.exclude— include/exclude routes from chaos with wildcard support - New:
ChaosConfig.routes— per-route chaos overrides - New:
ChaosConfig.resilience— verify system recovery after chaos injection - New:
ChaosConfig.maxInjectionsPerSuite— circuit breaker for total injections
// Test only specific routes
await fastify.apophis.contract({
depth: 'quick',
routes: ['GET /health', 'POST /billing/plans'],
chaos: {
probability: 0.3,
include: ['/billing/*'],
exclude: ['/billing/sensitive'],
resilience: { enabled: true, maxRetries: 3 },
maxInjectionsPerSuite: 50
}
})
Mutation Testing
- New:
src/quality/mutation.ts— synthetic bug injection to measure contract strength - New:
runMutationTesting()— generates mutations (flip operators, change numbers, remove clauses) and verifies tests catch them - New: Mutation score reporting (0-100%) with weak contract identification
import { runMutationTesting } from 'apophis-fastify/quality/mutation'
const report = await runMutationTesting(fastify)
console.log(`Mutation score: ${report.score}%`) // 85%
console.log('Weak contracts:', report.weakContracts)
Performance Improvements
- P2: Full SHA-256 hashes (64 chars) instead of truncated 16-char hashes
- P3: Configurable parse cache with
setParseCacheLimit(),getParseCacheLimit(),clearParseCache() - P5: Chunked NDJSON processing with
x-stream-max-chunk-sizelimit (default 1MB) - P8: Lazy topological sorting for extension registry (sorts only when needed)
Observability
- O2: Per-route chaos granularity with include/exclude patterns
- O3: Resilience verification — retry after chaos to confirm recovery
- O4: Pre-filter routes with contracts — skip hook evaluation for routes without annotations
- O5: Forked RNG per chaos layer — transport and outbound use independent streams
Fixed
- Critical: Disabled array-of-objects schema inference that generated invalid APOSTL (
data[].idsyntax). Arrays of objects now require explicitx-ensuresformulas. - Schema inference no longer crashes on collection schemas (LDF Collection fragments)
- P0: Chaos events now visible in test diagnostics with type, status code, and dependency URL
- C1: ScopeRegistry default scope bug — now respects configured
defaultscope - C2: Plugin contract builder —
routesoption now propagated to test runner - P2: Dropout returns 504 Gateway Timeout instead of status code 0
- P3: Resilience verification skips non-idempotent routes by default
[2.1.0] - 2026-04-26
Breaking Changes
Justin Support Removed
- Removed: Justin (subscript) expression evaluator and all Justin compatibility code
- Removed:
src/formula/justin.ts(wrapper with compile cache) - Removed:
src/formula/context-builder.ts(Justin context mapping) - Removed:
subscriptdependency from package.json - Changed: All contracts now use APOSTL exclusively
- Changed: Documentation updated to reflect APOSTL-only syntax
Migration
All x-ensures and x-requires formulas must use APOSTL syntax:
// v2.1 — APOSTL (required)
'x-ensures': ['status:201', 'response_body(this).id != null']
// v2.0 — Justin (removed)
'x-ensures': ['statusCode == 201', 'response.body.id != null']
See Getting Started Guide for full APOSTL reference.
[2.0.0] - 2026-04-25
Breaking Changes
APOSTL Replaced with Justin (Plain JavaScript Expressions)
- Removed: Custom APOSTL parser (
src/formula/parser.ts,src/formula/tokenizer.ts,src/formula/evaluator.ts,src/formula/substitutor.ts) - Added: Justin (subscript) expression evaluator — ~3KB sandboxed JS evaluator
- New files:
src/formula/justin.ts(wrapper with compile cache),src/formula/context-builder.ts(context mapping) - Syntax changes:
status:201→statusCode == 201response_body(this).id→response.body.idrequest_headers(this).auth→request.headers.authif a then b else T→a ? b : true(or!a || b)for x in arr: p→arr.every(x => p)x matches /r/→/r/.test(x)previous(expr)→previous.*(e.g.,previous.response.body.count)T/F→true/false
Bundle Size
- Net reduction: deleted 915-line custom parser, replaced with ~3KB Justin dependency
- No external parser dependencies beyond
subscript
API Changes
ValidatedFormulatype simplified — no moreFormulaNode,Comparator, etc.- Extension predicates now register as context variables/methods, not operation headers
- All
x-ensuresandx-requiresarrays use Justin syntax
Migration
See Migration Guide for complete conversion table.
[1.2.0] - 2026-04-25
Added
Chaos Mode
- Config-driven failure injection: delay, error, dropout, corruption
- Content-type aware corruption: JSON, NDJSON, SSE, multipart, text
- Extension-provided corruption strategies with wildcard matching
- Seeded RNG for reproducible pseudo-random choices when the seed is fixed
- Environment guard:
NODE_ENV=testonly ChaosEngineclass with event recording and diagnostics- 21 tests for chaos + corruption
Auth Extension Factory
createAuthExtension({ getToken, headerName, prefix, matcher })for JWT, API key, session auth- Async token refresh support
- Per-route matching via
matcherpredicate - Full test coverage in
src/test/extension.test.ts - Documentation:
docs/auth-patterns.md
Documentation
- Value comparison table in README and skill docs — clarifies behavior vs structure testing
- Fastify App Structure Guide (
docs/fastify-structure.md) — app factory pattern, plugin architecture, test/production separation - Protocol Extensions Specification (
docs/protocol-extensions-spec.md) — JWT, Time Control, Stateful, X.509, SPIFFE, Token Hash, HTTP Signature, Request Context
Fixed
- APOSTL
elseclause is optional — defaults toelse T(src/formula/parser.ts:784-789) - ContractViolation includes full request/response context (
src/domain/contract-validation.ts:134-145)
[1.2.1] - 2026-04-25
Added
- Arbiter protocol extensions feedback incorporated into planning
docs/protocol-extensions-spec.md— specification for JWT, Time Control, Stateful Predicates, X.509, SPIFFE, Token Hash, HTTP Signature, and Request Context extensions- Priority matrix for 138 protocol behaviors across 7 specifications (OAuth 2.1, WIMSE S2S, Transaction Tokens, SPIFFE/SPIRE, Token Exchange, Device Auth, CIBA)
Changed
- Updated
docs/attic/root-history/NEXT_STEPS_425.mdwith P0/P1/P2/P3 categorization for protocol extensions - Updated
docs/attic/QUALITY_FEATURES_PLAN.md— Chaos marked complete, Flake/Mutation scheduled for v1.3 - Updated
docs/PLUGIN_CONTRACTS_SPEC.md— noted complementarity with protocol extensions
[1.1.0] - 2026-04-24
Added
Multipart Uploads
multipart/form-datarequest generation from JSON Schema annotations- Fake file generation with size, MIME type, and count constraints
request.filesandrequest.fieldsJustin context variables- File arrays when
maxCount > 1 - Schema annotations:
x-content-type,x-multipart-fields,x-multipart-files
Streaming / NDJSON
- Response chunk collection for streaming routes
- NDJSON format parsing
response.chunksandresponse.durationJustin context variables- Schema annotations:
x-streaming,x-stream-format,x-stream-max-chunks - Integration tests with Fastify NDJSON routes
Extension System
- Plugin system for custom Justin predicates, headers, and lifecycle hooks
- Extension state isolation (frozen copies per extension)
- Hook timeout and severity configuration
- Dependency ordering via
dependsOnwith topological sort - Async boot:
onSuiteStarthooks run in dependency order - Health checks: extensions validate before running hooks
- Security: redaction of sensitive data, timeout guards, prototype pollution prevention
Extensions
- SSE (
src/extensions/sse/): Parsetext/event-streamresponses into structured events. Expression:response.sse[0].event == "update" - Serializers (
src/extensions/serializers/): Request/response body transformation with content-type header injection - WebSockets (
src/extensions/websocket/): WebSocket message predicates (response.ws.message.type,response.ws.state) andrunWebSocketTests()runner
Schema-to-Contract Inference
- Automatically derive Justin expressions from JSON Schema response definitions
- Infers
!= nullforrequiredfields - Infers
>=/<=forminimum/maximumbounds - Infers
.test()forpatternregexes - Infers
==forconstvalues and smallenumsets - Merges inferred contracts with explicit
x-ensures, deduplicating overlaps
Core Improvements
- Parser accepts registered extension headers
- Extension predicates checked before core operations during evaluation
evaluateAsync()for async predicate resolversvalidateFormula()with error position and suggestions for common mistakes- New types:
MultipartFile,MultipartPayload, streaming response fields
Changed
ApophisExtensioninterface includesheaders,dependsOn,healthCheckfieldsparse()accepts optionalextensionHeadersparameterExtensionRegistryexposesgetExtensionHeaders(),runHealthChecks()methods- TypeScript strict mode compliance
- Removed
dist/from git tracking
Fixed
- TypeScript strict mode: ~50 errors fixed across 15+ files
- Evaluator exports restored (
evaluate,evaluateBooleanResult,evaluateWithExtensions,evaluateAsync) - Status node handling in both sync and async evaluators
- Accessor undefined checks in
resolveOperationandresolveOperationAsync - Multipart files type safety in request builder
- Predicate return type narrowing (synchronous only)
- Extension test type safety
[1.0.0] - 2026-04-24
Added
- Contract-driven API testing for Fastify
- Property-based testing with fast-check
- APOSTL expression language for contracts
- Timeout enforcement and redirect capture
- Seeded RNG for reproducible concurrent tests
- Extension plugin system
- 412 tests
License
ISC