chore: remove legacy config rewriting, dead code branches, and unused public exports
This commit is contained in:
@@ -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": [
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
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<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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -336,32 +165,11 @@ async function loadRawConfig(options: ConfigCheckOptions): Promise<Config | null
|
||||
export async function runConfigChecks(options: ConfigCheckOptions): Promise<ConfigCheckResult[]> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<MigrationItem[]> {
|
||||
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',
|
||||
);
|
||||
|
||||
@@ -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
|
||||
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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user