diff --git a/src/cli/commands/migrate/rewriters/route-rewriter.ts b/src/cli/commands/migrate/rewriters/route-rewriter.ts index 30c0def..bd879c9 100644 --- a/src/cli/commands/migrate/rewriters/route-rewriter.ts +++ b/src/cli/commands/migrate/rewriters/route-rewriter.ts @@ -2,7 +2,7 @@ * Route rewriter for APOPHIS migrate command. * * Responsibilities: - * - Rewrite route schema annotations (e.g., x-validate-runtime → runtime) + * - Rewrite route schema annotations * - Preserve schema structure and formatting * - Handle annotations in Fastify route definitions * - Detect ambiguous annotations and require manual choice @@ -40,10 +40,12 @@ export interface AmbiguousRoutePattern { /** * Mapping of deprecated route schema annotations to their modern equivalents. + * x-validate-runtime is intentionally NOT here — it is the current, active annotation + * used by contract.ts and hook-validator.ts for per-route runtime validation opt-out. + * Rewriting it to 'runtime' would break it, since 'runtime' is a top-level plugin + * config option, not a valid route schema annotation. */ -export const LEGACY_ROUTE_ANNOTATIONS: Record = { - 'x-validate-runtime': 'runtime', -}; +export const LEGACY_ROUTE_ANNOTATIONS: Record = {}; /** * Ambiguous route patterns that require manual choice. diff --git a/src/cli/commands/replay/index.ts b/src/cli/commands/replay/index.ts index 57e8cc0..9600e46 100644 --- a/src/cli/commands/replay/index.ts +++ b/src/cli/commands/replay/index.ts @@ -73,33 +73,44 @@ function formatHumanOutput(result: ReplayResult, artifact: Artifact): string { w.includes('Artifact cwd no longer exists') ) + function formatFailure(label: string, failure: FailureRecord | undefined): void { + if (!failure) return + lines.push(label) + lines.push(` Route: ${failure.route}`) + if (failure.source && failure.source !== 'route') { + lines.push(` Source: ${failure.source}`) + } + if (failure.contract) { + lines.push(` Contract: ${failure.contract}`) + } + if (failure.expected) { + lines.push(` Expected: ${failure.expected}`) + } + if (failure.observed) { + lines.push(` Observed: ${failure.observed}`) + } + } + if (result.reproduced) { lines.push('Replay reproduced the original failure.') lines.push('') - lines.push('Original failure') - lines.push(` Route: ${result.originalFailure?.route}`) - lines.push(` Contract: ${result.originalFailure?.contract}`) - lines.push(` Expected: ${result.originalFailure?.expected}`) - lines.push(` Observed: ${result.originalFailure?.observed}`) + formatFailure('Original failure', result.originalFailure) lines.push(` Seed: ${artifact.seed}`) } else if (result.newFailure) { lines.push('Replay produced a different result.') lines.push('') - lines.push('Original failure') - lines.push(` Route: ${result.originalFailure?.route}`) - lines.push(` Contract: ${result.originalFailure?.contract}`) + formatFailure('Original failure', result.originalFailure) lines.push('') - lines.push('New result') - lines.push(` Route: ${result.newFailure.route}`) - lines.push(` Contract: ${result.newFailure.contract}`) - lines.push(` Expected: ${result.newFailure.expected}`) - lines.push(` Observed: ${result.newFailure.observed}`) + formatFailure('New result', result.newFailure) lines.push(` Seed: ${artifact.seed}`) } else { lines.push('Replay passed — failure no longer reproduces.') lines.push('') lines.push('Original failure') lines.push(` Route: ${result.originalFailure?.route}`) + if (result.originalFailure?.source && result.originalFailure.source !== 'route') { + lines.push(` Source: ${result.originalFailure.source}`) + } lines.push(` Contract: ${result.originalFailure?.contract}`) lines.push(` Seed: ${artifact.seed}`) } @@ -299,6 +310,14 @@ async function executeReplay( pluginContractRegistry, }) + // Drain plugin contract registry warnings (missing extensions, etc.) + if (pluginContractRegistry?.drainWarnings) { + const pcrWarnings = pluginContractRegistry.drainWarnings() + if (pcrWarnings.length > 0) { + warnings.push(...pcrWarnings) + } + } + // If no routes matched, or route found but no contracts (plugin not registered before routes), // try direct contract execution if (runResult.noRoutesMatched || runResult.noContractsFound) { @@ -403,6 +422,7 @@ async function executeReplay( observed: newFailure.observed, seed: artifact.seed || 42, replayCommand: `apophis replay --artifact ${artifactPath}`, + source: newFailure.source, }, warnings, }, artifact), @@ -416,6 +436,7 @@ async function executeReplay( observed: newFailure.observed, seed: artifact.seed || 42, replayCommand: `apophis replay --artifact ${artifactPath}`, + source: newFailure.source, }, } } diff --git a/src/cli/commands/verify/index.ts b/src/cli/commands/verify/index.ts index 3e8d753..2243ae9 100644 --- a/src/cli/commands/verify/index.ts +++ b/src/cli/commands/verify/index.ts @@ -123,6 +123,7 @@ function buildArtifact( seed: options.seed, replayCommand: `apophis replay --artifact ${f.artifactPath || ''}`, category: f.category ?? (f.observed ? classifyError(f.observed) : ErrorTaxonomy.RUNTIME), + source: f.source, } }) diff --git a/src/cli/commands/verify/runner.ts b/src/cli/commands/verify/runner.ts index 106e33a..2272a15 100644 --- a/src/cli/commands/verify/runner.ts +++ b/src/cli/commands/verify/runner.ts @@ -40,6 +40,8 @@ export interface VerifyFailure { artifactPath?: string formula?: string category?: string + /** Source of the contract: 'route' or 'plugin:name' */ + source?: string } export interface VerifyRunResult { @@ -649,6 +651,7 @@ export async function runVerify(deps: VerifyRunnerDeps): Promise { } }); // --------------------------------------------------------------------------- -// Test 12: Route rewriter detects x-validate-runtime annotation +// Test 12: x-validate-runtime is NOT a legacy annotation (it is the current, active format) // --------------------------------------------------------------------------- -test('route rewriter detects x-validate-runtime annotation', () => { +test('x-validate-runtime is current annotation, not legacy', () => { const dir = createTempDir(); try { const content = `export default { @@ -481,18 +481,7 @@ test('route rewriter detects x-validate-runtime annotation', () => { };`; writeFileSync(resolve(dir, 'test.routes.js'), content); const items = detectLegacyRouteAnnotations(content, 'test.routes.js'); - assert.strictEqual(items.length, 1, 'Should detect 1 legacy annotation'); - const firstItem = items[0]; - assert.ok(firstItem, 'Expected one migration item'); - assert.strictEqual(firstItem.legacy, 'x-validate-runtime'); - assert.strictEqual(firstItem.replacement, 'runtime'); - const result = rewriteRouteAnnotations( - resolve(dir, 'test.routes.js'), - items, - ); - assert.strictEqual(result.modified, true, 'Should modify content'); - assert.ok(result.content.includes("'runtime'"), 'Should have runtime'); - assert.ok(!result.content.includes('x-validate-runtime'), 'Should not have legacy annotation'); + assert.strictEqual(items.length, 0, 'x-validate-runtime must not be detected as legacy — it is the current annotation'); } finally { cleanup(dir); }