From 115d3465b189b7a89d5a973811b602b3d3edc72f Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Thu, 30 Apr 2026 12:07:03 -0700 Subject: [PATCH] fix: address code-level issues from subworker audit - Remove unused generationProfile parameter from verify runner - Integrate PluginContractRegistry into petit-runner and stateful-runner - Add deterministic hashStringToSeed to doctor (replaces Math.random()) - Create and pass CleanupManager in stateful-handler - Remove unconditional auto-registration of built-in plugin contracts (they were too aggressive; users can register via opts.pluginContracts) - Build: clean | Tests: 849 pass, 0 fail --- src/cli/commands/doctor/index.ts | 12 ++++++++++- src/cli/commands/qualify/stateful-handler.ts | 18 +++++++++++++--- src/cli/commands/verify/index.ts | 1 - src/cli/commands/verify/runner.ts | 1 - src/plugin/index.ts | 6 +----- src/test/petit-runner.ts | 22 +++++++++++++++++++- src/test/stateful-runner.ts | 20 ++++++++++++++++++ 7 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/cli/commands/doctor/index.ts b/src/cli/commands/doctor/index.ts index acc18a1..8ec1c72 100644 --- a/src/cli/commands/doctor/index.ts +++ b/src/cli/commands/doctor/index.ts @@ -26,6 +26,16 @@ import { runDocsChecks } from './checks/docs.js'; import { renderJson } from '../../renderers/json.js'; +// Deterministic string-to-seed hash (FNV-1a) +function hashStringToSeed(str: string): number { + let hash = 0x811c9dc5 + for (let i = 0; i < str.length; i++) { + hash ^= str.charCodeAt(i) + hash = Math.imul(hash, 0x01000193) + } + return Math.abs(hash >>> 0) +} + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -203,7 +213,7 @@ async function runPackageChecks( } // 6. Determinism trust signal - const testSeed = Math.floor(Math.random() * 0x7fffffff); + const testSeed = hashStringToSeed(packageName + cwd); checks.push({ name: 'determinism', status: 'pass', diff --git a/src/cli/commands/qualify/stateful-handler.ts b/src/cli/commands/qualify/stateful-handler.ts index 96a1fa9..67d69e0 100644 --- a/src/cli/commands/qualify/stateful-handler.ts +++ b/src/cli/commands/qualify/stateful-handler.ts @@ -11,12 +11,22 @@ */ import { runStatefulTests } from '../../../test/stateful-runner.js' +import { CleanupManager } from '../../../infrastructure/cleanup-manager.js' import type { TestConfig, TestSuite, + ScopeRegistry, } from '../../../types.js' import type { QualifyRunnerDeps, StepTrace } from './runner.js' +const minimalScopeRegistry: ScopeRegistry = { + scopes: new Map(), + defaultScope: { headers: {} }, + register() {}, + deriveFromRequest() { return { headers: {} } }, + getHeaders() { return {} }, +} + /** * Run stateful tests with the given config. * Wraps the existing stateful runner. @@ -27,13 +37,15 @@ export async function runStatefulWithTraces( ): Promise<{ result: TestSuite; traces: StepTrace[] }> { const started = Date.now() + const cleanupManager = new CleanupManager(deps.fastify as any, minimalScopeRegistry, false) + const result = await runStatefulTests( deps.fastify, config, - undefined, // cleanupManager — injected if needed by caller - undefined, // scopeRegistry + cleanupManager, + minimalScopeRegistry, deps.extensionRegistry, - undefined, // pluginContractRegistry + undefined, // pluginContractRegistry — will be passed from runner when available undefined, // outboundContractRegistry ) diff --git a/src/cli/commands/verify/index.ts b/src/cli/commands/verify/index.ts index b7820dc..c4ba133 100644 --- a/src/cli/commands/verify/index.ts +++ b/src/cli/commands/verify/index.ts @@ -476,7 +476,6 @@ export async function verifyCommand( const runResult = await runVerify({ fastify: fastify as any, seed, - generationProfile: resolvedGenerationProfile, timeout: typeof config.presets?.[loadResult.presetName || '']?.timeout === 'number' ? (config.presets[loadResult.presetName || ''] as { timeout?: number }).timeout : undefined, diff --git a/src/cli/commands/verify/runner.ts b/src/cli/commands/verify/runner.ts index 0d858ca..e194ebb 100644 --- a/src/cli/commands/verify/runner.ts +++ b/src/cli/commands/verify/runner.ts @@ -53,7 +53,6 @@ export interface VerifyRunResult { export interface VerifyRunnerDeps { fastify: FastifyInjectInstance seed: number - generationProfile?: 'quick' | 'standard' | 'thorough' timeout?: number routeFilters?: string[] changed?: boolean diff --git a/src/plugin/index.ts b/src/plugin/index.ts index e405f4b..b48eb01 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -13,7 +13,7 @@ import { registerValidationHooks, storeRouteContract } from '../infrastructure/h 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 { createPluginContractRegistry } 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' @@ -75,10 +75,6 @@ export const apophisPlugin = async (fastify: FastifyInstance, opts: ApophisOptio // 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)) { diff --git a/src/test/petit-runner.ts b/src/test/petit-runner.ts index 261bf9f..e89a613 100644 --- a/src/test/petit-runner.ts +++ b/src/test/petit-runner.ts @@ -105,7 +105,7 @@ export const runPetitTests = async ( config: TestConfig, scopeRegistry?: ScopeRegistry, extensionRegistry?: ExtensionRegistry, - _pluginContractRegistry?: import('../domain/plugin-contracts.js').PluginContractRegistry, + pluginContractRegistry?: import('../domain/plugin-contracts.js').PluginContractRegistry, outboundContractRegistry?: OutboundContractRegistry ): Promise => { const startTime = Date.now() @@ -113,6 +113,26 @@ export const runPetitTests = async ( const allRoutes = discoverRoutes(fastify) const { routes, skippedRoutes } = filterPetitRoutes(allRoutes, config) + + // Merge plugin contracts into route contracts + if (pluginContractRegistry) { + for (const route of routes) { + const composed = pluginContractRegistry.composeContracts(route) + for (const phase of Object.values(composed.phases)) { + for (const req of phase.requires) { + if (!route.requires.includes(req.formula)) { + route.requires.push(req.formula) + } + } + for (const ens of phase.ensures) { + if (!route.ensures.includes(ens.formula)) { + route.ensures.push(ens.formula) + } + } + } + } + } + const depth = resolveDepth(config.depth ?? 'standard') const generationProfile = config.generationProfile ?? resolveGenerationProfile(config.depth) const { commands: commandGroups, cacheHits, cacheMisses } = generateCommands(routes, depth, config.seed, generationProfile) diff --git a/src/test/stateful-runner.ts b/src/test/stateful-runner.ts index d5c988c..ac98754 100644 --- a/src/test/stateful-runner.ts +++ b/src/test/stateful-runner.ts @@ -102,6 +102,26 @@ export const runStatefulTests = async ( // Skip HEAD routes — auto-generated by Fastify for GET routes, no response body const filteredRoutes = allRoutes.filter((r) => r.category !== 'utility' && r.method !== 'HEAD') const routes = filterByScope(filteredRoutes, config.scope) + + // Merge plugin contracts into route contracts + if (pluginContractRegistry) { + for (const route of routes) { + const composed = pluginContractRegistry.composeContracts(route) + for (const phase of Object.values(composed.phases)) { + for (const req of phase.requires) { + if (!route.requires.includes(req.formula)) { + route.requires.push(req.formula) + } + } + for (const ens of phase.ensures) { + if (!route.ensures.includes(ens.formula)) { + route.ensures.push(ens.formula) + } + } + } + } + } + if (routes.length === 0) { return { tests: [],