Files
flyer-crawler.projectium.com/tests/qa/test-pm2-safeguard-logic.js
Torben Sorensen c059b30201
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 30m15s
PM2 Process Isolation
2026-02-17 20:49:01 -08:00

223 lines
8.3 KiB
JavaScript

/**
* PM2 Safeguard Logic Validation Tests
*
* This script tests the safeguard logic implemented in deployment workflows
* to prevent accidental deletion of all PM2 processes.
*
* Run with: node tests/qa/test-pm2-safeguard-logic.js
*/
// Simulate the safeguard logic from workflows
function evaluateSafeguard(totalProcesses, targetProcesses, threshold = 3) {
// SAFEGUARD 1: Process count validation
// If we're about to delete ALL processes AND there are more than threshold processes,
// this indicates a potential filter bug
const shouldAbort = targetProcesses === totalProcesses && totalProcesses > threshold;
return { shouldAbort, totalProcesses, targetProcesses };
}
// Test scenarios
const scenarios = [
// Normal operations - should NOT abort
{
name: 'Normal production cleanup - 3 errored out of 15',
totalProcs: 15,
targetProcs: 3,
expectedAbort: false,
description: 'Production deployment cleans up only the 3 errored production processes',
},
{
name: 'Normal test cleanup - 3 test processes out of 15',
totalProcs: 15,
targetProcs: 3,
expectedAbort: false,
description: 'Test deployment cleans up only the 3 test processes',
},
{
name: 'Single process cleanup - 1 errored out of 10',
totalProcs: 10,
targetProcs: 1,
expectedAbort: false,
description: 'Only one process is errored and targeted for cleanup',
},
{
name: 'No processes to clean - 0 out of 10',
totalProcs: 10,
targetProcs: 0,
expectedAbort: false,
description: 'No processes match the cleanup criteria',
},
{
name: 'Fresh server - 3 out of 3 (at threshold)',
totalProcs: 3,
targetProcs: 3,
expectedAbort: false,
description: 'Server with only 3 processes (threshold boundary - should proceed)',
},
{
name: 'Minimal server - 2 out of 2',
totalProcs: 2,
targetProcs: 2,
expectedAbort: false,
description: 'Server with only 2 processes (below threshold)',
},
{
name: 'Empty PM2 state - 0 out of 0',
totalProcs: 0,
targetProcs: 0,
expectedAbort: false,
description: 'No PM2 processes at all (fresh install)',
},
// Dangerous operations - SHOULD abort
{
name: 'Filter bug - all 10 processes targeted',
totalProcs: 10,
targetProcs: 10,
expectedAbort: true,
description: 'DANGEROUS: Filter would delete ALL 10 processes - indicates bug',
},
{
name: 'Filter bug - all 15 processes targeted',
totalProcs: 15,
targetProcs: 15,
expectedAbort: true,
description: 'DANGEROUS: Filter would delete ALL 15 processes - indicates bug',
},
{
name: 'Filter bug - all 4 processes targeted',
totalProcs: 4,
targetProcs: 4,
expectedAbort: true,
description: 'DANGEROUS: Filter would delete ALL 4 processes (just above threshold)',
},
{
name: 'Filter bug - all 100 processes targeted',
totalProcs: 100,
targetProcs: 100,
expectedAbort: true,
description: 'DANGEROUS: Filter would delete ALL 100 processes - extreme case',
},
];
// Run tests
console.log('========================================');
console.log('PM2 SAFEGUARD LOGIC VALIDATION');
console.log('========================================\n');
let passed = 0;
let failed = 0;
scenarios.forEach((scenario, index) => {
const result = evaluateSafeguard(scenario.totalProcs, scenario.targetProcs);
const testPassed = result.shouldAbort === scenario.expectedAbort;
if (testPassed) {
passed++;
console.log(`[PASS] Test ${index + 1}: ${scenario.name}`);
console.log(` Total: ${scenario.totalProcs}, Target: ${scenario.targetProcs}`);
console.log(` Expected abort: ${scenario.expectedAbort}, Got: ${result.shouldAbort}`);
} else {
failed++;
console.log(`[FAIL] Test ${index + 1}: ${scenario.name}`);
console.log(` Total: ${scenario.totalProcs}, Target: ${scenario.targetProcs}`);
console.log(` Expected abort: ${scenario.expectedAbort}, Got: ${result.shouldAbort}`);
console.log(` Description: ${scenario.description}`);
}
console.log('');
});
console.log('========================================');
console.log(`RESULTS: ${passed} passed, ${failed} failed`);
console.log('========================================');
// Edge case tests for specific workflow patterns
console.log('\n========================================');
console.log('WORKFLOW-SPECIFIC FILTER TESTS');
console.log('========================================\n');
// Simulate production workflow filter
function simulateProdFilter(processList) {
const prodProcesses = [
'flyer-crawler-api',
'flyer-crawler-worker',
'flyer-crawler-analytics-worker',
];
return processList.filter(
(p) => (p.status === 'errored' || p.status === 'stopped') && prodProcesses.includes(p.name),
);
}
// Simulate test workflow filter
function simulateTestFilter(processList) {
return processList.filter((p) => p.name && p.name.endsWith('-test'));
}
// Test case: Normal mixed environment
const mixedEnvProcesses = [
{ name: 'flyer-crawler-api', status: 'online' },
{ name: 'flyer-crawler-worker', status: 'errored' },
{ name: 'flyer-crawler-analytics-worker', status: 'online' },
{ name: 'flyer-crawler-api-test', status: 'online' },
{ name: 'flyer-crawler-worker-test', status: 'online' },
{ name: 'flyer-crawler-analytics-worker-test', status: 'online' },
{ name: 'stock-alert-api', status: 'online' },
{ name: 'stock-alert-worker', status: 'online' },
];
const prodFiltered = simulateProdFilter(mixedEnvProcesses);
const testFiltered = simulateTestFilter(mixedEnvProcesses);
console.log('Test: Mixed environment with production processes');
console.log(`Total processes: ${mixedEnvProcesses.length}`);
console.log(`Production filter matches: ${prodFiltered.length}`);
console.log(` Names: ${prodFiltered.map((p) => p.name).join(', ') || '(none)'}`);
console.log(`Test filter matches: ${testFiltered.length}`);
console.log(` Names: ${testFiltered.map((p) => p.name).join(', ')}`);
// Verify production filter does NOT match test or other apps
const prodFilterSafe = prodFiltered.every(
(p) => !p.name.endsWith('-test') && p.name.startsWith('flyer-crawler-'),
);
console.log(`Production filter safe (no test/other apps): ${prodFilterSafe ? 'PASS' : 'FAIL'}`);
// Verify test filter does NOT match production or other apps
const testFilterSafe = testFiltered.every((p) => p.name.endsWith('-test'));
console.log(`Test filter safe (only -test suffix): ${testFilterSafe ? 'PASS' : 'FAIL'}`);
// Test case: All processes errored (dangerous scenario)
console.log('\nTest: All production processes errored (edge case)');
const allErroredProd = [
{ name: 'flyer-crawler-api', status: 'errored' },
{ name: 'flyer-crawler-worker', status: 'errored' },
{ name: 'flyer-crawler-analytics-worker', status: 'errored' },
{ name: 'flyer-crawler-api-test', status: 'online' },
{ name: 'flyer-crawler-worker-test', status: 'online' },
{ name: 'stock-alert-api', status: 'online' },
];
const allErroredFiltered = simulateProdFilter(allErroredProd);
const safeguardCheck = evaluateSafeguard(allErroredProd.length, allErroredFiltered.length);
console.log(`Total processes: ${allErroredProd.length}`);
console.log(`Production errored processes: ${allErroredFiltered.length}`);
console.log(`Safeguard would abort: ${safeguardCheck.shouldAbort}`);
console.log(`Expected: false (3 out of 6 is not ALL processes)`);
console.log(`Result: ${safeguardCheck.shouldAbort === false ? 'PASS' : 'FAIL'}`);
// Test case: Bug simulation - filter returns everything
console.log('\nTest: Bug simulation - filter returns all processes');
const buggyFilterResult = mixedEnvProcesses; // Simulating a bug where filter returns everything
const buggySafeguardCheck = evaluateSafeguard(mixedEnvProcesses.length, buggyFilterResult.length);
console.log(`Total processes: ${mixedEnvProcesses.length}`);
console.log(`Buggy filter matches: ${buggyFilterResult.length}`);
console.log(`Safeguard would abort: ${buggySafeguardCheck.shouldAbort}`);
console.log(`Expected: true (prevents all-process deletion)`);
console.log(`Result: ${buggySafeguardCheck.shouldAbort === true ? 'PASS' : 'FAIL'}`);
console.log('\n========================================');
console.log('ALL TESTS COMPLETE');
console.log('========================================');
// Exit with appropriate code
process.exit(failed > 0 ? 1 : 0);