chore: crush git history - reborn from consolidation on 2026-03-10
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* Core plugin types for APOPHIS.
|
||||
* Route contracts, evaluation context, HTTP methods, and plugin decorations.
|
||||
*/
|
||||
|
||||
import type { PluginContractSpec } from '../plugin/contracts.js'
|
||||
import type { MultipartPayload, RedirectEntry } from '../infrastructure/http-executor.js'
|
||||
import type { TrackedResource } from '../infrastructure/cleanup-manager.js'
|
||||
|
||||
// ============================================================================
|
||||
// Branded Types (compile-time validation)
|
||||
// ============================================================================
|
||||
|
||||
/** Contract formula string. APOSTL is the primary and only syntax for contracts. */
|
||||
export type ValidatedFormula = string
|
||||
|
||||
/** Branded string representing a validated HTTP method. Only standard HTTP methods are allowed. */
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE' | 'CONNECT'
|
||||
|
||||
// ============================================================================
|
||||
// Domain: Route Classification
|
||||
// ============================================================================
|
||||
|
||||
export type OperationCategory = 'constructor' | 'mutator' | 'observer' | 'destructor' | 'utility'
|
||||
|
||||
export interface RouteVariant {
|
||||
name: string
|
||||
headers?: Record<string, string>
|
||||
when?: string // APOSTL condition for conditional variant selection
|
||||
}
|
||||
|
||||
export interface RouteContract {
|
||||
path: string
|
||||
method: HttpMethod
|
||||
category: OperationCategory
|
||||
requires: ValidatedFormula[]
|
||||
ensures: ValidatedFormula[]
|
||||
invariants: ValidatedFormula[]
|
||||
regexPatterns: Record<string, string>
|
||||
validateRuntime: boolean
|
||||
schema?: Record<string, unknown>
|
||||
/** Per-route timeout in milliseconds, extracted from schema['x-timeout']. Overrides global/plugin timeout. */
|
||||
timeout?: number
|
||||
/** Outbound dependency contracts for this route. Extracted from schema['x-outbound'] and normalized. */
|
||||
outbound?: readonly OutboundBinding[]
|
||||
/** Route-level variants for negotiated content-type or feature testing. Extracted from schema['x-variants']. */
|
||||
variants?: RouteVariant[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Domain: Scope / Tenant Isolation
|
||||
// ============================================================================
|
||||
|
||||
export interface ScopeConfig {
|
||||
headers: Record<string, string>
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ScopeRegistry {
|
||||
readonly scopes: ReadonlyMap<string, ScopeConfig>
|
||||
readonly defaultScope: ScopeConfig
|
||||
register(scopeName: string, config: ScopeConfig): void
|
||||
deriveFromRequest(headers: Record<string, string>): ScopeConfig
|
||||
getHeaders(scopeName: string | null, overrides?: Record<string, string>): Record<string, string>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Formula: Operation Resolver
|
||||
// ============================================================================
|
||||
|
||||
export interface OperationResolver {
|
||||
readonly cache: Map<string, EvalContext>
|
||||
execute(method: 'GET', url: string): Promise<EvalContext>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Formula: Evaluation Context
|
||||
// ============================================================================
|
||||
|
||||
export interface EvalContext {
|
||||
readonly request: {
|
||||
readonly body: unknown
|
||||
readonly headers: Record<string, string>
|
||||
readonly query: Record<string, unknown>
|
||||
readonly params: Record<string, unknown>
|
||||
readonly cookies?: Record<string, string> | undefined
|
||||
readonly multipart?: MultipartPayload
|
||||
}
|
||||
readonly response: {
|
||||
readonly body: unknown
|
||||
readonly headers: Record<string, string>
|
||||
readonly statusCode: number
|
||||
readonly responseTime?: number
|
||||
/** Parsed chunks for streaming responses (e.g., NDJSON). Only populated when route has x-streaming annotation. */
|
||||
readonly chunks?: readonly unknown[]
|
||||
/** Total stream duration in milliseconds. Only populated when route has x-streaming annotation. */
|
||||
readonly streamDurationMs?: number
|
||||
}
|
||||
readonly previous?: EvalContext
|
||||
/** Snapshot of the current request before the operation executed. Used for paper-style previous(...) over pure cross-operation checks. */
|
||||
readonly before?: EvalContext
|
||||
/** Runtime executor for pure operations referenced inside APOSTL formulas. */
|
||||
readonly operationResolver?: OperationResolver
|
||||
/** Redirect chain captured during request execution. Empty if no redirects occurred. */
|
||||
readonly redirects?: ReadonlyArray<RedirectEntry>
|
||||
/** Whether the request timed out. */
|
||||
readonly timedOut?: boolean
|
||||
/** The timeout value in milliseconds that was applied to this request. */
|
||||
readonly timeoutMs?: number
|
||||
}
|
||||
|
||||
export type EvalResult =
|
||||
| { success: true; value: unknown }
|
||||
| { success: false; error: string; violation?: ContractViolation }
|
||||
|
||||
// ============================================================================
|
||||
// Domain: Contract Violations (Rich Error Context)
|
||||
// ============================================================================
|
||||
|
||||
export interface ContractViolation {
|
||||
readonly type: 'contract-violation'
|
||||
readonly kind: 'precondition' | 'postcondition' | 'invariant' | 'regex'
|
||||
readonly route: { readonly method: string; readonly path: string }
|
||||
readonly formula: string
|
||||
readonly request: {
|
||||
readonly body: unknown
|
||||
readonly headers: Record<string, string>
|
||||
readonly query: Record<string, unknown>
|
||||
readonly params: Record<string, unknown>
|
||||
}
|
||||
readonly response: {
|
||||
readonly statusCode: number
|
||||
readonly headers: Record<string, string>
|
||||
readonly body: unknown
|
||||
}
|
||||
readonly context: {
|
||||
readonly expected: string
|
||||
readonly actual: string
|
||||
readonly diff: string | null
|
||||
}
|
||||
readonly suggestion: string
|
||||
/** Source of the contract: 'route' or 'plugin:name' */
|
||||
readonly source?: 'route' | `plugin:${string}`
|
||||
/** Hook phase for plugin contracts (e.g., 'onRequest', 'onSend') */
|
||||
readonly phase?: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Infrastructure: Fastify Inject Instance
|
||||
// ============================================================================
|
||||
|
||||
export interface FastifyInjectInstance {
|
||||
routes?: Array<{ method: string; url: string; schema?: Record<string, unknown> }>
|
||||
[key: string]: unknown
|
||||
inject(opts: { method: string; url: string; payload?: unknown; headers?: Record<string, string> }): Promise<{
|
||||
json(): unknown
|
||||
statusCode: number
|
||||
headers: Record<string, unknown>
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ApophisOptions {
|
||||
readonly swagger?: Record<string, unknown>
|
||||
readonly runtime?: 'off' | 'warn' | 'error'
|
||||
readonly cleanup?: boolean
|
||||
readonly scopes?: Record<string, ScopeConfig>
|
||||
readonly timeout?: number
|
||||
readonly extensions?: ReadonlyArray<unknown>
|
||||
readonly pluginContracts?: Record<string, PluginContractSpec>
|
||||
readonly outboundContracts?: Record<string, OutboundContractSpec>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Plugin: Decorated Fastify Instance
|
||||
// ============================================================================
|
||||
|
||||
export interface ApophisDecorations {
|
||||
readonly scope: ScopeRegistry
|
||||
readonly contract: (opts?: TestConfig) => Promise<TestSuite>
|
||||
readonly stateful: (opts?: TestConfig) => Promise<TestSuite>
|
||||
readonly check: (method: string, path: string) => Promise<CheckResult>
|
||||
readonly scenario: (opts: ScenarioConfig) => Promise<ScenarioResult>
|
||||
readonly cleanup: () => Promise<Array<{ resource: TrackedResource; error?: string }>>
|
||||
readonly spec: () => Record<string, unknown>
|
||||
/** Test-only utilities. These are NOT available in production. */
|
||||
readonly test: ApophisTestDecorations
|
||||
}
|
||||
|
||||
export interface ApophisTestDecorations {
|
||||
/** Register plugin contracts for hook-phase behavioral contracts */
|
||||
readonly registerPluginContracts: (name: string, spec: PluginContractSpec) => void
|
||||
/** Register shared outbound dependency contracts */
|
||||
readonly registerOutboundContracts: (contracts: Record<string, OutboundContractSpec>) => void
|
||||
/** Enable outbound mocking for imperative E2E tests */
|
||||
readonly enableOutboundMocks: (opts?: TestConfig['outboundMocks']) => void
|
||||
/** Disable outbound mocking */
|
||||
readonly disableOutboundMocks: () => void
|
||||
/** Get recorded outbound calls for a contract (or all if no name given) */
|
||||
readonly getOutboundCalls: (name?: string) => ReadonlyArray<OutboundCallRecord>
|
||||
}
|
||||
|
||||
// Forward declarations to avoid circular deps — these are defined in sibling modules
|
||||
export interface TestConfig {
|
||||
readonly depth?: import('./formula.js').TestDepth
|
||||
readonly scope?: string
|
||||
readonly seed?: number
|
||||
readonly timeout?: number
|
||||
readonly chaos?: import('./formula.js').ChaosConfig
|
||||
readonly routes?: string[]
|
||||
readonly variants?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly headers?: Record<string, string>
|
||||
}>
|
||||
readonly invariants?: string[] | false
|
||||
readonly outboundMocks?: false | {
|
||||
readonly mode?: 'example' | 'property'
|
||||
readonly contracts?: readonly string[]
|
||||
readonly overrides?: Record<string, {
|
||||
readonly forceStatus?: number
|
||||
readonly headers?: Record<string, string>
|
||||
readonly body?: unknown
|
||||
}>
|
||||
readonly unmatched?: 'error' | 'passthrough'
|
||||
}
|
||||
}
|
||||
|
||||
export interface TestSuite {
|
||||
readonly tests: ReadonlyArray<TestResult>
|
||||
readonly summary: TestSummary
|
||||
readonly routes: ReadonlyArray<RouteDisposition>
|
||||
}
|
||||
|
||||
export interface TestResult {
|
||||
readonly ok: boolean
|
||||
readonly name: string
|
||||
readonly id: number
|
||||
readonly directive?: string
|
||||
readonly diagnostics?: TestDiagnostics
|
||||
}
|
||||
|
||||
export interface TestSummary {
|
||||
readonly passed: number
|
||||
readonly failed: number
|
||||
readonly skipped: number
|
||||
readonly timeMs: number
|
||||
readonly cacheHits: number
|
||||
readonly cacheMisses: number
|
||||
readonly counterexample?: string
|
||||
readonly pluginContractsApplied?: number
|
||||
readonly pluginContractsFailed?: number
|
||||
}
|
||||
|
||||
export interface RouteDisposition {
|
||||
readonly path: string
|
||||
readonly method: string
|
||||
readonly status: 'tested' | 'skipped' | 'no-contract' | 'scope-filtered'
|
||||
readonly reason?: string
|
||||
}
|
||||
|
||||
export interface CheckResult {
|
||||
readonly ok: boolean
|
||||
readonly violations: ContractViolation[]
|
||||
}
|
||||
|
||||
export interface ScenarioConfig {
|
||||
readonly name: string
|
||||
readonly scope?: string
|
||||
readonly timeout?: number
|
||||
readonly stopOnFailure?: boolean
|
||||
readonly steps: readonly ScenarioStep[]
|
||||
}
|
||||
|
||||
export interface ScenarioStep {
|
||||
readonly name: string
|
||||
readonly request: ScenarioStepRequest
|
||||
readonly expect: readonly string[]
|
||||
readonly capture?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ScenarioStepRequest {
|
||||
readonly method: HttpMethod
|
||||
readonly url: string
|
||||
readonly headers?: Record<string, string>
|
||||
readonly query?: Record<string, string | number | boolean>
|
||||
readonly body?: unknown
|
||||
readonly form?: Record<string, string | number | boolean>
|
||||
}
|
||||
|
||||
export interface ScenarioResult {
|
||||
readonly name: string
|
||||
readonly ok: boolean
|
||||
readonly steps: readonly ScenarioStepResult[]
|
||||
readonly summary: {
|
||||
readonly passed: number
|
||||
readonly failed: number
|
||||
readonly timeMs: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ScenarioStepResult {
|
||||
readonly name: string
|
||||
readonly ok: boolean
|
||||
readonly statusCode?: number
|
||||
readonly diagnostics?: TestDiagnostics
|
||||
readonly captures?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface TestDiagnostics {
|
||||
readonly error?: string
|
||||
readonly statusCode?: number
|
||||
readonly violation?: ContractViolation
|
||||
readonly suggestion?: string
|
||||
readonly formula?: string
|
||||
readonly kind?: string
|
||||
readonly expected?: string
|
||||
readonly actual?: string
|
||||
readonly diff?: string | null
|
||||
readonly counterexample?: string
|
||||
readonly request?: unknown
|
||||
readonly response?: unknown
|
||||
readonly dependencyResponses?: ReadonlyArray<unknown>
|
||||
readonly chaosEvents?: ReadonlyArray<unknown>
|
||||
readonly failureBoundary?: string
|
||||
readonly chaos?: {
|
||||
readonly injected: boolean
|
||||
readonly events: ReadonlyArray<{
|
||||
readonly type: string
|
||||
readonly contractName?: string
|
||||
readonly delayMs?: number
|
||||
readonly statusCode?: number
|
||||
readonly corruptionStrategy?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export interface OutboundCallRecord {
|
||||
readonly name: string
|
||||
readonly url: string
|
||||
readonly method: string
|
||||
readonly requestBody?: unknown
|
||||
readonly responseStatus: number
|
||||
readonly responseHeaders: Record<string, string>
|
||||
readonly responseBody: unknown
|
||||
readonly timestamp: number
|
||||
}
|
||||
|
||||
// Forward declarations for outbound contracts (defined in formula.ts)
|
||||
export interface OutboundContractSpec {
|
||||
readonly target: string
|
||||
readonly method: string
|
||||
readonly request?: Record<string, unknown>
|
||||
readonly response: Record<number, Record<string, unknown>>
|
||||
readonly chaos?: import('./formula.js').OutboundChaosConfig
|
||||
readonly ensures?: readonly string[]
|
||||
readonly resource?: {
|
||||
readonly idField: string
|
||||
readonly idPattern?: string
|
||||
readonly createMethods?: readonly string[]
|
||||
readonly readMethods?: readonly string[]
|
||||
readonly updateMethods?: readonly string[]
|
||||
readonly deleteMethods?: readonly string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type OutboundBinding =
|
||||
| string
|
||||
| {
|
||||
readonly ref: string
|
||||
readonly chaos?: import('./formula.js').OutboundChaosConfig
|
||||
}
|
||||
| {
|
||||
readonly name: string
|
||||
readonly target: string
|
||||
readonly method: string
|
||||
readonly request?: Record<string, unknown>
|
||||
readonly response: Record<number, Record<string, unknown>>
|
||||
readonly chaos?: import('./formula.js').OutboundChaosConfig
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Extension types for APOPHIS.
|
||||
* Extension registry, predicates, and extension interfaces.
|
||||
*/
|
||||
|
||||
// Re-export moved types from their canonical locations
|
||||
export type {
|
||||
FormulaNode,
|
||||
Comparator,
|
||||
BooleanOperator,
|
||||
OperationPathSegment,
|
||||
OperationHeader,
|
||||
OperationParameter,
|
||||
OperationCall,
|
||||
ParseResult,
|
||||
} from '../domain/formula.js'
|
||||
|
||||
export type {
|
||||
RedirectEntry,
|
||||
MultipartFile,
|
||||
MultipartPayload,
|
||||
} from '../infrastructure/http-executor.js'
|
||||
|
||||
export type {
|
||||
TrackedResource,
|
||||
CleanupManager,
|
||||
} from '../infrastructure/cleanup-manager.js'
|
||||
|
||||
export type {
|
||||
ResourceHierarchy,
|
||||
ApiCommand,
|
||||
ModelState,
|
||||
} from '../domain/stateful.js'
|
||||
|
||||
export type {
|
||||
CachedCommand,
|
||||
CacheEntry,
|
||||
TestCache,
|
||||
} from '../incremental/cache.js'
|
||||
|
||||
export type {
|
||||
PluginContractSpec,
|
||||
ComposedContract,
|
||||
} from '../plugin/contracts.js'
|
||||
|
||||
// ============================================================================
|
||||
// Extension System
|
||||
// ============================================================================
|
||||
|
||||
/** Context passed to extension predicates for route filtering and matching. */
|
||||
export interface PredicateContext {
|
||||
readonly route: {
|
||||
readonly method: string
|
||||
readonly path: string
|
||||
readonly schema?: Record<string, unknown>
|
||||
}
|
||||
readonly scope?: string
|
||||
}
|
||||
|
||||
/** A predicate function used by extensions to match routes. */
|
||||
export type RoutePredicate = (ctx: PredicateContext) => boolean
|
||||
|
||||
/** An extension that can be registered with the APOPHIS plugin. */
|
||||
export interface ApophisExtension {
|
||||
readonly name: string
|
||||
/** Optional predicate to limit which routes this extension applies to. */
|
||||
readonly predicate?: RoutePredicate
|
||||
/** Hook to modify or augment the route contract before testing. */
|
||||
readonly transformContract?: (contract: import('./core.js').RouteContract) => import('./core.js').RouteContract
|
||||
/** Hook to modify the evaluation context before formula evaluation. */
|
||||
readonly transformContext?: (ctx: import('./core.js').EvalContext) => import('./core.js').EvalContext
|
||||
/** Hook to inspect or modify the test result after evaluation. */
|
||||
readonly transformResult?: (result: import('./formula.js').TestResult) => import('./formula.js').TestResult
|
||||
/** Additional formulas to enforce on matched routes. */
|
||||
readonly requires?: readonly string[]
|
||||
readonly ensures?: readonly string[]
|
||||
readonly invariants?: readonly string[]
|
||||
}
|
||||
|
||||
/** Registry for managing APOPHIS extensions. */
|
||||
export interface ExtensionRegistry {
|
||||
readonly extensions: ReadonlyArray<ApophisExtension>
|
||||
register(extension: ApophisExtension): void
|
||||
unregister(name: string): void
|
||||
findForRoute(ctx: PredicateContext): ReadonlyArray<ApophisExtension>
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* Formula and test configuration types for APOPHIS.
|
||||
* Test depths, chaos configs, outbound contracts, and result types.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Test: Configuration
|
||||
// ============================================================================
|
||||
|
||||
export type TestDepth = 'quick' | 'standard' | 'thorough' | { runs: number }
|
||||
|
||||
export interface TestConfig {
|
||||
readonly depth?: TestDepth
|
||||
readonly generationProfile?: GenerationProfile
|
||||
readonly scope?: string
|
||||
readonly seed?: number
|
||||
readonly timeout?: number
|
||||
readonly chaos?: ChaosConfig
|
||||
readonly routes?: string[]
|
||||
readonly variants?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly headers?: Record<string, string>
|
||||
}>
|
||||
readonly invariants?: string[] | false
|
||||
readonly outboundMocks?: false | {
|
||||
readonly mode?: 'example' | 'property'
|
||||
readonly contracts?: readonly string[]
|
||||
readonly overrides?: Record<string, {
|
||||
readonly forceStatus?: number
|
||||
readonly headers?: Record<string, string>
|
||||
readonly body?: unknown
|
||||
}>
|
||||
readonly unmatched?: 'error' | 'passthrough'
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Outbound Contracts
|
||||
// ============================================================================
|
||||
|
||||
export interface OutboundContractSpec {
|
||||
/** Target URL or URL pattern for the dependency */
|
||||
readonly target: string
|
||||
/** HTTP method for the dependency request */
|
||||
readonly method: string
|
||||
/** Request body JSON Schema */
|
||||
readonly request?: Record<string, unknown>
|
||||
/** Response schemas keyed by status code */
|
||||
readonly response: Record<number, Record<string, unknown>>
|
||||
/** Optional chaos config for this dependency */
|
||||
readonly chaos?: OutboundChaosConfig
|
||||
/**
|
||||
* Behavioral contract for this dependency.
|
||||
* APOSTL formulas the mock will uphold when generating responses.
|
||||
*/
|
||||
readonly ensures?: readonly string[]
|
||||
/**
|
||||
* Resource model: declares this dependency manages stateful resources.
|
||||
*/
|
||||
readonly resource?: {
|
||||
/** Field in response body that holds the resource ID */
|
||||
readonly idField: string
|
||||
/** URL pattern for fetch-by-id (e.g., '/v1/payment_intents/:id') */
|
||||
readonly idPattern?: string
|
||||
/** Methods that create resources (default: POST) */
|
||||
readonly createMethods?: readonly string[]
|
||||
/** Methods that read resources (default: GET) */
|
||||
readonly readMethods?: readonly string[]
|
||||
/** Methods that update resources (default: PATCH, PUT) */
|
||||
readonly updateMethods?: readonly string[]
|
||||
/** Methods that delete resources (default: DELETE) */
|
||||
readonly deleteMethods?: readonly string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type OutboundBinding =
|
||||
| string // reference to a shared contract by name
|
||||
| {
|
||||
/** Reference to a shared contract by name */
|
||||
readonly ref: string
|
||||
/** Route-local chaos overrides */
|
||||
readonly chaos?: OutboundChaosConfig
|
||||
}
|
||||
| {
|
||||
/** Inline contract name */
|
||||
readonly name: string
|
||||
/** Target URL or URL pattern */
|
||||
readonly target: string
|
||||
/** HTTP method */
|
||||
readonly method: string
|
||||
/** Request body JSON Schema */
|
||||
readonly request?: Record<string, unknown>
|
||||
/** Response schemas keyed by status code */
|
||||
readonly response: Record<number, Record<string, unknown>>
|
||||
/** Optional chaos config */
|
||||
readonly chaos?: OutboundChaosConfig
|
||||
}
|
||||
|
||||
export interface ResolvedOutboundContract {
|
||||
readonly name: string
|
||||
readonly target: string
|
||||
readonly method: string
|
||||
readonly request?: Record<string, unknown>
|
||||
readonly response: Record<number, Record<string, unknown>>
|
||||
readonly chaos?: OutboundChaosConfig
|
||||
readonly ensures?: readonly string[]
|
||||
readonly resource?: OutboundContractSpec['resource']
|
||||
}
|
||||
|
||||
export interface OutboundCallRecord {
|
||||
readonly name: string
|
||||
readonly url: string
|
||||
readonly method: string
|
||||
readonly requestBody?: unknown
|
||||
readonly responseStatus: number
|
||||
readonly responseHeaders: Record<string, string>
|
||||
readonly responseBody: unknown
|
||||
readonly timestamp: number
|
||||
}
|
||||
|
||||
export interface OutboundChaosConfig {
|
||||
/** Target hostname or URL pattern to intercept */
|
||||
readonly target: string
|
||||
/** Delay outbound requests */
|
||||
readonly delay?: {
|
||||
readonly probability: number
|
||||
readonly minMs: number
|
||||
readonly maxMs: number
|
||||
}
|
||||
/** Return error responses instead of forwarding */
|
||||
readonly error?: {
|
||||
readonly probability: number
|
||||
/** Possible error responses to return */
|
||||
readonly responses: Array<{
|
||||
readonly statusCode: number
|
||||
readonly headers?: Record<string, string>
|
||||
readonly body?: unknown
|
||||
}>
|
||||
}
|
||||
/** Simulate network failures */
|
||||
readonly dropout?: {
|
||||
readonly probability: number
|
||||
/** Status code to simulate (default: 504) */
|
||||
readonly statusCode?: number
|
||||
}
|
||||
/** Corrupt response bodies */
|
||||
readonly corruption?: {
|
||||
readonly probability: number
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chaos Configuration
|
||||
// ============================================================================
|
||||
|
||||
export interface ChaosConfig {
|
||||
/** Probability of injecting any chaos event (0.0 - 1.0) */
|
||||
readonly probability: number
|
||||
/** Delay injection: add artificial latency */
|
||||
readonly delay?: {
|
||||
readonly probability: number
|
||||
readonly minMs: number
|
||||
readonly maxMs: number
|
||||
}
|
||||
/** Error injection: force HTTP error responses */
|
||||
readonly error?: {
|
||||
readonly probability: number
|
||||
readonly statusCode: number
|
||||
readonly body?: unknown
|
||||
}
|
||||
/** Dropout injection: simulate network failure */
|
||||
readonly dropout?: {
|
||||
readonly probability: number
|
||||
/** Status code to return (default: 504 Gateway Timeout) */
|
||||
readonly statusCode?: number
|
||||
}
|
||||
/** Corruption injection: corrupt response bodies */
|
||||
readonly corruption?: {
|
||||
readonly probability: number
|
||||
}
|
||||
/** Per-route chaos overrides. Keys are route paths, values override global config for that route */
|
||||
readonly routes?: Record<string, Partial<Omit<ChaosConfig, 'routes'>>>
|
||||
/** Include only these routes for chaos (if empty, all routes are included) */
|
||||
readonly include?: string[]
|
||||
/** Exclude these routes from chaos */
|
||||
readonly exclude?: string[]
|
||||
/** Resilience verification: retry after chaos to verify recovery */
|
||||
readonly resilience?: {
|
||||
/** Enable resilience verification (default: false) */
|
||||
readonly enabled: boolean
|
||||
/** Max retry attempts after chaos (default: 3) */
|
||||
readonly maxRetries?: number
|
||||
/** Backoff between retries in ms (default: 100) */
|
||||
readonly backoffMs?: number
|
||||
}
|
||||
/** Outbound HTTP request interception for dependency-aware chaos */
|
||||
readonly outbound?: OutboundChaosConfig[]
|
||||
/** Skip resilience for non-idempotent routes (default: ['constructor', 'mutator']) */
|
||||
readonly skipResilienceFor?: ('constructor' | 'mutator' | 'observer' | 'destructor' | 'utility')[]
|
||||
/** Use proper status codes for dropout (P2) */
|
||||
readonly dropoutStatusCode?: number
|
||||
/** Maximum number of chaos injections per test suite (default: Infinity) */
|
||||
readonly maxInjectionsPerSuite?: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Depth Configuration
|
||||
// ============================================================================
|
||||
|
||||
export interface DepthConfig {
|
||||
readonly contractRuns: number
|
||||
readonly propertyRuns: number
|
||||
readonly statefulRuns: number
|
||||
readonly maxCommands: number
|
||||
}
|
||||
|
||||
export type GenerationProfile = 'quick' | 'standard' | 'thorough'
|
||||
|
||||
export const DEPTH_CONFIGS: Record<'quick' | 'standard' | 'thorough', DepthConfig> = {
|
||||
quick: { contractRuns: 10, propertyRuns: 50, statefulRuns: 5, maxCommands: 10 },
|
||||
standard: { contractRuns: 50, propertyRuns: 100, statefulRuns: 20, maxCommands: 30 },
|
||||
thorough: { contractRuns: 200, propertyRuns: 1000, statefulRuns: 100, maxCommands: 50 }
|
||||
}
|
||||
|
||||
export function resolveDepth(depth: TestDepth): DepthConfig {
|
||||
if (typeof depth === 'string') {
|
||||
return DEPTH_CONFIGS[depth]
|
||||
}
|
||||
return {
|
||||
contractRuns: depth.runs,
|
||||
propertyRuns: depth.runs,
|
||||
statefulRuns: Math.max(1, Math.floor(depth.runs / 10)),
|
||||
maxCommands: Math.max(5, Math.floor(depth.runs / 5)),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveGenerationProfile(depth: TestDepth | undefined): GenerationProfile {
|
||||
if (depth === undefined) {
|
||||
return 'standard'
|
||||
}
|
||||
if (typeof depth === 'string') {
|
||||
return depth
|
||||
}
|
||||
|
||||
if (depth.runs <= 25) return 'quick'
|
||||
if (depth.runs >= 250) return 'thorough'
|
||||
return 'standard'
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test: Results
|
||||
// ============================================================================
|
||||
|
||||
export interface TestResult {
|
||||
readonly ok: boolean
|
||||
readonly name: string
|
||||
readonly id: number
|
||||
readonly directive?: string
|
||||
readonly diagnostics?: TestDiagnostics
|
||||
}
|
||||
|
||||
export interface TestDiagnostics {
|
||||
readonly error?: string
|
||||
readonly statusCode?: number
|
||||
readonly violation?: import('./core.js').ContractViolation
|
||||
readonly suggestion?: string
|
||||
readonly formula?: string
|
||||
readonly kind?: string
|
||||
readonly expected?: string
|
||||
readonly actual?: string
|
||||
readonly diff?: string | null
|
||||
readonly counterexample?: string
|
||||
readonly request?: unknown
|
||||
readonly response?: unknown
|
||||
readonly dependencyResponses?: ReadonlyArray<unknown>
|
||||
readonly chaosEvents?: ReadonlyArray<unknown>
|
||||
readonly failureBoundary?: string
|
||||
/** Chaos injection details — array of events that were applied */
|
||||
readonly chaos?: {
|
||||
readonly injected: boolean
|
||||
readonly events: ReadonlyArray<{
|
||||
readonly type: string
|
||||
readonly contractName?: string
|
||||
readonly delayMs?: number
|
||||
readonly statusCode?: number
|
||||
readonly corruptionStrategy?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export interface RouteDisposition {
|
||||
readonly path: string
|
||||
readonly method: string
|
||||
readonly status: 'tested' | 'skipped' | 'no-contract' | 'scope-filtered'
|
||||
readonly reason?: string
|
||||
}
|
||||
|
||||
export interface TestSummary {
|
||||
readonly passed: number
|
||||
readonly failed: number
|
||||
readonly skipped: number
|
||||
readonly timeMs: number
|
||||
readonly cacheHits: number
|
||||
readonly cacheMisses: number
|
||||
readonly counterexample?: string
|
||||
/** Number of plugin contracts applied during testing */
|
||||
readonly pluginContractsApplied?: number
|
||||
/** Number of plugin contract failures */
|
||||
readonly pluginContractsFailed?: number
|
||||
}
|
||||
|
||||
export interface TestSuite {
|
||||
readonly tests: ReadonlyArray<TestResult>
|
||||
readonly summary: TestSummary
|
||||
readonly routes: ReadonlyArray<RouteDisposition>
|
||||
}
|
||||
|
||||
export interface CheckResult {
|
||||
readonly ok: boolean
|
||||
readonly violations: import('./core.js').ContractViolation[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scenario Types
|
||||
// ============================================================================
|
||||
|
||||
export interface ScenarioStepRequest {
|
||||
readonly method: import('./core.js').HttpMethod
|
||||
readonly url: string
|
||||
readonly headers?: Record<string, string>
|
||||
readonly query?: Record<string, string | number | boolean>
|
||||
readonly body?: unknown
|
||||
readonly form?: Record<string, string | number | boolean>
|
||||
}
|
||||
|
||||
export interface ScenarioStep {
|
||||
readonly name: string
|
||||
readonly request: ScenarioStepRequest
|
||||
readonly expect: readonly string[]
|
||||
readonly capture?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ScenarioConfig {
|
||||
readonly name: string
|
||||
readonly scope?: string
|
||||
readonly timeout?: number
|
||||
readonly stopOnFailure?: boolean
|
||||
readonly steps: readonly ScenarioStep[]
|
||||
}
|
||||
|
||||
export interface ScenarioStepResult {
|
||||
readonly name: string
|
||||
readonly ok: boolean
|
||||
readonly statusCode?: number
|
||||
readonly diagnostics?: TestDiagnostics
|
||||
readonly captures?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ScenarioResult {
|
||||
readonly name: string
|
||||
readonly ok: boolean
|
||||
readonly steps: readonly ScenarioStepResult[]
|
||||
readonly summary: {
|
||||
readonly passed: number
|
||||
readonly failed: number
|
||||
readonly timeMs: number
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user