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
This commit is contained in:
John Dvorak
2026-04-30 12:07:03 -07:00
parent dc7a4205ec
commit 115d3465b1
7 changed files with 68 additions and 12 deletions
+11 -1
View File
@@ -26,6 +26,16 @@ import { runDocsChecks } from './checks/docs.js';
import { renderJson } from '../../renderers/json.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 // Types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -203,7 +213,7 @@ async function runPackageChecks(
} }
// 6. Determinism trust signal // 6. Determinism trust signal
const testSeed = Math.floor(Math.random() * 0x7fffffff); const testSeed = hashStringToSeed(packageName + cwd);
checks.push({ checks.push({
name: 'determinism', name: 'determinism',
status: 'pass', status: 'pass',
+15 -3
View File
@@ -11,12 +11,22 @@
*/ */
import { runStatefulTests } from '../../../test/stateful-runner.js' import { runStatefulTests } from '../../../test/stateful-runner.js'
import { CleanupManager } from '../../../infrastructure/cleanup-manager.js'
import type { import type {
TestConfig, TestConfig,
TestSuite, TestSuite,
ScopeRegistry,
} from '../../../types.js' } from '../../../types.js'
import type { QualifyRunnerDeps, StepTrace } from './runner.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. * Run stateful tests with the given config.
* Wraps the existing stateful runner. * Wraps the existing stateful runner.
@@ -27,13 +37,15 @@ export async function runStatefulWithTraces(
): Promise<{ result: TestSuite; traces: StepTrace[] }> { ): Promise<{ result: TestSuite; traces: StepTrace[] }> {
const started = Date.now() const started = Date.now()
const cleanupManager = new CleanupManager(deps.fastify as any, minimalScopeRegistry, false)
const result = await runStatefulTests( const result = await runStatefulTests(
deps.fastify, deps.fastify,
config, config,
undefined, // cleanupManager — injected if needed by caller cleanupManager,
undefined, // scopeRegistry minimalScopeRegistry,
deps.extensionRegistry, deps.extensionRegistry,
undefined, // pluginContractRegistry undefined, // pluginContractRegistry — will be passed from runner when available
undefined, // outboundContractRegistry undefined, // outboundContractRegistry
) )
-1
View File
@@ -476,7 +476,6 @@ export async function verifyCommand(
const runResult = await runVerify({ const runResult = await runVerify({
fastify: fastify as any, fastify: fastify as any,
seed, seed,
generationProfile: resolvedGenerationProfile,
timeout: typeof config.presets?.[loadResult.presetName || '']?.timeout === 'number' timeout: typeof config.presets?.[loadResult.presetName || '']?.timeout === 'number'
? (config.presets[loadResult.presetName || ''] as { timeout?: number }).timeout ? (config.presets[loadResult.presetName || ''] as { timeout?: number }).timeout
: undefined, : undefined,
-1
View File
@@ -53,7 +53,6 @@ export interface VerifyRunResult {
export interface VerifyRunnerDeps { export interface VerifyRunnerDeps {
fastify: FastifyInjectInstance fastify: FastifyInjectInstance
seed: number seed: number
generationProfile?: 'quick' | 'standard' | 'thorough'
timeout?: number timeout?: number
routeFilters?: string[] routeFilters?: string[]
changed?: boolean changed?: boolean
+1 -5
View File
@@ -13,7 +13,7 @@ import { registerValidationHooks, storeRouteContract } from '../infrastructure/h
import { extractContract } from '../domain/contract.js' import { extractContract } from '../domain/contract.js'
import { createExtensionRegistry } from '../extension/registry.js' import { createExtensionRegistry } from '../extension/registry.js'
import type { ApophisExtension } from '../extension/types.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 type { PluginContractRegistry } from '../domain/plugin-contracts.js'
import { OutboundContractRegistry } from '../domain/outbound-contracts.js' import { OutboundContractRegistry } from '../domain/outbound-contracts.js'
import { createOutboundMockRuntime, type OutboundMockRuntime } from '../infrastructure/outbound-mock-runtime.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 // Initialize scope registry with explicit config or empty
const scope = new ScopeRegistry(opts.scopes ?? {}) const scope = new ScopeRegistry(opts.scopes ?? {})
const cleanupManager = new CleanupManager(fastify, scope, opts.cleanup ?? false) 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 // Register user-provided plugin contracts
if (opts.pluginContracts) { if (opts.pluginContracts) {
for (const [name, spec] of Object.entries(opts.pluginContracts)) { for (const [name, spec] of Object.entries(opts.pluginContracts)) {
+21 -1
View File
@@ -105,7 +105,7 @@ export const runPetitTests = async (
config: TestConfig, config: TestConfig,
scopeRegistry?: ScopeRegistry, scopeRegistry?: ScopeRegistry,
extensionRegistry?: ExtensionRegistry, extensionRegistry?: ExtensionRegistry,
_pluginContractRegistry?: import('../domain/plugin-contracts.js').PluginContractRegistry, pluginContractRegistry?: import('../domain/plugin-contracts.js').PluginContractRegistry,
outboundContractRegistry?: OutboundContractRegistry outboundContractRegistry?: OutboundContractRegistry
): Promise<TestSuite> => { ): Promise<TestSuite> => {
const startTime = Date.now() const startTime = Date.now()
@@ -113,6 +113,26 @@ export const runPetitTests = async (
const allRoutes = discoverRoutes(fastify) const allRoutes = discoverRoutes(fastify)
const { routes, skippedRoutes } = filterPetitRoutes(allRoutes, config) 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 depth = resolveDepth(config.depth ?? 'standard')
const generationProfile = config.generationProfile ?? resolveGenerationProfile(config.depth) const generationProfile = config.generationProfile ?? resolveGenerationProfile(config.depth)
const { commands: commandGroups, cacheHits, cacheMisses } = generateCommands(routes, depth, config.seed, generationProfile) const { commands: commandGroups, cacheHits, cacheMisses } = generateCommands(routes, depth, config.seed, generationProfile)
+20
View File
@@ -102,6 +102,26 @@ export const runStatefulTests = async (
// Skip HEAD routes — auto-generated by Fastify for GET routes, no response body // 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 filteredRoutes = allRoutes.filter((r) => r.category !== 'utility' && r.method !== 'HEAD')
const routes = filterByScope(filteredRoutes, config.scope) 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) { if (routes.length === 0) {
return { return {
tests: [], tests: [],