Files
apophis-fastify/src/test/cli/migrate-reliability.test.ts
T

842 lines
28 KiB
TypeScript
Raw Normal View History

/**
* WS8: Migrate mode reliability improvements - Comprehensive tests
*
* Tests cover:
* 1. Mixed legacy and modern config detection
* 2. Dry-run shows exact rewrites (file path, line number, legacy text, replacement text)
* 3. Write performs rewrites correctly
* 4. Ambiguous rewrite stops and shows context
* 5. Legacy field with no equivalent emits guidance
* 6. Partial migration reports completed and remaining
* 7. Preserves comments/formatting where feasible
* 8. Migrate exits 0 when config is already modern
* 9. Migrate exits 2 when ambiguous in write mode
* 10. Migrate emits guidance for each legacy field
* 11. Config rewriter replaces legacy fields
* 12. Route rewriter detects x-validate-runtime annotation
* 13. Code rewriter detects legacy patterns
*
* Architecture:
* - Dependency injection: all dependencies passed explicitly
* - No optional imports
* - Inline comments for documentation
* - Property and state model-based testing focused on confidence
* - Iterative small steps with rapid feedback loops
*/
import { test } from 'node:test';
import assert from 'node:assert';
import { writeFileSync, readFileSync } 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';
import {
rewriteCodePatterns,
detectLegacyCodePatterns,
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
// ---------------------------------------------------------------------------
test('migrate ambiguous rewrite stops and shows surrounding context', async () => {
const dir = createTempDir();
try {
// Create a file with an ambiguous code pattern
const ambiguousCode = `import Fastify from 'fastify';
const app = Fastify();
// This is ambiguous: what does oldApi() mean here?
app.register(oldApi());
export default app;`;
writeFileSync(resolve(dir, 'app.js'), ambiguousCode);
// Also create a config file so migration has something to work with
const config = `export default {
mode: "verify",
};`;
writeFileSync(resolve(dir, 'apophis.config.js'), config);
const ctx = makeCtx({ cwd: dir });
const result = await migrateCommand({ write: true }, ctx);
// Should stop with exit code 2 (USAGE_ERROR) because ambiguous patterns found
assert.strictEqual(result.exitCode, 2, 'Should exit 2 when ambiguous patterns found in write mode');
assert.ok(result.remaining.length > 0, 'Should have remaining items');
assert.ok(result.message, 'Should have output message');
assert.ok(result.message.includes('Ambiguous'), 'Should mention ambiguous patterns');
assert.ok(result.message.includes('oldApi()'), 'Should show the ambiguous pattern');
assert.ok(result.message.includes('manual choice'), 'Should mention manual choice');
// Verify context is shown (surrounding lines)
assert.ok(result.message.includes('app.register'), 'Should show surrounding context');
} finally {
cleanup(dir);
}
});
// ---------------------------------------------------------------------------
// 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 () => {
const dir = createTempDir();
try {
const modernConfig = `export default {
mode: "verify",
profiles: {
quick: {
preset: "safe-ci",
routes: ["GET /users"],
},
},
presets: {
"safe-ci": {
depth: "quick",
timeout: 5000,
},
},
environments: {
local: {
allowVerify: true,
},
},
};`;
writeFileSync(resolve(dir, 'apophis.config.js'), modernConfig);
const ctx = makeCtx({ cwd: dir });
const result = await migrateCommand({ check: true }, ctx);
assert.strictEqual(result.exitCode, 0, 'Should exit 0 for modern config');
assert.strictEqual(result.items.length, 0, 'Should have no items');
assert.ok(result.message, 'Should have message');
assert.ok(result.message.includes('up to date'), 'Should indicate up to date');
} finally {
cleanup(dir);
}
});
// ---------------------------------------------------------------------------
// Test 9: Migrate exits 2 when ambiguous in write mode
// ---------------------------------------------------------------------------
test('migrate exits 2 when ambiguous patterns found in write mode', async () => {
const dir = createTempDir();
try {
const config = `export default {
mode: "verify",
};`;
writeFileSync(resolve(dir, 'apophis.config.js'), config);
// Create app with an ambiguous pattern
const code = `import Fastify from 'fastify';
const app = Fastify();
// Ambiguous pattern
app.register(oldApi());
export default app;`;
writeFileSync(resolve(dir, 'app.js'), code);
const ctx = makeCtx({ cwd: dir });
const result = await migrateCommand({ write: true }, ctx);
// Should exit 2 because ambiguous patterns found
assert.strictEqual(result.exitCode, 2, 'Should exit 2 when ambiguous patterns found in write mode');
assert.ok(result.remaining.length > 0, 'Should have remaining ambiguous items');
assert.ok((result.manualChoicesRequired ?? 0) > 0, 'Should indicate manual choices required');
} finally {
cleanup(dir);
}
});
// ---------------------------------------------------------------------------
// 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: Route rewriter detects x-validate-runtime annotation
// ---------------------------------------------------------------------------
test('route rewriter detects x-validate-runtime annotation', () => {
const dir = createTempDir();
try {
const content = `export default {
schema: {
'x-validate-runtime': true,
},
};`;
writeFileSync(resolve(dir, 'test.routes.js'), content);
const items = detectLegacyRouteAnnotations(content, 'test.routes.js');
assert.strictEqual(items.length, 1, 'Should detect 1 legacy annotation');
const firstItem = items[0];
assert.ok(firstItem, 'Expected one migration item');
assert.strictEqual(firstItem.legacy, 'x-validate-runtime');
assert.strictEqual(firstItem.replacement, 'runtime');
const result = rewriteRouteAnnotations(
resolve(dir, 'test.routes.js'),
items,
);
assert.strictEqual(result.modified, true, 'Should modify content');
assert.ok(result.content.includes("'runtime'"), 'Should have runtime');
assert.ok(!result.content.includes('x-validate-runtime'), 'Should not have legacy annotation');
} finally {
cleanup(dir);
}
});
// ---------------------------------------------------------------------------
// Test 13: Code rewriter detects legacy patterns
// ---------------------------------------------------------------------------
test('code rewriter detects legacy patterns', () => {
const dir = createTempDir();
try {
const content = `import Fastify from 'fastify';
const app = Fastify();
app.register(contract());
app.register(stateful());
app.register(scenario());
export default app;`;
writeFileSync(resolve(dir, 'test.app.js'), content);
const items = detectLegacyCodePatterns(content, 'test.app.js');
assert.strictEqual(items.length, 3, 'Should detect 3 legacy patterns');
const result = rewriteCodePatterns(
resolve(dir, 'test.app.js'),
items,
);
assert.strictEqual(result.modified, true, 'Should modify content');
assert.ok(result.content.includes("verify({ kind: 'contract' })"), 'Should have verify');
assert.ok(result.content.includes("qualify({ kind: 'stateful' })"), 'Should have qualify stateful');
assert.ok(result.content.includes("qualify({ kind: 'scenario' })"), 'Should have qualify scenario');
} finally {
cleanup(dir);
}
});
// ---------------------------------------------------------------------------
// 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', () => {
const dir = createTempDir();
try {
const content = `export default {
schema: {
// This is ambiguous
'x-validate': true,
},
};`;
writeFileSync(resolve(dir, 'test.routes.js'), content);
const items = detectAmbiguousRoutePatterns(content, 'test.routes.js');
assert.strictEqual(items.length, 1, 'Should detect 1 ambiguous pattern');
const firstItem = items[0];
assert.ok(firstItem, 'Expected one migration item');
assert.strictEqual(firstItem.legacy, 'x-validate');
assert.ok(firstItem.ambiguous, 'Should be marked as ambiguous');
assert.ok(firstItem.guidance, 'Should have guidance');
assert.ok(firstItem.guidance.includes('Possible resolutions'), 'Should list possible resolutions');
} finally {
cleanup(dir);
}
});
// ---------------------------------------------------------------------------
// Test 17: Ambiguous code pattern detection with context
// ---------------------------------------------------------------------------
test('code rewriter detects ambiguous code patterns with surrounding context', () => {
const dir = createTempDir();
try {
const content = `import Fastify from 'fastify';
const app = Fastify();
// Ambiguous pattern
app.register(oldApi());
export default app;`;
writeFileSync(resolve(dir, 'test.app.js'), content);
const items = detectAmbiguousCodePatterns(content, 'test.app.js');
assert.strictEqual(items.length, 1, 'Should detect 1 ambiguous pattern');
const firstItem = items[0];
assert.ok(firstItem, 'Expected one migration item');
assert.strictEqual(firstItem.legacy, 'oldApi()');
assert.ok(firstItem.ambiguous, 'Should be marked as ambiguous');
assert.ok(firstItem.guidance, 'Should have guidance');
assert.ok(firstItem.guidance.includes('Possible resolutions'), 'Should list possible resolutions');
assert.ok(firstItem.guidance.includes('Context:'), 'Should show surrounding context');
} finally {
cleanup(dir);
}
});
// ---------------------------------------------------------------------------
// 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 () => {
const dir = createTempDir();
try {
const ctx = makeCtx({ cwd: dir });
const result = await migrateCommand({ check: true }, ctx);
assert.strictEqual(result.exitCode, 2, 'Should exit 2 when no files found');
assert.ok(result.message?.includes('No config or app files found'), 'Should mention no files found');
} finally {
cleanup(dir);
}
});