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
+266
View File
@@ -0,0 +1,266 @@
/**
* Unified Builder Module — All APOPHIS plugin builder functions
*
* Responsibility: Consolidate builder files into a single module
* to reduce module fragmentation and improve maintainability.
*/
import type { FastifyInstance } from 'fastify'
import type { ExtensionRegistry } from '../extension/types.js'
import type { ScenarioConfig, ScenarioResult, ScopeRegistry, ApophisOptions, TestConfig, TestSuite, CheckResult } from '../types.js'
import type { CleanupManager, TrackedResource } from '../infrastructure/cleanup-manager.js'
import type { PluginContractRegistry } from '../domain/plugin-contracts.js'
import type { OutboundContractRegistry } from '../domain/outbound-contracts.js'
import { runScenario } from '../test/scenario-runner.js'
import { runPetitTests } from '../test/petit-runner.js'
import { runStatefulTests } from '../test/stateful-runner.js'
import { assertNonProduction } from '../infrastructure/production-safety.js'
import { discoverRoutes } from '../domain/discovery.js'
import { buildRequest, extractPathParams } from '../domain/request-builder.js'
import { executeHttp } from '../infrastructure/http-executor.js'
import { validatePostconditionsAsync } from '../domain/contract-validation.js'
import { createOperationResolver, prefetchPreviousOperations } from '../formula/runtime.js'
import { parse } from '../formula/parser.js'
// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------
const normalizeTestConfig = (opts: TestConfig = {}): TestConfig => ({
depth: opts.depth ?? 'standard',
scope: opts.scope,
seed: opts.seed,
timeout: opts.timeout,
chaos: opts.chaos,
routes: opts.routes,
variants: opts.variants,
outboundMocks: opts.outboundMocks,
})
// ---------------------------------------------------------------------------
// Cleanup builder
// ---------------------------------------------------------------------------
/**
* Build cleanup function for resource cleanup
* @param cleanupManager - The cleanup manager instance
* @returns Async function that triggers cleanup and returns results
*/
export const buildCleanup = (cleanupManager: CleanupManager) =>
async (): Promise<Array<{ resource: TrackedResource; error?: string }>> => {
return cleanupManager.cleanup()
}
// ---------------------------------------------------------------------------
// Scenario builder
// ---------------------------------------------------------------------------
/**
* Build scenario runner function
* @param fastify - Fastify instance for injection
* @param scope - Scope registry for headers
* @param extensionRegistry - Extension registry for custom operations
* @returns Async function that runs a scenario config
*/
export const buildScenario = (
fastify: FastifyInstance,
scope: ScopeRegistry,
extensionRegistry: ExtensionRegistry
) => async (opts: ScenarioConfig): Promise<ScenarioResult> => {
assertNonProduction('scenario')
const scopeHeaders = scope.getHeaders(opts.scope ?? null)
const injectInstance = fastify as unknown as import('../types.js').FastifyInjectInstance
return runScenario(injectInstance, opts, scopeHeaders, extensionRegistry)
}
// ---------------------------------------------------------------------------
// Contract (PETIT) builder
// ---------------------------------------------------------------------------
/**
* Build contract test runner (PETIT)
* @param fastify - Fastify instance
* @param scope - Scope registry
* @param extensionRegistry - Extension registry
* @param pluginContractRegistry - Plugin contract registry
* @param outboundContractRegistry - Outbound contract registry
* @returns Async function that runs PETIT tests
*/
export const buildContract = (
fastify: FastifyInstance,
scope: ScopeRegistry,
extensionRegistry: ExtensionRegistry,
pluginContractRegistry: PluginContractRegistry,
outboundContractRegistry: OutboundContractRegistry
) => async (opts: TestConfig = {}): Promise<TestSuite> => {
const config = normalizeTestConfig(opts)
const injectInstance = fastify as unknown as import('../types.js').FastifyInjectInstance
const suite = await runPetitTests(injectInstance, config, scope, extensionRegistry, pluginContractRegistry, outboundContractRegistry)
// Loud failure on empty discovery
if (suite.tests.length === 0) {
const routes = discoverRoutes(fastify as unknown as { routes?: Array<{ method: string; url: string; schema?: Record<string, unknown> } > })
if (routes.length === 0) {
throw new Error(
'No routes discovered. Did you register APOPHIS before defining routes? ' +
'APOPHIS must be registered via `await fastify.register(apophis)` before any routes are defined.'
)
}
}
return suite
}
// ---------------------------------------------------------------------------
// Stateful builder
// ---------------------------------------------------------------------------
/**
* Build stateful test runner
* @param fastify - Fastify instance
* @param scope - Scope registry
* @param cleanupManager - Cleanup manager
* @param extensionRegistry - Extension registry
* @param pluginContractRegistry - Plugin contract registry
* @param outboundContractRegistry - Outbound contract registry
* @returns Async function that runs stateful tests
*/
export const buildStateful = (
fastify: FastifyInstance,
scope: ScopeRegistry,
cleanupManager: CleanupManager,
extensionRegistry: ExtensionRegistry,
pluginContractRegistry: PluginContractRegistry,
outboundContractRegistry: OutboundContractRegistry
) => async (opts: TestConfig = {}): Promise<TestSuite> => {
const config = normalizeTestConfig(opts)
const injectInstance = fastify as unknown as import('../types.js').FastifyInjectInstance
return runStatefulTests(injectInstance, config, cleanupManager, scope, extensionRegistry, pluginContractRegistry, outboundContractRegistry)
}
// ---------------------------------------------------------------------------
// Check builder
// ---------------------------------------------------------------------------
/**
* Build single route contract check runner
* @param fastify - Fastify instance
* @param scope - Scope registry
* @param extensionRegistry - Extension registry
* @param pluginContractRegistry - Plugin contract registry
* @returns Async function that checks a single route
*/
export const buildCheck = (
fastify: FastifyInstance,
scope: ScopeRegistry,
extensionRegistry: ExtensionRegistry,
pluginContractRegistry: PluginContractRegistry
) => async (method: string, path: string): Promise<CheckResult> => {
const routes = discoverRoutes(fastify as unknown as { routes?: Array<{ method: string; url: string; schema?: Record<string, unknown> } > })
const route = routes.find(r => r.method === method && r.path === path)
if (!route) {
throw new Error(`Route not found: ${method} ${path}`)
}
const scopeHeaders = scope.getHeaders(null)
let request = buildRequest(route, {}, scopeHeaders, { resources: new Map(), counters: new Map() })
// Run extension request build hooks
for (const ext of extensionRegistry.extensions) {
if (!ext.onBuildRequest) continue
const extState = extensionRegistry.getState(ext.name) ?? {}
const result = await ext.onBuildRequest({
route,
request,
scopeHeaders,
state: { resources: new Map(), counters: new Map() },
extensionState: extState,
})
if (result !== undefined) {
request = result
}
}
const preContext = {
request: {
body: request.body,
headers: request.headers,
query: request.query ?? {},
params: extractPathParams(route.path, request.url),
multipart: request.multipart,
},
response: {
body: null,
headers: {},
statusCode: 0,
},
operationResolver: createOperationResolver(
fastify as unknown as import('../types.js').FastifyInjectInstance,
request.headers
),
} as import('../types.js').EvalContext
const extensionHeaders = extensionRegistry.getExtensionHeaders()
const apostlAsts = route.ensures.flatMap((formula) => {
try {
return [parse(formula, extensionHeaders).ast]
} catch {
return []
}
})
await prefetchPreviousOperations(apostlAsts, preContext, route, extensionRegistry)
const executedCtx = await executeHttp(fastify as unknown as import('../types.js').FastifyInjectInstance, route, request)
const ctx = {
...executedCtx,
before: preContext,
operationResolver: createOperationResolver(
fastify as unknown as import('../types.js').FastifyInjectInstance,
request.headers,
preContext
),
} as import('../types.js').EvalContext
const violations: import('../types.js').ContractViolation[] = []
for (const ensure of route.ensures) {
const result = await validatePostconditionsAsync([ensure], ctx, route, extensionRegistry)
if (!result.success && result.violation) {
violations.push(result.violation)
}
}
return {
ok: violations.length === 0,
violations,
}
}
// ---------------------------------------------------------------------------
// Swagger / Spec builders
// ---------------------------------------------------------------------------
/**
* Register swagger plugin if not already present
* @param fastify - Fastify instance
* @param opts - APOPHIS options containing swagger config
*/
export const registerSwagger = async (fastify: FastifyInstance, opts: ApophisOptions): Promise<void> => {
if ((fastify as unknown as Record<string, unknown>).swagger !== undefined) {
return
}
const swagger = await import('@fastify/swagger')
await fastify.register(swagger.default as unknown as Parameters<typeof fastify.register>[0], opts.swagger ?? {})
}
/**
* Build OpenAPI spec enriched with route contract metadata
* @param fastify - Fastify instance
* @returns Function that returns the enriched spec object
*/
export const buildSpec = (fastify: FastifyInstance) => (): Record<string, unknown> => {
const routes = discoverRoutes(fastify as unknown as { routes?: Array<{ method: string; url: string; schema?: Record<string, unknown> } > })
const spec = (fastify as unknown as { swagger: () => Record<string, unknown> }).swagger()
return {
...spec,
'x-apophis-contracts': routes.map((route) => ({
path: route.path,
method: route.method,
category: route.category,
requires: route.requires,
ensures: route.ensures,
invariants: route.invariants,
})),
}
}
+42
View File
@@ -0,0 +1,42 @@
import type { RouteContract } from '../types.js'
/**
* Plugin Contract Types
* Types for plugin contract system and composed contracts.
*/
export interface PluginContractSpec {
/** Route path prefix pattern. Supports wildcards: '/api/**' matches '/api/users' */
readonly appliesTo: string
/** Contracts organized by hook phase */
readonly hooks: {
readonly [phase: string]: {
/** Preconditions that must hold before this phase executes */
readonly requires?: readonly string[]
/** Postconditions that must hold after this phase executes */
readonly ensures?: readonly string[]
}
}
/** Plugin metadata for diagnostics */
readonly meta?: {
readonly name?: string
readonly version?: string
readonly description?: string
}
/**
* Lazy extension references — Apophis extensions this plugin needs.
* Extensions are resolved at test time from the extension registry.
* If a required extension is missing, the plugin's contracts are skipped with a warning.
*/
readonly extensions?: ReadonlyArray<{
readonly name: string
readonly required?: boolean
}>
}
export interface ComposedContract {
readonly route: RouteContract
phases: {
[phase: string]: {
requires: Array<{ formula: string; source: 'route' | `plugin:${string}` }>
ensures: Array<{ formula: string; source: 'route' | `plugin:${string}` }>
}
}
}
+154
View File
@@ -0,0 +1,154 @@
/**
* APOPHIS Plugin v1.0 — Fastify plugin entry point.
* Thin wrapper: delegates all work to pure domain functions.
* Fastify plugin API is accidental; APOPHIS logic is essential.
*
* Architecture: Orchestrator — imports focused builders from submodules.
*/
import type { FastifyInstance } from 'fastify'
import { ScopeRegistry } from '../infrastructure/scope-registry.js'
import { CleanupManager } from '../infrastructure/cleanup-manager.js'
import { captureRoute } from '../domain/discovery.js'
import { registerValidationHooks, storeRouteContract } from '../infrastructure/hook-validator.js'
import { extractContract } from '../domain/contract.js'
import { createExtensionRegistry } from '../extension/registry.js'
import type { ApophisExtension } from '../extension/types.js'
import { createPluginContractRegistry, BUILTIN_PLUGIN_CONTRACTS } from '../domain/plugin-contracts.js'
import type { PluginContractRegistry } from '../domain/plugin-contracts.js'
import { OutboundContractRegistry } from '../domain/outbound-contracts.js'
import { createOutboundMockRuntime, type OutboundMockRuntime } from '../infrastructure/outbound-mock-runtime.js'
import { validateProductionSafety, assertTestEnv } from '../infrastructure/production-safety.js'
import {
registerSwagger,
buildSpec,
buildScenario,
buildCleanup,
buildContract,
buildStateful,
buildCheck,
} from './builders.js'
import type { ApophisDecorations, ApophisOptions, OutboundCallRecord, OutboundContractSpec, TestConfig } from '../types.js'
export const apophisPlugin = async (fastify: FastifyInstance, opts: ApophisOptions): Promise<void> => {
// Production safety: hard-fail if test-only options are present in production
validateProductionSafety(opts)
await registerSwagger(fastify, opts)
// Initialize registries before route capture so onRoute can validate formulas
// with any registered extension operation headers.
const pluginContractRegistry = createPluginContractRegistry()
const extensionRegistry = createExtensionRegistry()
extensionRegistry.setPluginContractRegistry(pluginContractRegistry)
// Initialize outbound contract registry
const outboundContractRegistry = new OutboundContractRegistry()
if (opts.outboundContracts) {
outboundContractRegistry.registerAll(opts.outboundContracts)
}
// Track active outbound mock runtime for imperative E2E tests
let activeMockRuntime: OutboundMockRuntime | undefined
// Capture routes as they're registered via Fastify's onRoute hook
fastify.addHook('onRoute', (routeOptions) => {
const method = Array.isArray(routeOptions.method)
? routeOptions.method.join(',')
: routeOptions.method
const schema = routeOptions.schema as Record<string, unknown> | undefined
const prefix = (routeOptions as unknown as Record<string, unknown>).prefix as string | undefined
const url = prefix && !routeOptions.url.startsWith(prefix)
? `${prefix}${routeOptions.url}`
: routeOptions.url
captureRoute(fastify, {
method,
url,
schema,
prefix,
})
// Extract contract and attach to route config for runtime validation hooks
const contract = extractContract(url, method, schema)
if (contract.validateRuntime && (contract.requires.length > 0 || contract.ensures.length > 0)) {
const config = routeOptions.config as Record<string, unknown> || {}
config.apophisContract = contract
routeOptions.config = config as typeof routeOptions.config
// Store for hook validator lookup (Fastify doesn't expose routes after ready)
const routeKey = `${contract.method} ${contract.path}`
storeRouteContract(routeKey, contract, extensionRegistry.getExtensionHeaders())
}
})
// Initialize scope registry with explicit config or empty
const scope = new ScopeRegistry(opts.scopes ?? {})
const cleanupManager = new CleanupManager(fastify, scope, opts.cleanup ?? false)
// Register built-in plugin contracts
for (const [name, spec] of Object.entries(BUILTIN_PLUGIN_CONTRACTS)) {
pluginContractRegistry.register(name, spec)
}
// Register user-provided plugin contracts
if (opts.pluginContracts) {
for (const [name, spec] of Object.entries(opts.pluginContracts)) {
pluginContractRegistry.register(name, spec)
}
}
if (opts.extensions) {
for (const ext of opts.extensions) {
extensionRegistry.register(ext as ApophisExtension)
}
}
const decorations: ApophisDecorations = {
scope,
contract: buildContract(fastify, scope, extensionRegistry, pluginContractRegistry, outboundContractRegistry),
stateful: buildStateful(fastify, scope, cleanupManager, extensionRegistry, pluginContractRegistry, outboundContractRegistry),
check: buildCheck(fastify, scope, extensionRegistry, pluginContractRegistry),
scenario: buildScenario(fastify, scope, extensionRegistry),
cleanup: buildCleanup(cleanupManager),
spec: buildSpec(fastify),
test: {
registerPluginContracts: (name: string, spec: import('../types.js').PluginContractSpec) => {
assertTestEnv('registerPluginContracts')
pluginContractRegistry.register(name, spec)
},
registerOutboundContracts: (contracts: Record<string, OutboundContractSpec>) => {
assertTestEnv('registerOutboundContracts')
outboundContractRegistry.registerAll(contracts)
},
enableOutboundMocks: (mockOpts?: TestConfig['outboundMocks']) => {
assertTestEnv('enableOutboundMocks')
if (activeMockRuntime) {
activeMockRuntime.restore()
}
// Disable case
if (mockOpts === false) {
activeMockRuntime = undefined
return
}
const contracts = outboundContractRegistry.resolve(
Array.from(outboundContractRegistry['contracts'].entries()).map(([name]) => name)
)
const mode = mockOpts?.mode ?? 'example'
const unmatched = mockOpts?.unmatched ?? 'error'
const seed = Math.floor(Math.random() * 0xFFFFFFFF)
activeMockRuntime = createOutboundMockRuntime({
contracts,
mode,
overrides: mockOpts?.overrides,
unmatched,
seed,
})
activeMockRuntime.install()
},
disableOutboundMocks: () => {
assertTestEnv('disableOutboundMocks')
if (activeMockRuntime) {
activeMockRuntime.restore()
activeMockRuntime = undefined
}
},
getOutboundCalls: (name?: string): ReadonlyArray<OutboundCallRecord> => {
assertTestEnv('getOutboundCalls')
return activeMockRuntime?.getCalls(name) ?? []
},
},
}
fastify.decorate('apophis', decorations)
// Runtime validation: never register hooks in production
const isProd = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'prod'
if (opts.runtime && opts.runtime !== 'off' && !isProd) {
registerValidationHooks(fastify, { validateRuntime: true, runtimeLevel: opts.runtime })
}
}