fix: remove destructive migrate rewrite, add replay source attribution and warning drain

This commit is contained in:
John Dvorak
2026-05-22 11:41:43 -07:00
parent 1de735ee08
commit 6e4656add5
6 changed files with 50 additions and 31 deletions
@@ -2,7 +2,7 @@
* Route rewriter for APOPHIS migrate command. * Route rewriter for APOPHIS migrate command.
* *
* Responsibilities: * Responsibilities:
* - Rewrite route schema annotations (e.g., x-validate-runtime → runtime) * - Rewrite route schema annotations
* - Preserve schema structure and formatting * - Preserve schema structure and formatting
* - Handle annotations in Fastify route definitions * - Handle annotations in Fastify route definitions
* - Detect ambiguous annotations and require manual choice * - Detect ambiguous annotations and require manual choice
@@ -40,10 +40,12 @@ export interface AmbiguousRoutePattern {
/** /**
* Mapping of deprecated route schema annotations to their modern equivalents. * 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<string, string> = { export const LEGACY_ROUTE_ANNOTATIONS: Record<string, string> = {};
'x-validate-runtime': 'runtime',
};
/** /**
* Ambiguous route patterns that require manual choice. * Ambiguous route patterns that require manual choice.
+34 -13
View File
@@ -73,33 +73,44 @@ function formatHumanOutput(result: ReplayResult, artifact: Artifact): string {
w.includes('Artifact cwd no longer exists') 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) { if (result.reproduced) {
lines.push('Replay reproduced the original failure.') lines.push('Replay reproduced the original failure.')
lines.push('') lines.push('')
lines.push('Original failure') formatFailure('Original failure', result.originalFailure)
lines.push(` Route: ${result.originalFailure?.route}`)
lines.push(` Contract: ${result.originalFailure?.contract}`)
lines.push(` Expected: ${result.originalFailure?.expected}`)
lines.push(` Observed: ${result.originalFailure?.observed}`)
lines.push(` Seed: ${artifact.seed}`) lines.push(` Seed: ${artifact.seed}`)
} else if (result.newFailure) { } else if (result.newFailure) {
lines.push('Replay produced a different result.') lines.push('Replay produced a different result.')
lines.push('') lines.push('')
lines.push('Original failure') formatFailure('Original failure', result.originalFailure)
lines.push(` Route: ${result.originalFailure?.route}`)
lines.push(` Contract: ${result.originalFailure?.contract}`)
lines.push('') lines.push('')
lines.push('New result') formatFailure('New result', result.newFailure)
lines.push(` Route: ${result.newFailure.route}`)
lines.push(` Contract: ${result.newFailure.contract}`)
lines.push(` Expected: ${result.newFailure.expected}`)
lines.push(` Observed: ${result.newFailure.observed}`)
lines.push(` Seed: ${artifact.seed}`) lines.push(` Seed: ${artifact.seed}`)
} else { } else {
lines.push('Replay passed — failure no longer reproduces.') lines.push('Replay passed — failure no longer reproduces.')
lines.push('') lines.push('')
lines.push('Original failure') lines.push('Original failure')
lines.push(` Route: ${result.originalFailure?.route}`) 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(` Contract: ${result.originalFailure?.contract}`)
lines.push(` Seed: ${artifact.seed}`) lines.push(` Seed: ${artifact.seed}`)
} }
@@ -299,6 +310,14 @@ async function executeReplay(
pluginContractRegistry, 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), // If no routes matched, or route found but no contracts (plugin not registered before routes),
// try direct contract execution // try direct contract execution
if (runResult.noRoutesMatched || runResult.noContractsFound) { if (runResult.noRoutesMatched || runResult.noContractsFound) {
@@ -403,6 +422,7 @@ async function executeReplay(
observed: newFailure.observed, observed: newFailure.observed,
seed: artifact.seed || 42, seed: artifact.seed || 42,
replayCommand: `apophis replay --artifact ${artifactPath}`, replayCommand: `apophis replay --artifact ${artifactPath}`,
source: newFailure.source,
}, },
warnings, warnings,
}, artifact), }, artifact),
@@ -416,6 +436,7 @@ async function executeReplay(
observed: newFailure.observed, observed: newFailure.observed,
seed: artifact.seed || 42, seed: artifact.seed || 42,
replayCommand: `apophis replay --artifact ${artifactPath}`, replayCommand: `apophis replay --artifact ${artifactPath}`,
source: newFailure.source,
}, },
} }
} }
+1
View File
@@ -123,6 +123,7 @@ function buildArtifact(
seed: options.seed, seed: options.seed,
replayCommand: `apophis replay --artifact ${f.artifactPath || '<artifact-path-unavailable>'}`, replayCommand: `apophis replay --artifact ${f.artifactPath || '<artifact-path-unavailable>'}`,
category: f.category ?? (f.observed ? classifyError(f.observed) : ErrorTaxonomy.RUNTIME), category: f.category ?? (f.observed ? classifyError(f.observed) : ErrorTaxonomy.RUNTIME),
source: f.source,
} }
}) })
+4
View File
@@ -40,6 +40,8 @@ export interface VerifyFailure {
artifactPath?: string artifactPath?: string
formula?: string formula?: string
category?: string category?: string
/** Source of the contract: 'route' or 'plugin:name' */
source?: string
} }
export interface VerifyRunResult { export interface VerifyRunResult {
@@ -649,6 +651,7 @@ export async function runVerify(deps: VerifyRunnerDeps): Promise<VerifyRunResult
observed: diagnostic.observed, observed: diagnostic.observed,
formula: diagnostic.formula, formula: diagnostic.formula,
category: diagnostic.category, category: diagnostic.category,
source: route.formulaSources?.[formula],
}) })
} else { } else {
passedCount++ passedCount++
@@ -664,6 +667,7 @@ export async function runVerify(deps: VerifyRunnerDeps): Promise<VerifyRunResult
observed: diagnostic.observed, observed: diagnostic.observed,
formula: diagnostic.formula, formula: diagnostic.formula,
category: diagnostic.category, category: diagnostic.category,
source: route.formulaSources?.[formula],
}) })
} }
} }
+2
View File
@@ -195,6 +195,8 @@ export interface FailureRecord {
category?: string; category?: string;
diff?: string; diff?: string;
actual?: string; actual?: string;
/** Source of the contract: 'route' or 'plugin:name' */
source?: string;
} }
/** /**
+3 -14
View File
@@ -469,9 +469,9 @@ test('config rewriter replaces legacy fields', () => {
} }
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 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(); const dir = createTempDir();
try { try {
const content = `export default { const content = `export default {
@@ -481,18 +481,7 @@ test('route rewriter detects x-validate-runtime annotation', () => {
};`; };`;
writeFileSync(resolve(dir, 'test.routes.js'), content); writeFileSync(resolve(dir, 'test.routes.js'), content);
const items = detectLegacyRouteAnnotations(content, 'test.routes.js'); const items = detectLegacyRouteAnnotations(content, 'test.routes.js');
assert.strictEqual(items.length, 1, 'Should detect 1 legacy annotation'); assert.strictEqual(items.length, 0, 'x-validate-runtime must not be detected as legacy — it is the current 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');
} finally { } finally {
cleanup(dir); cleanup(dir);
} }