diff --git a/docs/OUTBOUND_CONTRACT_MOCKING_SPEC.md b/docs/OUTBOUND_CONTRACT_MOCKING_SPEC.md index d70be18..07b4311 100644 --- a/docs/OUTBOUND_CONTRACT_MOCKING_SPEC.md +++ b/docs/OUTBOUND_CONTRACT_MOCKING_SPEC.md @@ -1,6 +1,9 @@ ## Outbound Contract-Driven Mocking Spec -Status: Proposed +Status: Implemented (Phase 1) + +Phase 1 (implemented): Schema parsing (`x-outbound`), mock runtime, imperative API (`enableOutboundMocks`, `getOutboundCalls`), fetch patching. +Phase 2 (pending): APOSTL extensions `outbound_calls(this)` and `outbound_last(this)` for contract assertions. Date: 2026-04-27 This document supersedes Arbiter's local draft at `~/Business/workspace/Arbiter/docs/APOPHIS_OUTBOUND_MOCK_PROPOSAL.md` and its interim adapter at `~/Business/workspace/Arbiter/src/server/server/services/StripeFetchAdapter.js`. diff --git a/docs/PLUGIN_CONTRACTS_SPEC.md b/docs/PLUGIN_CONTRACTS_SPEC.md index 63893cd..ea3ae07 100644 --- a/docs/PLUGIN_CONTRACTS_SPEC.md +++ b/docs/PLUGIN_CONTRACTS_SPEC.md @@ -1,6 +1,10 @@ # APOPHIS Plugin Contract System Specification -## Status: Implemented +## Status: Partially implemented + +- Registry, types, and registration API: **implemented** +- Runner integration (merging plugin contracts into route execution): **pending** +- Built-in contracts for `@fastify/auth`, `@fastify/compress`, `@fastify/cors`, `@fastify/rate-limit`: **registered but not yet applied** **Note**: Plugin contracts are complementary to Protocol Extensions (see `docs/protocol-extensions-spec.md`). Protocol extensions add domain-specific predicates (JWT, X.509, SPIFFE); plugin contracts add hook-phase behavioral contracts for Fastify plugins. diff --git a/docs/chaos.md b/docs/chaos.md index 44df7ed..cc394cc 100644 --- a/docs/chaos.md +++ b/docs/chaos.md @@ -30,6 +30,8 @@ timeout_occurred(this) == false response_time(this) < 1000 ``` +**Note**: Delay events are generated by the chaos arbitrary but the inbound delay handler is currently a no-op. Use this for timeout contract documentation; actual delay injection requires the outbound delay strategy or a custom handler. + ### Error Forces HTTP status codes. Tests error-handling contracts: diff --git a/docs/cli.md b/docs/cli.md index 7ce6dd3..92d05dc 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -255,10 +255,12 @@ apophis replay --artifact reports/apophis/failure-*.json |---|---|---|---|---| | `verify` | enabled | enabled | optional | optional, usually off | | `observe` | optional | optional | enabled | enabled | -| `qualify: scenario` | enabled | enabled | enabled with allowlist | disabled by default | -| `qualify: stateful` | enabled | enabled | synthetic-only | disabled by default | -| `qualify: chaos` | enabled | enabled | canary-only | disabled by default | -| outbound mocks | enabled | enabled | allowlisted only | disabled by default | +| `qualify` | enabled | enabled | optional | disabled by default | +| `chaos` | enabled | enabled | optional | disabled by default | | runtime throw-on-violation | optional | optional | exceptional | disabled by default | -Operational rule: Production must never inherit qualify capabilities accidentally from a generic config file. +Notes: +- `qualify` is gated as a whole. The code does not distinguish scenario, stateful, and chaos sub-modes in environment policy. +- `chaos` on protected routes requires `allowChaosOnProtected: true`. +- `observe` blocking requires `allowBlocking: true`. +- Production must never inherit qualify capabilities accidentally from a generic config file. diff --git a/docs/getting-started.md b/docs/getting-started.md index e001100..924e39b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -50,7 +50,7 @@ app.post('/users', { }); ``` -> **Warning:** Using `Date.now()` or `Math.random()` in handlers breaks determinism and replay. Use a stable function of the input instead. +> **Warning:** Using `Date.now()` or `Math.random()` in handlers breaks determinism and replay. Use a stable function of the input instead. APOPHIS does not proactively detect nondeterministic handlers; it warns only when a replay diverges from the original run. ## Step 4: Run Verify @@ -105,6 +105,31 @@ Fix the bug in your handler. Re-run verify. The failure should now pass. - Add observe mode for runtime drift detection: see [observe.md](observe.md) - Add qualify mode for scenario, stateful, and chaos checks: see [qualify.md](qualify.md) +## Variants + +Test the same route with different headers or content types: + +```javascript +await fastify.apophis.contract({ + variants: [ + { name: 'json', headers: { accept: 'application/json' } }, + { name: 'xml', headers: { accept: 'application/xml' } } + ] +}) +``` + +Or declare variants in the route schema: + +```javascript +app.get('/users', { + schema: { + 'x-variants': [ + { name: 'json', headers: { accept: 'application/json' } } + ] + } +}) +``` + ## Config Reference For the full configuration reference, see [CLI Reference](cli.md). diff --git a/docs/verify.md b/docs/verify.md index a8523a7..cceefa1 100644 --- a/docs/verify.md +++ b/docs/verify.md @@ -81,7 +81,7 @@ Run only routes modified in the current git branch: apophis verify --profile ci --changed ``` -If no routes changed, exits 0 with a message. +If no routes changed, exits 2 with a message. ## Failure Output Format diff --git a/src/cli/core/generation-profile.ts b/src/cli/core/generation-profile.ts index 2e19bbc..8c695bf 100644 --- a/src/cli/core/generation-profile.ts +++ b/src/cli/core/generation-profile.ts @@ -10,7 +10,12 @@ export class GenerationProfileResolutionError extends Error { } function isBuiltInProfile(value: string): value is ResolvedGenerationProfile { - return value === 'quick' || value === 'standard' || value === 'thorough' + return value === 'quick' || value === 'standard' || value === 'thorough' || value === 'deep' +} + +function normalizeProfile(value: string): ResolvedGenerationProfile { + if (value === 'deep') return 'thorough' + return value as ResolvedGenerationProfile } export function resolveGenerationProfileOverride( @@ -22,13 +27,13 @@ export function resolveGenerationProfileOverride( } if (isBuiltInProfile(rawProfile)) { - return rawProfile + return normalizeProfile(rawProfile) } const aliases = config.generationProfiles if (!aliases) { throw new GenerationProfileResolutionError( - `Unknown generation profile "${rawProfile}". Use one of: quick, standard, thorough, or define an alias in config.generationProfiles.`, + `Unknown generation profile "${rawProfile}". Use one of: quick, standard, deep, or define an alias in config.generationProfiles.`, ) } @@ -36,16 +41,16 @@ export function resolveGenerationProfileOverride( if (!alias) { const available = Object.keys(aliases).join(', ') || 'none' throw new GenerationProfileResolutionError( - `Unknown generation profile "${rawProfile}". Built-ins: quick, standard, thorough. Config aliases: ${available}.`, + `Unknown generation profile "${rawProfile}". Built-ins: quick, standard, deep. Config aliases: ${available}.`, ) } const target = typeof alias === 'string' ? alias : alias.base if (!isBuiltInProfile(target)) { throw new GenerationProfileResolutionError( - `Invalid generation profile alias "${rawProfile}". Alias must resolve to quick, standard, or thorough.`, + `Invalid generation profile alias "${rawProfile}". Alias must resolve to quick, standard, or deep.`, ) } - return target + return normalizeProfile(target) }