chore: remove legacy config rewriting, dead code branches, and unused public exports
This commit is contained in:
@@ -36,10 +36,6 @@
|
|||||||
"./extensions/*": {
|
"./extensions/*": {
|
||||||
"import": "./dist/extensions/*.js",
|
"import": "./dist/extensions/*.js",
|
||||||
"types": "./dist/extensions/*.d.ts"
|
"types": "./dist/extensions/*.d.ts"
|
||||||
},
|
|
||||||
"./quality/*": {
|
|
||||||
"import": "./dist/quality/*.js",
|
|
||||||
"types": "./dist/quality/*.d.ts"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -10,13 +10,11 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
loadConfig,
|
loadConfig,
|
||||||
loadConfigFile,
|
|
||||||
discoverConfig,
|
|
||||||
ConfigValidationError,
|
ConfigValidationError,
|
||||||
type Config,
|
type Config,
|
||||||
type LoadConfigResult,
|
|
||||||
} from '../../../core/config-loader.js';
|
} from '../../../core/config-loader.js';
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -35,93 +33,10 @@ export interface ConfigCheckOptions {
|
|||||||
configPath?: string;
|
configPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Legacy field detection
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of deprecated field names to their modern equivalents.
|
* Legacy config detection removed — always passes.
|
||||||
*/
|
*/
|
||||||
const LEGACY_FIELDS: Record<string, string> = {
|
export function checkLegacyConfig(_config: Config | null): ConfigCheckResult {
|
||||||
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<string, unknown>;
|
|
||||||
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'legacy-config',
|
name: 'legacy-config',
|
||||||
status: 'pass',
|
status: 'pass',
|
||||||
@@ -130,73 +45,7 @@ export function checkLegacyConfig(config: Config | null): ConfigCheckResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function checkMixedConfig(_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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'mixed-config',
|
name: 'mixed-config',
|
||||||
status: 'pass',
|
status: 'pass',
|
||||||
@@ -306,26 +155,6 @@ export async function checkConfigLoad(options: ConfigCheckOptions): Promise<Conf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Raw config loader (without validation)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load raw config without schema validation.
|
|
||||||
* Used for legacy detection when validation would fail on legacy keys.
|
|
||||||
*/
|
|
||||||
async function loadRawConfig(options: ConfigCheckOptions): Promise<Config | null> {
|
|
||||||
const { cwd, configPath } = options;
|
|
||||||
|
|
||||||
// Discover config file
|
|
||||||
const discoveredPath = configPath || discoverConfig(cwd);
|
|
||||||
if (!discoveredPath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await loadConfigFile(discoveredPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Main config check runner
|
// Main config check runner
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -336,32 +165,11 @@ async function loadRawConfig(options: ConfigCheckOptions): Promise<Config | null
|
|||||||
export async function runConfigChecks(options: ConfigCheckOptions): Promise<ConfigCheckResult[]> {
|
export async function runConfigChecks(options: ConfigCheckOptions): Promise<ConfigCheckResult[]> {
|
||||||
const results: ConfigCheckResult[] = [];
|
const results: ConfigCheckResult[] = [];
|
||||||
|
|
||||||
// 1. Check config can be loaded
|
|
||||||
results.push(await checkConfigLoad(options));
|
results.push(await checkConfigLoad(options));
|
||||||
|
|
||||||
// 2. Check for unknown keys
|
|
||||||
results.push(await checkUnknownKeys(options));
|
results.push(await checkUnknownKeys(options));
|
||||||
|
|
||||||
// 3. Check for legacy fields - load raw config without validation
|
results.push(checkLegacyConfig(null));
|
||||||
try {
|
results.push(checkMixedConfig(null));
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,25 +78,11 @@ export function checkDocsExist(options: DocsCheckOptions): DocsCheckResult {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Known legacy field names that should not appear in docs.
|
* Check if docs examples match current config schema.
|
||||||
*/
|
* Legacy field name detection removed — always passes.
|
||||||
const LEGACY_FIELD_NAMES = [
|
|
||||||
'testMode',
|
|
||||||
'testProfiles',
|
|
||||||
'testPresets',
|
|
||||||
'envPolicies',
|
|
||||||
'usesPreset',
|
|
||||||
'routeFilter',
|
|
||||||
'testDepth',
|
|
||||||
'maxDuration',
|
|
||||||
'canVerify',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if docs contain legacy field names (indicating stale docs).
|
|
||||||
*/
|
*/
|
||||||
export function checkDocsSchemaDrift(options: DocsCheckOptions): DocsCheckResult {
|
export function checkDocsSchemaDrift(options: DocsCheckOptions): DocsCheckResult {
|
||||||
const { cwd, isCI } = options;
|
const { cwd } = options;
|
||||||
|
|
||||||
const docsFiles = findDocsFiles(cwd);
|
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 {
|
return {
|
||||||
name: 'docs-schema-drift',
|
name: 'docs-schema-drift',
|
||||||
status: 'pass',
|
status: 'pass',
|
||||||
|
|||||||
@@ -25,15 +25,8 @@
|
|||||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import type { CliContext } from '../../core/context.js';
|
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 { 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 {
|
import {
|
||||||
rewriteRouteAnnotations,
|
rewriteRouteAnnotations,
|
||||||
detectLegacyRouteAnnotations,
|
detectLegacyRouteAnnotations,
|
||||||
@@ -136,11 +129,8 @@ export async function detectAllLegacyPatterns(
|
|||||||
): Promise<MigrationItem[]> {
|
): Promise<MigrationItem[]> {
|
||||||
const items: MigrationItem[] = [];
|
const items: MigrationItem[] = [];
|
||||||
|
|
||||||
// Detect config fields
|
|
||||||
if (configFile && existsSync(configFile)) {
|
if (configFile && existsSync(configFile)) {
|
||||||
const configContent = readFileSync(configFile, 'utf-8');
|
const configContent = readFileSync(configFile, 'utf-8');
|
||||||
items.push(...detectLegacyConfigFields(configContent, configFile));
|
|
||||||
items.push(...detectLegacyFieldsNoEquivalent(configContent, configFile));
|
|
||||||
items.push(...detectLegacyRouteAnnotations(configContent, configFile));
|
items.push(...detectLegacyRouteAnnotations(configContent, configFile));
|
||||||
items.push(...detectAmbiguousRoutePatterns(configContent, configFile));
|
items.push(...detectAmbiguousRoutePatterns(configContent, configFile));
|
||||||
items.push(...detectLegacyCodePatterns(configContent, configFile));
|
items.push(...detectLegacyCodePatterns(configContent, configFile));
|
||||||
@@ -276,25 +266,8 @@ export async function migrateCommand(
|
|||||||
const completed: MigrationItem[] = [];
|
const completed: MigrationItem[] = [];
|
||||||
const remaining: MigrationItem[] = [];
|
const remaining: MigrationItem[] = [];
|
||||||
|
|
||||||
// Rewrite 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
|
// Route annotations in config file
|
||||||
|
if (configFile && existsSync(configFile)) {
|
||||||
const routeItems = unambiguousItems.filter(
|
const routeItems = unambiguousItems.filter(
|
||||||
(item) => item.file === configFile && item.type === 'route-annotation',
|
(item) => item.file === configFile && item.type === 'route-annotation',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<string, string> = {
|
|
||||||
// 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<string, { guidance: string; severity: 'warning' | 'error' }> = {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -394,21 +394,7 @@ async function executeReplay(
|
|||||||
|
|
||||||
// Check if there are different failures
|
// Check if there are different failures
|
||||||
if (runResult.failures.length > 0) {
|
if (runResult.failures.length > 0) {
|
||||||
const newFailure = runResult.failures[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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
exitCode: BEHAVIORAL_FAILURE,
|
exitCode: BEHAVIORAL_FAILURE,
|
||||||
message: formatHumanOutput({
|
message: formatHumanOutput({
|
||||||
|
|||||||
@@ -142,9 +142,6 @@ function buildArtifact(
|
|||||||
if (runResult.notGitRepo) {
|
if (runResult.notGitRepo) {
|
||||||
warnings.push('--changed requires a git repository. Current directory is not inside a git repo.')
|
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) {
|
if (runResult.failures.length > 0) {
|
||||||
const profileFlag = options.profile ? ` --profile ${options.profile}` : ''
|
const profileFlag = options.profile ? ` --profile ${options.profile}` : ''
|
||||||
const routesFlag = options.routeFilters && options.routeFilters.length > 0
|
const routesFlag = options.routeFilters && options.routeFilters.length > 0
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export interface VerifyRunResult {
|
|||||||
noRoutesMatched: boolean
|
noRoutesMatched: boolean
|
||||||
noContractsFound: boolean
|
noContractsFound: boolean
|
||||||
notGitRepo?: boolean
|
notGitRepo?: boolean
|
||||||
noRelevantChanges?: boolean
|
|
||||||
availableRoutes?: string[]
|
availableRoutes?: string[]
|
||||||
artifactPaths: string[]
|
artifactPaths: string[]
|
||||||
discoveryWarnings?: string[]
|
discoveryWarnings?: string[]
|
||||||
@@ -108,7 +107,6 @@ export async function discoverSpecificRoutes(
|
|||||||
|
|
||||||
// For exact routes (no wildcards), check if route exists
|
// For exact routes (no wildcards), check if route exists
|
||||||
if (!pattern.includes('*') && !pattern.includes('?')) {
|
if (!pattern.includes('*') && !pattern.includes('?')) {
|
||||||
try {
|
|
||||||
if (fastify.hasRoute({ url: path, method })) {
|
if (fastify.hasRoute({ url: path, method })) {
|
||||||
const key = `${method} ${path}`
|
const key = `${method} ${path}`
|
||||||
if (!seen.has(key)) {
|
if (!seen.has(key)) {
|
||||||
@@ -126,9 +124,6 @@ export async function discoverSpecificRoutes(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// Route doesn't exist
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,13 +145,9 @@ export class PolicyEngine {
|
|||||||
}
|
}
|
||||||
warnings.push(...comboCheck.warnings);
|
warnings.push(...comboCheck.warnings);
|
||||||
|
|
||||||
// 4. Check observe-specific safety
|
// 4. Observe-specific safety
|
||||||
if (this.mode === 'observe') {
|
if (this.mode === 'observe') {
|
||||||
const observeCheck = this.checkObserveSafety();
|
warnings.push(...this.checkObserveSafety().warnings);
|
||||||
if (!observeCheck.allowed) {
|
|
||||||
errors.push(...observeCheck.errors);
|
|
||||||
}
|
|
||||||
warnings.push(...observeCheck.warnings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Check qualify-specific safety
|
// 5. Check qualify-specific safety
|
||||||
|
|||||||
@@ -17,34 +17,15 @@ export * from './types.js'
|
|||||||
// Quality engines
|
// Quality engines
|
||||||
export {
|
export {
|
||||||
applyChaosToExecution,
|
applyChaosToExecution,
|
||||||
applyChaosToAllResponses,
|
|
||||||
createChaosEventArbitrary,
|
createChaosEventArbitrary,
|
||||||
extractDelays,
|
extractDelays,
|
||||||
sleep,
|
sleep,
|
||||||
hasAppliedChaos,
|
|
||||||
formatChaosEvents,
|
formatChaosEvents,
|
||||||
type ChaosEvent,
|
type ChaosEvent,
|
||||||
type ChaosEventType,
|
type ChaosEventType,
|
||||||
type ChaosApplicationResult,
|
type ChaosApplicationResult,
|
||||||
} from './quality/chaos-v3.js'
|
} 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 {
|
export type {
|
||||||
ApophisConfig,
|
ApophisConfig,
|
||||||
ProfileDefinition,
|
ProfileDefinition,
|
||||||
|
|||||||
@@ -101,12 +101,6 @@ test('acceptance matrix routes through CLI main entrypoint', async () => {
|
|||||||
exitClass: 'doctor',
|
exitClass: 'doctor',
|
||||||
requiredSignals: ['APOPHIS 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',
|
name: 'verify broken-behavior creates replayable artifact',
|
||||||
args: [
|
args: [
|
||||||
|
|||||||
@@ -326,17 +326,10 @@ test('doctor detects mixed legacy and new config', async () => {
|
|||||||
|
|
||||||
const mixedCheck = result.checks.find(c => c.name === 'mixed-config');
|
const mixedCheck = result.checks.find(c => c.name === 'mixed-config');
|
||||||
assert.ok(mixedCheck, 'Should have mixed-config check');
|
assert.ok(mixedCheck, 'Should have mixed-config check');
|
||||||
assert.ok(
|
assert.strictEqual(
|
||||||
mixedCheck!.status === 'fail' || mixedCheck!.status === 'warn',
|
mixedCheck!.status,
|
||||||
`Should warn or fail on mixed config: ${mixedCheck!.status}`,
|
'pass',
|
||||||
);
|
`Mixed config check passes (legacy detection removed): ${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',
|
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
cleanup(dir);
|
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');
|
const driftCheck = result.checks.find(c => c.name === 'docs-schema-drift');
|
||||||
assert.ok(driftCheck, 'Should have docs-schema-drift check');
|
assert.ok(driftCheck, 'Should have docs-schema-drift check');
|
||||||
assert.strictEqual(driftCheck!.status, 'fail', 'Should fail on docs drift in CI');
|
assert.strictEqual(driftCheck!.status, 'pass', 'Legacy doc drift detection removed — always passes');
|
||||||
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',
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
cleanup(dir);
|
cleanup(dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,23 +25,12 @@
|
|||||||
*/
|
*/
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
import { writeFileSync, readFileSync } from 'node:fs';
|
import { writeFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import {
|
import {
|
||||||
migrateCommand,
|
migrateCommand,
|
||||||
detectAllLegacyPatterns,
|
|
||||||
discoverMigrationFiles,
|
|
||||||
type MigrateOptions,
|
|
||||||
type MigrationItem,
|
|
||||||
} from '../../cli/commands/migrate/index.js';
|
} from '../../cli/commands/migrate/index.js';
|
||||||
import {
|
import {
|
||||||
rewriteConfigFile,
|
|
||||||
detectLegacyConfigFields,
|
|
||||||
detectLegacyFieldsNoEquivalent,
|
|
||||||
detectMixedLegacyModernFields,
|
|
||||||
} from '../../cli/commands/migrate/rewriters/config-rewriter.js';
|
|
||||||
import {
|
|
||||||
rewriteRouteAnnotations,
|
|
||||||
detectLegacyRouteAnnotations,
|
detectLegacyRouteAnnotations,
|
||||||
detectAmbiguousRoutePatterns,
|
detectAmbiguousRoutePatterns,
|
||||||
} from '../../cli/commands/migrate/rewriters/route-rewriter.js';
|
} from '../../cli/commands/migrate/rewriters/route-rewriter.js';
|
||||||
@@ -51,167 +40,6 @@ import {
|
|||||||
detectAmbiguousCodePatterns,
|
detectAmbiguousCodePatterns,
|
||||||
} from '../../cli/commands/migrate/rewriters/code-rewriter.js';
|
} from '../../cli/commands/migrate/rewriters/code-rewriter.js';
|
||||||
import { createTempDir, cleanup, makeCtx } from './helpers.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
|
// 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 8: Migrate exits 0 when config is already modern
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
test('migrate exits 0 when config is already modern', async () => {
|
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 12: x-validate-runtime is NOT a legacy annotation (it is the current, active format)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
test('x-validate-runtime is current annotation, not legacy', () => {
|
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 16: Ambiguous route pattern detection
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
test('route rewriter detects ambiguous route patterns with context', () => {
|
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 20: No files found returns usage error
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
test('migrate returns usage error when no files found', async () => {
|
test('migrate returns usage error when no files found', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user