chore: crush git history - reborn from consolidation on 2026-03-10

This commit is contained in:
John Dvorak
2026-03-10 00:00:00 -07:00
commit d278c4b105
313 changed files with 87549 additions and 0 deletions
+378
View File
@@ -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
}
+86
View File
@@ -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>
}
+368
View File
@@ -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
}
}