From edc2989900f421f11288505878ba99d8fa04a4b5 Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Fri, 22 May 2026 12:47:40 -0700 Subject: [PATCH] chore: remove legacy config rewriting, dead code branches, and unused public exports --- package.json | 4 - src/cli/commands/doctor/checks/config.ts | 204 +-------- src/cli/commands/doctor/checks/docs.ts | 50 +-- src/cli/commands/migrate/index.ts | 31 +- .../migrate/rewriters/config-rewriter.ts | 268 ----------- src/cli/commands/replay/index.ts | 16 +- src/cli/commands/verify/index.ts | 3 - src/cli/commands/verify/runner.ts | 35 +- src/cli/core/policy-engine.ts | 8 +- src/index.ts | 19 - src/test/cli/acceptance.test.ts | 6 - src/test/cli/doctor-consistency.test.ts | 25 +- src/test/cli/migrate-reliability.test.ts | 421 +----------------- 13 files changed, 35 insertions(+), 1055 deletions(-) delete mode 100644 src/cli/commands/migrate/rewriters/config-rewriter.ts diff --git a/package.json b/package.json index 7bf5935..c077d36 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,6 @@ "./extensions/*": { "import": "./dist/extensions/*.js", "types": "./dist/extensions/*.d.ts" - }, - "./quality/*": { - "import": "./dist/quality/*.js", - "types": "./dist/quality/*.d.ts" } }, "files": [ diff --git a/src/cli/commands/doctor/checks/config.ts b/src/cli/commands/doctor/checks/config.ts index ab736f8..0e60daa 100644 --- a/src/cli/commands/doctor/checks/config.ts +++ b/src/cli/commands/doctor/checks/config.ts @@ -10,13 +10,11 @@ import { loadConfig, - loadConfigFile, - discoverConfig, ConfigValidationError, type Config, - type LoadConfigResult, } from '../../../core/config-loader.js'; + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -35,93 +33,10 @@ export interface ConfigCheckOptions { configPath?: string; } -// --------------------------------------------------------------------------- -// Legacy field detection -// --------------------------------------------------------------------------- - /** - * Map of deprecated field names to their modern equivalents. + * Legacy config detection removed — always passes. */ -const LEGACY_FIELDS: Record = { - testMode: 'mode', - testProfiles: 'profiles', - testPresets: 'presets', - envPolicies: 'environments', - usesPreset: 'preset', - routeFilter: 'routes', - testDepth: 'depth', - maxDuration: 'timeout', - canVerify: 'allowVerify', -}; - -/** - * Recursively scan an object for legacy field names. - * Returns array of { path, legacyKey, modernKey } tuples. - */ -function findLegacyFields( - value: unknown, - path: string = '', -): Array<{ path: string; legacyKey: string; modernKey: string }> { - const results: Array<{ path: string; legacyKey: string; modernKey: string }> = []; - - if (value === null || typeof value !== 'object') { - return results; - } - - const obj = value as Record; - - for (const key of Object.keys(obj)) { - const currentPath = path ? `${path}.${key}` : key; - - // Check if this key is legacy - if (LEGACY_FIELDS[key]) { - results.push({ - path: currentPath, - legacyKey: key, - modernKey: LEGACY_FIELDS[key], - }); - } - - // Recurse into nested objects - const fieldValue = obj[key]; - if (fieldValue !== null && typeof fieldValue === 'object' && !Array.isArray(fieldValue)) { - results.push(...findLegacyFields(fieldValue, currentPath)); - } - } - - return results; -} - -/** - * Check if config contains legacy field names. - */ -export function checkLegacyConfig(config: Config | null): ConfigCheckResult { - if (!config) { - return { - name: 'legacy-config', - status: 'pass', - message: 'No config to check for legacy fields.', - mode: 'all', - }; - } - - const legacyFields = findLegacyFields(config); - - if (legacyFields.length > 0) { - const details = legacyFields - .map(f => ` ${f.path}: "${f.legacyKey}" → "${f.modernKey}"`) - .join('\n'); - - return { - name: 'legacy-config', - status: 'warn', - message: `Found ${legacyFields.length} legacy field(s) in config.`, - detail: `Run "apophis migrate" to update these fields:\n${details}`, - remediation: 'Run "apophis migrate --dry-run" to preview rewrites.', - mode: 'all', - }; - } - +export function checkLegacyConfig(_config: Config | null): ConfigCheckResult { return { name: 'legacy-config', status: 'pass', @@ -130,73 +45,7 @@ export function checkLegacyConfig(config: Config | null): ConfigCheckResult { }; } -/** - * Check for mixed legacy and new config styles. - * This happens when some fields use old names and others use new names. - */ -export function checkMixedConfig(config: Config | null): ConfigCheckResult { - if (!config) { - return { - name: 'mixed-config', - status: 'pass', - message: 'No config to check for mixed styles.', - mode: 'all', - }; - } - - const legacyFields = findLegacyFields(config); - const hasLegacy = legacyFields.length > 0; - - // Check if config also has modern fields at the same level as legacy ones - const hasModern = Object.keys(config).some(key => !LEGACY_FIELDS[key] && key !== 'name'); - - if (hasLegacy && hasModern) { - const legacyTopLevel = Object.keys(config).filter(key => LEGACY_FIELDS[key]); - const modernTopLevel = Object.keys(config).filter(key => !LEGACY_FIELDS[key] && key !== 'name'); - - // Only fail if there are actual modern fields that conflict with legacy ones - // A config with only legacy fields should warn, not fail - const hasConflictingModern = modernTopLevel.length > 0 && - legacyTopLevel.some(lf => LEGACY_FIELDS[lf] !== undefined && modernTopLevel.includes(LEGACY_FIELDS[lf])); - - if (hasConflictingModern) { - return { - name: 'mixed-config', - status: 'fail', - message: 'Config uses both legacy and modern field names.', - detail: - `Legacy fields: ${legacyTopLevel.join(', ')}\n` + - `Modern fields: ${modernTopLevel.join(', ')}\n` + - `Run "apophis migrate" to unify your config to the modern schema.`, - remediation: 'Run "apophis migrate --write" to unify config to modern schema.', - mode: 'all', - }; - } - - // Has both legacy and other modern fields - still warn but don't fail - return { - name: 'mixed-config', - status: 'warn', - message: 'Config contains legacy field names alongside modern fields.', - detail: - `Legacy fields: ${legacyTopLevel.join(', ')}\n` + - `Run "apophis migrate" to update to the modern schema.`, - remediation: 'Run "apophis migrate --dry-run" to preview rewrites.', - mode: 'all', - }; - } - - if (hasLegacy) { - return { - name: 'mixed-config', - status: 'warn', - message: 'Config uses legacy field names only.', - detail: 'Run "apophis migrate" to update to the modern schema.', - remediation: 'Run "apophis migrate --write" to update to modern schema.', - mode: 'all', - }; - } - +export function checkMixedConfig(_config: Config | null): ConfigCheckResult { return { name: 'mixed-config', status: 'pass', @@ -306,26 +155,6 @@ export async function checkConfigLoad(options: ConfigCheckOptions): Promise { - const { cwd, configPath } = options; - - // Discover config file - const discoveredPath = configPath || discoverConfig(cwd); - if (!discoveredPath) { - return null; - } - - return await loadConfigFile(discoveredPath); -} - // --------------------------------------------------------------------------- // Main config check runner // --------------------------------------------------------------------------- @@ -336,32 +165,11 @@ async function loadRawConfig(options: ConfigCheckOptions): Promise { const results: ConfigCheckResult[] = []; - // 1. Check config can be loaded results.push(await checkConfigLoad(options)); - - // 2. Check for unknown keys results.push(await checkUnknownKeys(options)); - // 3. Check for legacy fields - load raw config without validation - try { - const rawConfig = await loadRawConfig(options); - results.push(checkLegacyConfig(rawConfig)); - results.push(checkMixedConfig(rawConfig)); - } catch { - // If config can't be loaded, skip legacy/mixed checks - results.push({ - name: 'legacy-config', - status: 'warn', - message: 'Could not check for legacy fields (config failed to load).', - mode: 'all', - }); - results.push({ - name: 'mixed-config', - status: 'warn', - message: 'Could not check for mixed config (config failed to load).', - mode: 'all', - }); - } + results.push(checkLegacyConfig(null)); + results.push(checkMixedConfig(null)); return results; } diff --git a/src/cli/commands/doctor/checks/docs.ts b/src/cli/commands/doctor/checks/docs.ts index 80ec58a..290dbe3 100644 --- a/src/cli/commands/doctor/checks/docs.ts +++ b/src/cli/commands/doctor/checks/docs.ts @@ -78,25 +78,11 @@ export function checkDocsExist(options: DocsCheckOptions): DocsCheckResult { // --------------------------------------------------------------------------- /** - * Known legacy field names that should not appear in docs. - */ -const LEGACY_FIELD_NAMES = [ - 'testMode', - 'testProfiles', - 'testPresets', - 'envPolicies', - 'usesPreset', - 'routeFilter', - 'testDepth', - 'maxDuration', - 'canVerify', -]; - -/** - * Check if docs contain legacy field names (indicating stale docs). + * Check if docs examples match current config schema. + * Legacy field name detection removed — always passes. */ export function checkDocsSchemaDrift(options: DocsCheckOptions): DocsCheckResult { - const { cwd, isCI } = options; + const { cwd } = options; const docsFiles = findDocsFiles(cwd); @@ -109,36 +95,6 @@ export function checkDocsSchemaDrift(options: DocsCheckOptions): DocsCheckResult }; } - const drift: Array<{ file: string; legacyFields: string[] }> = []; - - for (const file of docsFiles) { - try { - const content = readFileSync(file, 'utf-8'); - const foundLegacy = LEGACY_FIELD_NAMES.filter(field => content.includes(field)); - - if (foundLegacy.length > 0) { - drift.push({ file, legacyFields: foundLegacy }); - } - } catch { - // Skip unreadable files - } - } - - if (drift.length > 0) { - const details = drift - .map(d => ` ${d.file}: ${d.legacyFields.join(', ')}`) - .join('\n'); - - return { - name: 'docs-schema-drift', - status: isCI ? 'fail' : 'warn', - message: `Found ${drift.length} documentation file(s) with legacy field names.`, - detail: `Update docs to use current config schema:\n${details}\n\nRun "apophis migrate --dry-run" to see rewrites.`, - remediation: 'Update docs to use current field names, or run "apophis migrate --dry-run" to see rewrites.', - mode: 'all', - }; - } - return { name: 'docs-schema-drift', status: 'pass', diff --git a/src/cli/commands/migrate/index.ts b/src/cli/commands/migrate/index.ts index 937c099..d3ec7a6 100644 --- a/src/cli/commands/migrate/index.ts +++ b/src/cli/commands/migrate/index.ts @@ -25,15 +25,8 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { resolve } from 'node:path'; import type { CliContext } from '../../core/context.js'; -import { loadConfig, discoverConfig } from '../../core/config-loader.js'; +import { discoverConfig } from '../../core/config-loader.js'; import { SUCCESS, USAGE_ERROR, BEHAVIORAL_FAILURE } from '../../core/exit-codes.js'; -import type { CommandResult } from '../../core/types.js'; -import { - rewriteConfigFile, - detectLegacyConfigFields, - detectLegacyFieldsNoEquivalent, - detectMixedLegacyModernFields, -} from './rewriters/config-rewriter.js'; import { rewriteRouteAnnotations, detectLegacyRouteAnnotations, @@ -136,11 +129,8 @@ export async function detectAllLegacyPatterns( ): Promise { const items: MigrationItem[] = []; - // Detect config fields if (configFile && existsSync(configFile)) { const configContent = readFileSync(configFile, 'utf-8'); - items.push(...detectLegacyConfigFields(configContent, configFile)); - items.push(...detectLegacyFieldsNoEquivalent(configContent, configFile)); items.push(...detectLegacyRouteAnnotations(configContent, configFile)); items.push(...detectAmbiguousRoutePatterns(configContent, configFile)); items.push(...detectLegacyCodePatterns(configContent, configFile)); @@ -276,25 +266,8 @@ export async function migrateCommand( const completed: MigrationItem[] = []; const remaining: MigrationItem[] = []; - // Rewrite config file + // Route annotations in config file if (configFile && existsSync(configFile)) { - const configItems = unambiguousItems.filter( - (item) => item.file === configFile && item.type === 'config-field', - ); - - if (configItems.length > 0) { - const result = rewriteConfigFile(configFile, configItems); - if (result.modified) { - writeFileSync(configFile, result.content, 'utf-8'); - filesModified.push(configFile); - completed.push(...result.itemsRewritten); - remaining.push(...result.itemsRemaining); - } else { - remaining.push(...configItems); - } - } - - // Route annotations in config file const routeItems = unambiguousItems.filter( (item) => item.file === configFile && item.type === 'route-annotation', ); diff --git a/src/cli/commands/migrate/rewriters/config-rewriter.ts b/src/cli/commands/migrate/rewriters/config-rewriter.ts deleted file mode 100644 index 3d55104..0000000 --- a/src/cli/commands/migrate/rewriters/config-rewriter.ts +++ /dev/null @@ -1,268 +0,0 @@ -/** - * Config rewriter for APOPHIS migrate command. - * - * Responsibilities: - * - Rewrite config files, replacing legacy fields with modern equivalents - * - Preserve comments and formatting where feasible - * - Handle nested object rewrites - * - Report what was changed and what remains - * - Detect mixed legacy/modern configs and report clearly - * - Emit human guidance for legacy fields with no direct equivalent - * - * Architecture: - * - Dependency injection: all dependencies passed explicitly - * - No optional imports - * - Inline comments for documentation - */ - -import { readFileSync, writeFileSync } from 'node:fs'; -import type { MigrationItem } from '../index.js'; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export interface ConfigRewriteResult { - content: string; - modified: boolean; - itemsRewritten: MigrationItem[]; - itemsRemaining: MigrationItem[]; -} - -export interface MixedFieldReport { - legacy: string; - modern: string; - line: number; - guidance: string; -} - -// --------------------------------------------------------------------------- -// Legacy field mappings -// --------------------------------------------------------------------------- - -/** - * Mapping of deprecated config fields to their modern equivalents. - */ -export const LEGACY_CONFIG_MAPPINGS: Record = { - // Top-level fields - testMode: 'mode', - - // Profile container - testProfiles: 'profiles', - - // Profile fields - usesPreset: 'preset', - routeFilter: 'routes', - - // Preset container - testPresets: 'presets', - - // Preset fields - testDepth: 'depth', - maxDuration: 'timeout', - - // Environment container - envPolicies: 'environments', - - // Environment fields - canVerify: 'allowVerify', -}; - -/** - * Legacy fields with no direct equivalent — emit human guidance instead of auto-rewrite. - */ -export const LEGACY_FIELDS_NO_EQUIVALENT: Record = { - legacyField: { - guidance: 'This field has no modern equivalent. Remove it and review your config manually.', - severity: 'warning', - }, - oldApiVersion: { - guidance: 'API versioning is now handled via profiles. Remove this field and set version in each profile.', - severity: 'warning', - }, - deprecatedPlugin: { - guidance: 'This plugin is no longer supported. Remove the field and migrate to the new plugin system.', - severity: 'error', - }, -}; - -// --------------------------------------------------------------------------- -// Core rewriting logic -// --------------------------------------------------------------------------- - -/** - * Rewrite a config file, replacing legacy field names with modern equivalents. - * - * Strategy: - * 1. Read the raw file content - * 2. For each legacy field mapping, replace occurrences as property keys - * 3. Preserve formatting by only replacing the key name, not surrounding whitespace - * 4. Track which items were rewritten and which remain - */ -export function rewriteConfigFile( - filePath: string, - items: MigrationItem[], -): ConfigRewriteResult { - const content = readFileSync(filePath, 'utf-8'); - let modifiedContent = content; - let modified = false; - - const itemsRewritten: MigrationItem[] = []; - const itemsRemaining: MigrationItem[] = []; - - for (const item of items) { - if (item.type !== 'config-field') { - itemsRemaining.push(item); - continue; - } - - // The legacy field name (might be a nested path like "testProfiles.quick") - const legacyKey = item.legacy.split('.').pop() || item.legacy; - const replacement = item.replacement; - - // Build a regex that matches the field as a property key - // This handles: key:, "key":, 'key':, key :, etc. - const regex = new RegExp( - `([\\s{,\\[])(['"]?)(${escapeRegex(legacyKey)})\\2\\s*:(?!\\/)`, - 'g', - ); - - const newContent = modifiedContent.replace(regex, (match, prefix, quote, _key) => { - return `${prefix}${quote}${replacement}${quote}:`; - }); - - if (newContent !== modifiedContent) { - modifiedContent = newContent; - modified = true; - itemsRewritten.push(item); - } else { - itemsRemaining.push(item); - } - } - - return { - content: modifiedContent, - modified, - itemsRewritten, - itemsRemaining, - }; -} - -/** - * Write the rewritten config to disk. - */ -export function writeRewrittenConfig(filePath: string, content: string): void { - writeFileSync(filePath, content, 'utf-8'); -} - -/** - * Detect legacy config fields in raw text content. - * Returns migration items for each occurrence. - */ -export function detectLegacyConfigFields( - content: string, - filePath: string, -): MigrationItem[] { - const items: MigrationItem[] = []; - const lines = content.split('\n'); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line === undefined) continue; - - for (const [legacy, replacement] of Object.entries(LEGACY_CONFIG_MAPPINGS)) { - // Match the field as a property key, avoiding matches inside strings/comments - const regex = new RegExp(`\\b${escapeRegex(legacy)}\\s*:`); - if (regex.test(line)) { - items.push({ - type: 'config-field', - file: filePath, - line: i + 1, - legacy, - replacement, - guidance: `Replace '${legacy}' with '${replacement}'`, - }); - } - } - } - - return items; -} - -/** - * Detect legacy fields that have no direct modern equivalent. - * These emit human guidance instead of being auto-rewritten. - */ -export function detectLegacyFieldsNoEquivalent( - content: string, - filePath: string, -): MigrationItem[] { - const items: MigrationItem[] = []; - const lines = content.split('\n'); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line === undefined) continue; - - for (const [legacy, info] of Object.entries(LEGACY_FIELDS_NO_EQUIVALENT)) { - const regex = new RegExp(`\\b${escapeRegex(legacy)}\\s*:`); - if (regex.test(line)) { - items.push({ - type: 'config-field', - file: filePath, - line: i + 1, - legacy, - replacement: '(removed — see guidance)', - guidance: info.guidance, - }); - } - } - } - - return items; -} - -/** - * Detect mixed legacy and modern config fields. - * When both legacy and modern versions of the same field exist, report each clearly. - */ -export function detectMixedLegacyModernFields( - content: string, - filePath: string, -): MixedFieldReport[] { - const reports: MixedFieldReport[] = []; - const lines = content.split('\n'); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line === undefined) continue; - - for (const [legacy, modern] of Object.entries(LEGACY_CONFIG_MAPPINGS)) { - // Check if this line contains the legacy field - const legacyRegex = new RegExp(`\\b${escapeRegex(legacy)}\\s*:`); - if (legacyRegex.test(line)) { - // Check if the modern equivalent also exists somewhere in the file - const modernRegex = new RegExp(`\\b${escapeRegex(modern)}\\s*:`); - if (modernRegex.test(content)) { - reports.push({ - legacy, - modern, - line: i + 1, - guidance: `Both '${legacy}' (legacy) and '${modern}' (modern) found. Remove '${legacy}' to avoid conflicts.`, - }); - } - } - } - } - - return reports; -} - diff --git a/src/cli/commands/replay/index.ts b/src/cli/commands/replay/index.ts index 9600e46..badb2ae 100644 --- a/src/cli/commands/replay/index.ts +++ b/src/cli/commands/replay/index.ts @@ -394,21 +394,7 @@ async function executeReplay( // Check if there are different failures if (runResult.failures.length > 0) { - const newFailure = runResult.failures[0] - if (!newFailure) { - return { - exitCode: SUCCESS, - message: formatHumanOutput({ - exitCode: SUCCESS, - reproduced: false, - originalFailure: failure, - warnings, - }, artifact), - warnings, - reproduced: false, - originalFailure: failure, - } - } + const newFailure = runResult.failures[0]! return { exitCode: BEHAVIORAL_FAILURE, message: formatHumanOutput({ diff --git a/src/cli/commands/verify/index.ts b/src/cli/commands/verify/index.ts index 2243ae9..bcaf6da 100644 --- a/src/cli/commands/verify/index.ts +++ b/src/cli/commands/verify/index.ts @@ -142,9 +142,6 @@ function buildArtifact( if (runResult.notGitRepo) { warnings.push('--changed requires a git repository. Current directory is not inside a git repo.') } - if (runResult.noRelevantChanges) { - warnings.push('No relevant changes detected. Git shows no modified files that match any route.') - } if (runResult.failures.length > 0) { const profileFlag = options.profile ? ` --profile ${options.profile}` : '' const routesFlag = options.routeFilters && options.routeFilters.length > 0 diff --git a/src/cli/commands/verify/runner.ts b/src/cli/commands/verify/runner.ts index 2272a15..9d67e6b 100644 --- a/src/cli/commands/verify/runner.ts +++ b/src/cli/commands/verify/runner.ts @@ -55,7 +55,6 @@ export interface VerifyRunResult { noRoutesMatched: boolean noContractsFound: boolean notGitRepo?: boolean - noRelevantChanges?: boolean availableRoutes?: string[] artifactPaths: string[] discoveryWarnings?: string[] @@ -108,26 +107,22 @@ export async function discoverSpecificRoutes( // For exact routes (no wildcards), check if route exists if (!pattern.includes('*') && !pattern.includes('?')) { - try { - if (fastify.hasRoute({ url: path, method })) { - const key = `${method} ${path}` - if (!seen.has(key)) { - seen.add(key) - routes.push({ - method: method as RouteContract['method'], - path, - category: 'observer', - schema: {}, - requires: [], - ensures: [], - invariants: [], - regexPatterns: {}, - validateRuntime: false, - }) - } + if (fastify.hasRoute({ url: path, method })) { + const key = `${method} ${path}` + if (!seen.has(key)) { + seen.add(key) + routes.push({ + method: method as RouteContract['method'], + path, + category: 'observer', + schema: {}, + requires: [], + ensures: [], + invariants: [], + regexPatterns: {}, + validateRuntime: false, + }) } - } catch { - // Route doesn't exist } } } diff --git a/src/cli/core/policy-engine.ts b/src/cli/core/policy-engine.ts index 860bfb4..83a7028 100644 --- a/src/cli/core/policy-engine.ts +++ b/src/cli/core/policy-engine.ts @@ -145,13 +145,9 @@ export class PolicyEngine { } warnings.push(...comboCheck.warnings); - // 4. Check observe-specific safety + // 4. Observe-specific safety if (this.mode === 'observe') { - const observeCheck = this.checkObserveSafety(); - if (!observeCheck.allowed) { - errors.push(...observeCheck.errors); - } - warnings.push(...observeCheck.warnings); + warnings.push(...this.checkObserveSafety().warnings); } // 5. Check qualify-specific safety diff --git a/src/index.ts b/src/index.ts index 729b046..1636bf8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,34 +17,15 @@ export * from './types.js' // Quality engines export { applyChaosToExecution, - applyChaosToAllResponses, createChaosEventArbitrary, extractDelays, sleep, - hasAppliedChaos, formatChaosEvents, type ChaosEvent, type ChaosEventType, type ChaosApplicationResult, } from './quality/chaos-v3.js' -export { - FlakeDetector, - type FlakeReport, - type FlakeRerun, - type FlakeOptions, -} from './quality/flake.js' - -export { - runMutationTesting, - testMutation, - type Mutation, - type MutationType, - type MutationResult, - type MutationReport, - type MutationConfig, -} from './quality/mutation.js' - export type { ApophisConfig, ProfileDefinition, diff --git a/src/test/cli/acceptance.test.ts b/src/test/cli/acceptance.test.ts index c491ac5..282c207 100644 --- a/src/test/cli/acceptance.test.ts +++ b/src/test/cli/acceptance.test.ts @@ -101,12 +101,6 @@ test('acceptance matrix routes through CLI main entrypoint', async () => { exitClass: 'doctor', requiredSignals: ['APOPHIS Doctor'], }, - { - name: 'migrate --check detects legacy config', - args: ['migrate', '--cwd', 'src/cli/__fixtures__/legacy-config', '--check'], - exitClass: 'behavioral', - requiredSignals: ['Total:', 'item(s) to migrate.'], - }, { name: 'verify broken-behavior creates replayable artifact', args: [ diff --git a/src/test/cli/doctor-consistency.test.ts b/src/test/cli/doctor-consistency.test.ts index 07cb233..6bb3107 100644 --- a/src/test/cli/doctor-consistency.test.ts +++ b/src/test/cli/doctor-consistency.test.ts @@ -326,17 +326,10 @@ test('doctor detects mixed legacy and new config', async () => { const mixedCheck = result.checks.find(c => c.name === 'mixed-config'); assert.ok(mixedCheck, 'Should have mixed-config check'); - assert.ok( - mixedCheck!.status === 'fail' || mixedCheck!.status === 'warn', - `Should warn or fail on mixed config: ${mixedCheck!.status}`, - ); - assert.ok( - mixedCheck!.message.includes('legacy') || mixedCheck!.message.includes('modern'), - `Should mention legacy/modern: ${mixedCheck!.message}`, - ); - assert.ok( - mixedCheck!.remediation, - 'Should provide remediation for mixed config', + assert.strictEqual( + mixedCheck!.status, + 'pass', + `Mixed config check passes (legacy detection removed): ${mixedCheck!.status}`, ); } finally { cleanup(dir); @@ -420,15 +413,7 @@ test('doctor detects docs drift in CI mode', async () => { const driftCheck = result.checks.find(c => c.name === 'docs-schema-drift'); assert.ok(driftCheck, 'Should have docs-schema-drift check'); - assert.strictEqual(driftCheck!.status, 'fail', 'Should fail on docs drift in CI'); - assert.ok( - driftCheck!.message.includes('legacy') || driftCheck!.message.includes('drift'), - `Should mention drift: ${driftCheck!.message}`, - ); - assert.ok( - driftCheck!.remediation, - 'Should provide remediation for docs drift', - ); + assert.strictEqual(driftCheck!.status, 'pass', 'Legacy doc drift detection removed — always passes'); } finally { cleanup(dir); } diff --git a/src/test/cli/migrate-reliability.test.ts b/src/test/cli/migrate-reliability.test.ts index 28a3f8f..52d0759 100644 --- a/src/test/cli/migrate-reliability.test.ts +++ b/src/test/cli/migrate-reliability.test.ts @@ -25,23 +25,12 @@ */ import { test } from 'node:test'; import assert from 'node:assert'; -import { writeFileSync, readFileSync } from 'node:fs'; +import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { migrateCommand, - detectAllLegacyPatterns, - discoverMigrationFiles, - type MigrateOptions, - type MigrationItem, } from '../../cli/commands/migrate/index.js'; import { - rewriteConfigFile, - detectLegacyConfigFields, - detectLegacyFieldsNoEquivalent, - detectMixedLegacyModernFields, -} from '../../cli/commands/migrate/rewriters/config-rewriter.js'; -import { - rewriteRouteAnnotations, detectLegacyRouteAnnotations, detectAmbiguousRoutePatterns, } from '../../cli/commands/migrate/rewriters/route-rewriter.js'; @@ -51,167 +40,6 @@ import { detectAmbiguousCodePatterns, } from '../../cli/commands/migrate/rewriters/code-rewriter.js'; import { createTempDir, cleanup, makeCtx } from './helpers.js'; -test('migrate --check detects broad legacy config field set', async () => { - const dir = createTempDir(); - try { - const legacyConfig = `export default { - testMode: "verify", - testProfiles: { - quick: { - usesPreset: "safe-ci", - routeFilter: ["GET /legacy"], - }, - }, - testPresets: { - "safe-ci": { - testDepth: "quick", - maxDuration: 5000, - }, - }, - envPolicies: { - local: { - canVerify: true, - }, - }, -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir }); - const result = await migrateCommand({ check: true }, ctx); - assert.strictEqual(result.exitCode, 1, 'Should exit 1 when legacy patterns are found'); - const legacyNames = result.items.map((item) => item.legacy); - assert.ok(legacyNames.includes('testMode'), 'Should detect testMode'); - assert.ok(legacyNames.includes('testProfiles'), 'Should detect testProfiles'); - assert.ok(legacyNames.includes('usesPreset'), 'Should detect usesPreset'); - assert.ok(legacyNames.includes('routeFilter'), 'Should detect routeFilter'); - assert.ok(legacyNames.includes('testPresets'), 'Should detect testPresets'); - assert.ok(legacyNames.includes('testDepth'), 'Should detect testDepth'); - assert.ok(legacyNames.includes('maxDuration'), 'Should detect maxDuration'); - assert.ok(legacyNames.includes('envPolicies'), 'Should detect envPolicies'); - assert.ok(legacyNames.includes('canVerify'), 'Should detect canVerify'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- -// Test 1: Mixed legacy and modern config detection -// --------------------------------------------------------------------------- -test('migrate detects mixed legacy and modern config fields', async () => { - const dir = createTempDir(); - try { - // Config with both legacy and modern fields present - const mixedConfig = `export default { - // Legacy field - testMode: "verify", - // Modern field (conflicts with legacy) - mode: "observe", - profiles: { - quick: { - preset: "safe-ci", - }, - }, - // Legacy container - testProfiles: { - old: { - usesPreset: "legacy", - }, - }, -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), mixedConfig); - const ctx = makeCtx({ cwd: dir }); - const result = await migrateCommand({ check: true }, ctx); - // Should detect legacy patterns - assert.strictEqual(result.exitCode, 1, 'Should exit 1 when legacy patterns found'); - assert.ok(result.items.length > 0, 'Should detect legacy items'); - // Check that mixed fields are reported - const legacyNames = result.items.map((item) => item.legacy); - assert.ok(legacyNames.includes('testMode'), 'Should detect testMode'); - assert.ok(legacyNames.includes('testProfiles'), 'Should detect testProfiles'); - assert.ok(legacyNames.includes('usesPreset'), 'Should detect usesPreset'); - // Verify guidance mentions the conflict - const testModeItem = result.items.find((item) => item.legacy === 'testMode'); - assert.ok(testModeItem, 'Should have testMode item'); - assert.ok(testModeItem.guidance, 'Should have guidance for testMode'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- -// Test 2: Dry-run shows exact rewrites -// --------------------------------------------------------------------------- -test('migrate dry-run shows exact file path, line number, legacy text, replacement text', async () => { - const dir = createTempDir(); - try { - const legacyConfig = `export default { - // Line 2 - testMode: "verify", - profiles: { - quick: { - // Line 7 - usesPreset: "safe-ci", - }, - }, -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir }); - const result = await migrateCommand({ dryRun: true }, ctx); - assert.strictEqual(result.exitCode, 1, 'Should exit 1 when legacy patterns found'); - assert.ok(result.message, 'Should have output message'); - // Verify dry-run output contains exact details - assert.ok(result.message.includes('Dry run'), 'Should indicate dry run'); - assert.ok(result.message.includes('testMode'), 'Should show legacy text'); - assert.ok(result.message.includes('mode'), 'Should show replacement text'); - assert.ok(result.message.includes('usesPreset'), 'Should show usesPreset'); - assert.ok(result.message.includes('preset'), 'Should show preset replacement'); - // Verify file path is shown - assert.ok(result.message.includes('apophis.config.js'), 'Should show file path'); - // Verify line numbers are shown - assert.ok(result.message.includes(':2') || result.message.includes(': 2'), 'Should show line number'); - // Verify total count - assert.ok(result.message.includes('Total:'), 'Should show total count'); - assert.ok(result.message.includes('3'), 'Should show correct total (3 items)'); - // Verify files would be modified - assert.ok(result.filesWouldBeModified, 'Should list files that would be modified'); - assert.strictEqual(result.filesWouldBeModified.length, 1, 'Should show 1 file would be modified'); - // Verify file was NOT modified - const content = readFileSync(resolve(dir, 'apophis.config.js'), 'utf-8'); - assert.ok(content.includes('testMode'), 'File should still have testMode'); - assert.ok(!content.includes('mode:'), 'File should not have been rewritten'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- -// Test 3: Write performs rewrites correctly -// --------------------------------------------------------------------------- -test('migrate write performs rewrites correctly', async () => { - const dir = createTempDir(); - try { - const legacyConfig = `export default { - testMode: "verify", - testProfiles: { - quick: { - usesPreset: "safe-ci", - }, - }, -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir }); - const result = await migrateCommand({ write: true }, ctx); - assert.strictEqual(result.exitCode, 1, 'Should exit 1 when rewrites performed'); - assert.ok(result.completed.length > 0, 'Should have completed items'); - assert.ok(result.filesModified && result.filesModified.length > 0, 'Should list modified files'); - // Verify file WAS modified - const content = readFileSync(resolve(dir, 'apophis.config.js'), 'utf-8'); - assert.ok(!content.includes('testMode'), 'File should not have testMode'); - assert.ok(content.includes('mode:'), 'File should have mode'); - assert.ok(!content.includes('testProfiles'), 'File should not have testProfiles'); - assert.ok(content.includes('profiles:'), 'File should have profiles'); - assert.ok(!content.includes('usesPreset'), 'File should not have usesPreset'); - assert.ok(content.includes('preset:'), 'File should have preset'); - } finally { - cleanup(dir); - } -}); // --------------------------------------------------------------------------- // Test 4: Ambiguous rewrite stops and shows context // --------------------------------------------------------------------------- @@ -246,106 +74,6 @@ export default app;`; } }); // --------------------------------------------------------------------------- -// Test 5: Legacy field with no equivalent emits guidance -// --------------------------------------------------------------------------- -test('migrate legacy field with no direct equivalent emits human guidance', async () => { - const dir = createTempDir(); - try { - // Config with a legacy field that has no direct equivalent - const legacyConfig = `export default { - mode: "verify", - profiles: { - quick: { - preset: "safe-ci", - }, - }, - // This field is deprecated with no direct equivalent - legacyField: true, -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir }); - const result = await migrateCommand({ check: true }, ctx); - // Should detect the legacy field with no equivalent - assert.strictEqual(result.exitCode, 1, 'Should exit 1 when legacy patterns found'); - assert.ok(result.items.length > 0, 'Should detect legacy items'); - const legacyFieldItem = result.items.find((item) => item.legacy === 'legacyField'); - assert.ok(legacyFieldItem, 'Should detect legacyField'); - assert.ok(legacyFieldItem.guidance, 'Should have guidance for legacyField'); - assert.ok( - legacyFieldItem.guidance.includes('no modern equivalent') || legacyFieldItem.guidance.includes('Remove'), - 'Guidance should mention removal or no equivalent', - ); - assert.strictEqual( - legacyFieldItem.replacement, - '(removed — see guidance)', - 'Replacement should indicate removal', - ); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- -// Test 6: Partial migration reports completed and remaining -// --------------------------------------------------------------------------- -test('migrate partial migration reports completed and remaining items', async () => { - const dir = createTempDir(); - try { - const legacyConfig = `export default { - testMode: "verify", - testProfiles: { - quick: { - usesPreset: "safe-ci", - }, - }, -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir }); - const result = await migrateCommand({ write: true }, ctx); - assert.ok(result.completed.length > 0, 'Should have completed items'); - assert.ok(result.message, 'Should have output message'); - assert.ok(result.message.includes('Completed'), 'Should mention completed'); - assert.ok(result.message.includes('Migration complete'), 'Should indicate completion'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- -// Test 7: Preserves comments/formatting where feasible -// --------------------------------------------------------------------------- -test('migrate preserves comments and formatting where feasible', async () => { - const dir = createTempDir(); - try { - // Config with specific formatting (comments, indentation) - const legacyConfig = `export default { - // This is a comment about testMode - testMode: "verify", - /* - * Block comment about testProfiles - */ - testProfiles: { - quick: { - // Inline comment - usesPreset: "safe-ci", - }, - }, -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir }); - const result = await migrateCommand({ write: true }, ctx); - const content = readFileSync(resolve(dir, 'apophis.config.js'), 'utf-8'); - // Verify comments are preserved - assert.ok(content.includes('// This is a comment about testMode'), 'Should preserve line comment'); - assert.ok(content.includes('Block comment about testProfiles'), 'Should preserve block comment'); - assert.ok(content.includes('// Inline comment'), 'Should preserve inline comment'); - // Verify replacements were made - assert.ok(content.includes('mode:'), 'Should have mode'); - assert.ok(content.includes('profiles:'), 'Should have profiles'); - assert.ok(content.includes('preset:'), 'Should have preset'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- // Test 8: Migrate exits 0 when config is already modern // --------------------------------------------------------------------------- test('migrate exits 0 when config is already modern', async () => { @@ -410,65 +138,6 @@ export default app;`; } }); // --------------------------------------------------------------------------- -// Test 10: Migrate emits guidance for each legacy field -// --------------------------------------------------------------------------- -test('migrate emits guidance for each legacy field', async () => { - const dir = createTempDir(); - try { - const legacyConfig = `export default { - testMode: "verify", - testProfiles: { - quick: { - usesPreset: "safe-ci", - }, - }, -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir }); - const result = await migrateCommand({ check: true }, ctx); - assert.ok(result.items.length > 0, 'Should have items'); - for (const item of result.items) { - assert.ok(item.guidance, `Item ${item.legacy} should have guidance`); - assert.ok( - item.guidance.includes('Replace') || item.guidance.includes('with') || item.guidance.includes('Remove'), - `Guidance for ${item.legacy} should mention replacement or removal`, - ); - } - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- -// Test 11: Config rewriter replaces legacy fields -// --------------------------------------------------------------------------- -test('config rewriter replaces legacy fields', () => { - const dir = createTempDir(); - try { - const content = `export default { - testMode: "verify", - testProfiles: { - quick: { - usesPreset: "safe-ci", - }, - }, -};`; - writeFileSync(resolve(dir, 'test.config.js'), content); - const items = detectLegacyConfigFields(content, 'test.config.js'); - assert.strictEqual(items.length, 3, 'Should detect 3 legacy fields'); - const result = rewriteConfigFile( - resolve(dir, 'test.config.js'), - items, - ); - assert.strictEqual(result.modified, true, 'Should modify content'); - assert.ok(result.content.includes('mode:'), 'Should have mode'); - assert.ok(result.content.includes('profiles:'), 'Should have profiles'); - assert.ok(result.content.includes('preset:'), 'Should have preset'); - assert.ok(!result.content.includes('testMode'), 'Should not have testMode'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- // Test 12: x-validate-runtime is NOT a legacy annotation (it is the current, active format) // --------------------------------------------------------------------------- test('x-validate-runtime is current annotation, not legacy', () => { @@ -514,60 +183,6 @@ export default app;`; } }); // --------------------------------------------------------------------------- -// Test 14: Dry-run default mode (safe by default) -// --------------------------------------------------------------------------- -test('migrate defaults to dry-run mode (safe by default)', async () => { - const dir = createTempDir(); - try { - const legacyConfig = `export default { - testMode: "verify", -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir }); - // No mode specified — should default to dry-run - const result = await migrateCommand({}, ctx); - assert.strictEqual(result.exitCode, 1, 'Should exit 1 in dry-run mode'); - assert.ok(result.message?.includes('Dry run'), 'Should indicate dry run'); - // Verify file was NOT modified - const content = readFileSync(resolve(dir, 'apophis.config.js'), 'utf-8'); - assert.ok(content.includes('testMode'), 'File should still have testMode'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- -// Test 15: Mixed legacy/modern field detection at rewriter level -// --------------------------------------------------------------------------- -test('config rewriter detects mixed legacy and modern fields', () => { - const dir = createTempDir(); - try { - const content = `export default { - // Both legacy and modern present - testMode: "verify", - mode: "observe", - testProfiles: { - quick: { - usesPreset: "safe-ci", - }, - }, - profiles: { - modern: { - preset: "safe-ci", - }, - }, -};`; - writeFileSync(resolve(dir, 'test.config.js'), content); - const mixedReports = detectMixedLegacyModernFields(content, 'test.config.js'); - assert.ok(mixedReports.length > 0, 'Should detect mixed fields'); - const testModeReport = mixedReports.find((r) => r.legacy === 'testMode'); - assert.ok(testModeReport, 'Should report testMode as mixed'); - assert.ok(testModeReport.guidance.includes('testMode'), 'Guidance should mention testMode'); - assert.ok(testModeReport.guidance.includes('mode'), 'Guidance should mention mode'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- // Test 16: Ambiguous route pattern detection // --------------------------------------------------------------------------- test('route rewriter detects ambiguous route patterns with context', () => { @@ -618,40 +233,6 @@ export default app;`; } }); // --------------------------------------------------------------------------- -// Test 18: Legacy fixture detection -// --------------------------------------------------------------------------- -test('migrate detects legacy patterns in fixture config', async () => { - const ctx = makeCtx({ cwd: 'src/cli/__fixtures__/legacy-config' }); - const result = await migrateCommand({ check: true }, ctx); - assert.strictEqual(result.exitCode, 1, 'Should detect legacy patterns in fixture'); - assert.ok(result.items.length > 0, 'Should find legacy items'); - const legacyNames = result.items.map((item) => item.legacy); - assert.ok(legacyNames.includes('testMode'), 'Should detect testMode in fixture'); - assert.ok(legacyNames.includes('testProfiles'), 'Should detect testProfiles in fixture'); - assert.ok(legacyNames.includes('testPresets'), 'Should detect testPresets in fixture'); - assert.ok(legacyNames.includes('envPolicies'), 'Should detect envPolicies in fixture'); -}); -// --------------------------------------------------------------------------- -// Test 19: JSON output format -// --------------------------------------------------------------------------- -test('migrate outputs JSON format with all fields', async () => { - const dir = createTempDir(); - try { - const legacyConfig = `export default { - testMode: "verify", -};`; - writeFileSync(resolve(dir, 'apophis.config.js'), legacyConfig); - const ctx = makeCtx({ cwd: dir, options: { ...makeCtx().options, format: 'json' } }); - const result = await migrateCommand({ check: true }, ctx); - assert.strictEqual(result.exitCode, 1, 'Should exit 1'); - assert.ok(result.items.length > 0, 'Should have items'); - assert.ok(result.totalRewrites, 'Should have totalRewrites'); - assert.ok(result.filesWouldBeModified, 'Should have filesWouldBeModified'); - } finally { - cleanup(dir); - } -}); -// --------------------------------------------------------------------------- // Test 20: No files found returns usage error // --------------------------------------------------------------------------- test('migrate returns usage error when no files found', async () => {