fix: remove destructive migrate rewrite, add replay source attribution and warning drain
This commit is contained in:
@@ -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<string, string> = {
|
||||
'x-validate-runtime': 'runtime',
|
||||
};
|
||||
export const LEGACY_ROUTE_ANNOTATIONS: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Ambiguous route patterns that require manual choice.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ function buildArtifact(
|
||||
seed: options.seed,
|
||||
replayCommand: `apophis replay --artifact ${f.artifactPath || '<artifact-path-unavailable>'}`,
|
||||
category: f.category ?? (f.observed ? classifyError(f.observed) : ErrorTaxonomy.RUNTIME),
|
||||
source: f.source,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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<VerifyRunResult
|
||||
observed: diagnostic.observed,
|
||||
formula: diagnostic.formula,
|
||||
category: diagnostic.category,
|
||||
source: route.formulaSources?.[formula],
|
||||
})
|
||||
} else {
|
||||
passedCount++
|
||||
@@ -664,6 +667,7 @@ export async function runVerify(deps: VerifyRunnerDeps): Promise<VerifyRunResult
|
||||
observed: diagnostic.observed,
|
||||
formula: diagnostic.formula,
|
||||
category: diagnostic.category,
|
||||
source: route.formulaSources?.[formula],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,8 @@ export interface FailureRecord {
|
||||
category?: string;
|
||||
diff?: string;
|
||||
actual?: string;
|
||||
/** Source of the contract: 'route' or 'plugin:name' */
|
||||
source?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user