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.
|
* 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.
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user