Files

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 from schema['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-outbound route schema annotation — reference shared contracts or inline contracts per route
  • New: OutboundContractRegistry — normalizes string refs, ref-with-overrides, and inline contracts
  • New: OutboundMockRuntime — patches globalThis.fetch during 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) and outbound_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: runStatefulTests now uses EnhancedChaosEngine from chaos-v2.ts (was using deprecated ChaosEngine from chaos.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 ChaosConfig type — deleted EnhancedChaosConfig, 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: services field (documented but unimplemented)
  • Removed: corruption.strategies array (documented 3 ways, used 0 ways)
  • Removed: reportInDiagnostics flag (dead config, never checked)
  • Removed: makeInvalidJson strategy (dead code, never wired)
  • Removed: Unreachable event types transport-partial and transport-corrupt-headers
  • Fixed: Strategy mapping now uses structural descriptors (kind field) instead of fragile substring matching on human-readable names
  • Fixed: truncateJson now actually uses the RNG parameter (was always cutting at 50%)
  • Fixed: assertTestEnv moved to constructor (was violating its own invariant by calling at request time)

Outbound Chaos Now Usable

  • New: wrapFetch() helper — wraps any fetch implementation 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 prevent probability: 1 from 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-size limit (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[].id syntax). Arrays of objects now require explicit x-ensures formulas.
  • 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 default scope
  • C2: Plugin contract builder — routes option 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: subscript dependency 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:201statusCode == 201
    • response_body(this).idresponse.body.id
    • request_headers(this).authrequest.headers.auth
    • if a then b else Ta ? b : true (or !a || b)
    • for x in arr: parr.every(x => p)
    • x matches /r//r/.test(x)
    • previous(expr)previous.* (e.g., previous.response.body.count)
    • T / Ftrue / false

Bundle Size

  • Net reduction: deleted 915-line custom parser, replaced with ~3KB Justin dependency
  • No external parser dependencies beyond subscript

API Changes

  • ValidatedFormula type simplified — no more FormulaNode, Comparator, etc.
  • Extension predicates now register as context variables/methods, not operation headers
  • All x-ensures and x-requires arrays 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=test only
  • ChaosEngine class 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 matcher predicate
  • 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 else clause is optional — defaults to else 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.md with 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-data request generation from JSON Schema annotations
  • Fake file generation with size, MIME type, and count constraints
  • request.files and request.fields Justin 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.chunks and response.duration Justin 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 dependsOn with topological sort
  • Async boot: onSuiteStart hooks 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/): Parse text/event-stream responses 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) and runWebSocketTests() runner

Schema-to-Contract Inference

  • Automatically derive Justin expressions from JSON Schema response definitions
  • Infers != null for required fields
  • Infers >= / <= for minimum / maximum bounds
  • Infers .test() for pattern regexes
  • Infers == for const values and small enum sets
  • 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 resolvers
  • validateFormula() with error position and suggestions for common mistakes
  • New types: MultipartFile, MultipartPayload, streaming response fields

Changed

  • ApophisExtension interface includes headers, dependsOn, healthCheck fields
  • parse() accepts optional extensionHeaders parameter
  • ExtensionRegistry exposes getExtensionHeaders(), 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 resolveOperation and resolveOperationAsync
  • 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