// tests/pm2-namespace.test.ts // Comprehensive tests for PM2 namespace implementation // Validates ecosystem configs, workflow files, migration script, and documentation import { describe, it, expect, beforeAll } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; // Base path for the project const PROJECT_ROOT = path.resolve(__dirname, '..'); /** * Helper to read and parse a JavaScript/CommonJS config file * Returns the module.exports object */ function parseEcosystemConfig(filePath: string): { namespace?: string; apps: Array<{ name: string; [key: string]: unknown }>; } { const fullPath = path.join(PROJECT_ROOT, filePath); const content = fs.readFileSync(fullPath, 'utf-8'); // Extract namespace from module.exports const namespaceMatch = content.match(/namespace:\s*['"]([^'"]+)['"]/); const namespace = namespaceMatch ? namespaceMatch[1] : undefined; // Extract apps array - look for the apps property const appsMatch = content.match(/apps:\s*\[([\s\S]*?)\]/); const apps: Array<{ name: string }> = []; if (appsMatch) { // Extract all app names const nameMatches = content.matchAll(/name:\s*['"]([^'"]+)['"]/g); for (const match of nameMatches) { apps.push({ name: match[1] }); } } return { namespace, apps }; } /** * Helper to read a workflow file */ function readWorkflowFile(filePath: string): string { const fullPath = path.join(PROJECT_ROOT, filePath); return fs.readFileSync(fullPath, 'utf-8'); } /** * Helper to check if a file exists */ function fileExists(filePath: string): boolean { const fullPath = path.join(PROJECT_ROOT, filePath); return fs.existsSync(fullPath); } /** * Helper to read any file */ function readFile(filePath: string): string { const fullPath = path.join(PROJECT_ROOT, filePath); return fs.readFileSync(fullPath, 'utf-8'); } describe('PM2 Namespace Implementation', () => { describe('Ecosystem Configurations', () => { describe('ecosystem.config.cjs (Production)', () => { let config: { namespace?: string; apps: Array<{ name: string }> }; beforeAll(() => { config = parseEcosystemConfig('ecosystem.config.cjs'); }); it('should exist', () => { expect(fileExists('ecosystem.config.cjs')).toBe(true); }); it('should have namespace property set to "flyer-crawler-prod"', () => { expect(config.namespace).toBe('flyer-crawler-prod'); }); it('should have namespace at module.exports level (not inside apps)', () => { const content = readFile('ecosystem.config.cjs'); // Namespace should be at module.exports level const moduleExportsMatch = content.match(/module\.exports\s*=\s*\{[\s\S]*?namespace:/); expect(moduleExportsMatch).not.toBeNull(); // Extract just the module.exports block to avoid matching 'apps:' in comments const exportBlock = content.slice(content.indexOf('module.exports')); const namespaceInExport = exportBlock.indexOf("namespace:"); const appsInExport = exportBlock.indexOf("apps:"); expect(namespaceInExport).toBeGreaterThan(-1); expect(appsInExport).toBeGreaterThan(-1); expect(namespaceInExport).toBeLessThan(appsInExport); }); it('should contain production app definitions', () => { const appNames = config.apps.map((app) => app.name); expect(appNames).toContain('flyer-crawler-api'); expect(appNames).toContain('flyer-crawler-worker'); expect(appNames).toContain('flyer-crawler-analytics-worker'); }); it('should NOT contain test app definitions', () => { const appNames = config.apps.map((app) => app.name); expect(appNames).not.toContain('flyer-crawler-api-test'); expect(appNames).not.toContain('flyer-crawler-worker-test'); expect(appNames).not.toContain('flyer-crawler-analytics-worker-test'); }); }); describe('ecosystem-test.config.cjs (Test)', () => { let config: { namespace?: string; apps: Array<{ name: string }> }; beforeAll(() => { config = parseEcosystemConfig('ecosystem-test.config.cjs'); }); it('should exist', () => { expect(fileExists('ecosystem-test.config.cjs')).toBe(true); }); it('should have namespace property set to "flyer-crawler-test"', () => { expect(config.namespace).toBe('flyer-crawler-test'); }); it('should have namespace at module.exports level (not inside apps)', () => { const content = readFile('ecosystem-test.config.cjs'); // Namespace should be at module.exports level const moduleExportsMatch = content.match(/module\.exports\s*=\s*\{[\s\S]*?namespace:/); expect(moduleExportsMatch).not.toBeNull(); // Extract just the module.exports block to avoid matching 'apps:' in comments const exportBlock = content.slice(content.indexOf('module.exports')); const namespaceInExport = exportBlock.indexOf("namespace:"); const appsInExport = exportBlock.indexOf("apps:"); expect(namespaceInExport).toBeGreaterThan(-1); expect(appsInExport).toBeGreaterThan(-1); expect(namespaceInExport).toBeLessThan(appsInExport); }); it('should contain test app definitions', () => { const appNames = config.apps.map((app) => app.name); expect(appNames).toContain('flyer-crawler-api-test'); expect(appNames).toContain('flyer-crawler-worker-test'); expect(appNames).toContain('flyer-crawler-analytics-worker-test'); }); it('should NOT contain production app definitions', () => { const appNames = config.apps.map((app) => app.name); expect(appNames).not.toContain('flyer-crawler-api'); expect(appNames).not.toContain('flyer-crawler-worker'); expect(appNames).not.toContain('flyer-crawler-analytics-worker'); }); }); describe('ecosystem.dev.config.cjs (Development)', () => { let config: { namespace?: string; apps: Array<{ name: string }> }; beforeAll(() => { config = parseEcosystemConfig('ecosystem.dev.config.cjs'); }); it('should exist', () => { expect(fileExists('ecosystem.dev.config.cjs')).toBe(true); }); it('should have namespace property set to "flyer-crawler-dev"', () => { expect(config.namespace).toBe('flyer-crawler-dev'); }); it('should have namespace at module.exports level (not inside apps)', () => { const content = readFile('ecosystem.dev.config.cjs'); // Namespace should be at module.exports level const moduleExportsMatch = content.match(/module\.exports\s*=\s*\{[\s\S]*?namespace:/); expect(moduleExportsMatch).not.toBeNull(); // Extract just the module.exports block to avoid matching 'apps:' in comments const exportBlock = content.slice(content.indexOf('module.exports')); const namespaceInExport = exportBlock.indexOf("namespace:"); const appsInExport = exportBlock.indexOf("apps:"); expect(namespaceInExport).toBeGreaterThan(-1); expect(appsInExport).toBeGreaterThan(-1); expect(namespaceInExport).toBeLessThan(appsInExport); }); it('should contain development app definitions', () => { const appNames = config.apps.map((app) => app.name); expect(appNames).toContain('flyer-crawler-api-dev'); expect(appNames).toContain('flyer-crawler-worker-dev'); expect(appNames).toContain('flyer-crawler-vite-dev'); }); }); describe('Namespace Uniqueness', () => { it('should have unique namespaces across all environments', () => { const prodConfig = parseEcosystemConfig('ecosystem.config.cjs'); const testConfig = parseEcosystemConfig('ecosystem-test.config.cjs'); const devConfig = parseEcosystemConfig('ecosystem.dev.config.cjs'); const namespaces = [prodConfig.namespace, testConfig.namespace, devConfig.namespace]; // All should be defined expect(namespaces.every((ns) => ns !== undefined)).toBe(true); // All should be unique const uniqueNamespaces = new Set(namespaces); expect(uniqueNamespaces.size).toBe(3); }); }); }); describe('Workflow Files', () => { describe('deploy-to-test.yml', () => { let workflow: string; beforeAll(() => { workflow = readWorkflowFile('.gitea/workflows/deploy-to-test.yml'); }); it('should exist', () => { expect(fileExists('.gitea/workflows/deploy-to-test.yml')).toBe(true); }); it('should have --namespace flyer-crawler-test on pm2 list commands', () => { // Check for namespace flag in pm2 list commands const pm2ListMatches = workflow.match(/pm2\s+list\s+--namespace\s+flyer-crawler-test/g); expect(pm2ListMatches).not.toBeNull(); expect(pm2ListMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-test on pm2 jlist commands', () => { const pm2JlistMatches = workflow.match(/pm2\s+jlist\s+--namespace\s+flyer-crawler-test/g); expect(pm2JlistMatches).not.toBeNull(); expect(pm2JlistMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-test on pm2 save commands', () => { const pm2SaveMatches = workflow.match(/pm2\s+save\s+--namespace\s+flyer-crawler-test/g); expect(pm2SaveMatches).not.toBeNull(); expect(pm2SaveMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-test on pm2 stop commands', () => { const pm2StopMatches = workflow.match(/pm2\s+stop\s+--namespace\s+flyer-crawler-test/g); expect(pm2StopMatches).not.toBeNull(); expect(pm2StopMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-test on pm2 startOrReload commands', () => { const pm2StartMatches = workflow.match( /pm2\s+startOrReload\s+--namespace\s+flyer-crawler-test/g, ); expect(pm2StartMatches).not.toBeNull(); expect(pm2StartMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-test on pm2 delete commands', () => { const pm2DeleteMatches = workflow.match(/pm2\s+delete\s+--namespace\s+flyer-crawler-test/g); expect(pm2DeleteMatches).not.toBeNull(); expect(pm2DeleteMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-test on pm2 logs commands', () => { const pm2LogsMatches = workflow.match(/pm2\s+logs\s+--namespace\s+flyer-crawler-test/g); expect(pm2LogsMatches).not.toBeNull(); expect(pm2LogsMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-test on pm2 describe commands', () => { const pm2DescribeMatches = workflow.match( /pm2\s+describe\s+--namespace\s+flyer-crawler-test/g, ); expect(pm2DescribeMatches).not.toBeNull(); expect(pm2DescribeMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-test on pm2 env commands', () => { const pm2EnvMatches = workflow.match(/pm2\s+env\s+--namespace\s+flyer-crawler-test/g); expect(pm2EnvMatches).not.toBeNull(); expect(pm2EnvMatches!.length).toBeGreaterThan(0); }); it('should NOT contain pm2 commands without namespace for test processes', () => { const lines = workflow.split('\n'); const problematicLines = lines.filter((line) => { const trimmed = line.trim(); // Skip comments, echo statements (log messages), and inline JS exec/execSync calls if (trimmed.startsWith('#') || trimmed.startsWith('echo ') || trimmed.includes('exec(') || trimmed.includes('execSync(')) return false; // Check for pm2 commands that should have namespace const hasPm2Command = /pm2\s+(save|restart|stop|delete|list|jlist|describe|logs|env|ps)(\s|$)/.test(trimmed); if (!hasPm2Command) return false; // If it has a pm2 command, it should include --namespace return !trimmed.includes('--namespace'); }); if (problematicLines.length > 0) { console.log('Lines missing --namespace:', problematicLines); } expect(problematicLines.length).toBe(0); }); }); describe('deploy-to-prod.yml', () => { let workflow: string; beforeAll(() => { workflow = readWorkflowFile('.gitea/workflows/deploy-to-prod.yml'); }); it('should exist', () => { expect(fileExists('.gitea/workflows/deploy-to-prod.yml')).toBe(true); }); it('should have --namespace flyer-crawler-prod on pm2 list commands', () => { const pm2ListMatches = workflow.match(/pm2\s+list\s+--namespace\s+flyer-crawler-prod/g); expect(pm2ListMatches).not.toBeNull(); expect(pm2ListMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-prod on pm2 jlist commands', () => { const pm2JlistMatches = workflow.match(/pm2\s+jlist\s+--namespace\s+flyer-crawler-prod/g); expect(pm2JlistMatches).not.toBeNull(); expect(pm2JlistMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-prod on pm2 save commands', () => { const pm2SaveMatches = workflow.match(/pm2\s+save\s+--namespace\s+flyer-crawler-prod/g); expect(pm2SaveMatches).not.toBeNull(); expect(pm2SaveMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-prod on pm2 stop commands', () => { const pm2StopMatches = workflow.match(/pm2\s+stop\s+.*--namespace\s+flyer-crawler-prod/g); expect(pm2StopMatches).not.toBeNull(); expect(pm2StopMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-prod on pm2 startOrReload commands', () => { const pm2StartMatches = workflow.match( /pm2\s+startOrReload\s+.*--namespace\s+flyer-crawler-prod/g, ); expect(pm2StartMatches).not.toBeNull(); expect(pm2StartMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-prod on pm2 logs commands', () => { const pm2LogsMatches = workflow.match(/pm2\s+logs\s+.*--namespace\s+flyer-crawler-prod/g); expect(pm2LogsMatches).not.toBeNull(); expect(pm2LogsMatches!.length).toBeGreaterThan(0); }); it('should have --namespace flyer-crawler-prod on pm2 describe commands', () => { const pm2DescribeMatches = workflow.match( /pm2\s+describe\s+.*--namespace\s+flyer-crawler-prod/g, ); expect(pm2DescribeMatches).not.toBeNull(); expect(pm2DescribeMatches!.length).toBeGreaterThan(0); }); }); describe('restart-pm2.yml', () => { let workflow: string; beforeAll(() => { workflow = readWorkflowFile('.gitea/workflows/restart-pm2.yml'); }); it('should exist', () => { expect(fileExists('.gitea/workflows/restart-pm2.yml')).toBe(true); }); it('should have namespace flags for test environment operations', () => { const testNamespaceMatches = workflow.match(/--namespace\s+flyer-crawler-test/g); expect(testNamespaceMatches).not.toBeNull(); expect(testNamespaceMatches!.length).toBeGreaterThan(0); }); it('should have namespace flags for production environment operations', () => { const prodNamespaceMatches = workflow.match(/--namespace\s+flyer-crawler-prod/g); expect(prodNamespaceMatches).not.toBeNull(); expect(prodNamespaceMatches!.length).toBeGreaterThan(0); }); it('should support environment selection input', () => { expect(workflow).toContain('environment:'); expect(workflow).toContain("type: choice"); expect(workflow).toContain('test'); expect(workflow).toContain('production'); expect(workflow).toContain('both'); }); it('should use conditional logic for environment selection', () => { // Check for test environment conditional expect(workflow).toMatch(/if:.*environment.*==.*['"]test['"]/); // Check for production environment conditional expect(workflow).toMatch(/if:.*environment.*==.*['"]production['"]/); }); }); describe('pm2-diagnostics.yml', () => { let workflow: string; beforeAll(() => { workflow = readWorkflowFile('.gitea/workflows/pm2-diagnostics.yml'); }); it('should exist', () => { expect(fileExists('.gitea/workflows/pm2-diagnostics.yml')).toBe(true); }); it('should show both production and test namespaces', () => { expect(workflow).toContain('flyer-crawler-prod'); expect(workflow).toContain('flyer-crawler-test'); }); it('should have sections for production namespace diagnostics', () => { expect(workflow).toContain('Production Namespace'); expect(workflow).toContain('pm2 list --namespace flyer-crawler-prod'); expect(workflow).toContain('pm2 jlist --namespace flyer-crawler-prod'); }); it('should have sections for test namespace diagnostics', () => { expect(workflow).toContain('Test Namespace'); expect(workflow).toContain('pm2 list --namespace flyer-crawler-test'); expect(workflow).toContain('pm2 jlist --namespace flyer-crawler-test'); }); }); describe('Manual Workflows', () => { it('manual-deploy-major.yml should use correct production namespace', () => { if (fileExists('.gitea/workflows/manual-deploy-major.yml')) { const workflow = readWorkflowFile('.gitea/workflows/manual-deploy-major.yml'); // If it has PM2 commands, they should use the production namespace if (workflow.includes('pm2')) { expect(workflow).toContain('flyer-crawler-prod'); } } }); }); describe('PM2 Save Namespace Validation (All Workflows)', () => { // Pattern to detect pm2 save WITHOUT namespace flag // Uses negative lookahead to ensure --namespace follows pm2 save const pm2SaveWithoutNamespace = /pm2\s+save(?!\s+--namespace)/; // All workflow files that may contain pm2 save commands const workflowFiles = [ '.gitea/workflows/deploy-to-prod.yml', '.gitea/workflows/deploy-to-test.yml', '.gitea/workflows/restart-pm2.yml', '.gitea/workflows/manual-db-restore.yml', '.gitea/workflows/manual-deploy-major.yml', ]; it('should have --namespace on ALL pm2 save commands across all workflow files', () => { const violations: Array<{ file: string; line: number; content: string }> = []; for (const workflowPath of workflowFiles) { if (!fileExists(workflowPath)) { continue; } const workflow = readWorkflowFile(workflowPath); const lines = workflow.split('\n'); lines.forEach((line, index) => { const trimmed = line.trim(); // Skip comments if (trimmed.startsWith('#')) { return; } // Skip echo statements (log messages that mention pm2 save are not commands) if (trimmed.startsWith('echo ')) { return; } // Check if line contains pm2 save without namespace if (pm2SaveWithoutNamespace.test(trimmed)) { violations.push({ file: workflowPath, line: index + 1, content: trimmed, }); } }); } if (violations.length > 0) { console.log('\n=== PM2 SAVE COMMANDS MISSING --namespace FLAG ==='); violations.forEach((v) => { console.log(` ${v.file}:${v.line}`); console.log(` ${v.content}`); }); console.log('===================================================\n'); } expect(violations).toHaveLength(0); }); // Individual file checks for clarity in test output for (const workflowPath of workflowFiles) { it(`${workflowPath} should have --namespace on all pm2 save commands`, () => { if (!fileExists(workflowPath)) { // File doesn't exist, skip this test return; } const workflow = readWorkflowFile(workflowPath); const lines = workflow.split('\n'); const violatingLines: string[] = []; lines.forEach((line) => { const trimmed = line.trim(); // Skip comments and echo statements (log messages mentioning pm2 save) if (trimmed.startsWith('#') || trimmed.startsWith('echo ')) { return; } if (pm2SaveWithoutNamespace.test(trimmed)) { violatingLines.push(trimmed); } }); if (violatingLines.length > 0) { console.log(`Violations in ${workflowPath}:`, violatingLines); } expect(violatingLines).toHaveLength(0); }); } }); }); describe('Migration Script', () => { const scriptPath = 'scripts/migrate-pm2-namespaces.sh'; let scriptContent: string; beforeAll(() => { if (fileExists(scriptPath)) { scriptContent = readFile(scriptPath); } }); it('should exist', () => { expect(fileExists(scriptPath)).toBe(true); }); it('should have --dry-run option', () => { expect(scriptContent).toContain('--dry-run'); expect(scriptContent).toContain('DRY_RUN'); }); it('should have --test-only option', () => { expect(scriptContent).toContain('--test-only'); expect(scriptContent).toContain('TEST_ONLY'); }); it('should have --prod-only option', () => { expect(scriptContent).toContain('--prod-only'); expect(scriptContent).toContain('PROD_ONLY'); }); it('should define correct namespace constants', () => { expect(scriptContent).toContain('PROD_NAMESPACE="flyer-crawler-prod"'); expect(scriptContent).toContain('TEST_NAMESPACE="flyer-crawler-test"'); }); it('should define correct process names', () => { // Production processes expect(scriptContent).toContain('flyer-crawler-api'); expect(scriptContent).toContain('flyer-crawler-worker'); expect(scriptContent).toContain('flyer-crawler-analytics-worker'); // Test processes expect(scriptContent).toContain('flyer-crawler-api-test'); expect(scriptContent).toContain('flyer-crawler-worker-test'); expect(scriptContent).toContain('flyer-crawler-analytics-worker-test'); }); it('should contain rollback instructions function', () => { expect(scriptContent).toContain('show_rollback_instructions'); expect(scriptContent).toContain('ROLLBACK INSTRUCTIONS'); }); it('should have health check functionality', () => { expect(scriptContent).toContain('check_health'); expect(scriptContent).toContain('/api/health'); }); it('should have verification step', () => { expect(scriptContent).toContain('verify_migration'); }); it('should be idempotent (check if already migrated)', () => { // Should check if namespace already has processes expect(scriptContent).toContain('namespace_has_processes'); expect(scriptContent).toContain('no migration needed'); }); it('should have proper shebang and error handling', () => { expect(scriptContent.startsWith('#!/bin/bash')).toBe(true); expect(scriptContent).toContain('set -euo pipefail'); }); it('should use pm2 save with namespace after operations', () => { expect(scriptContent).toContain('pm2 save --namespace'); }); it('should have help option', () => { expect(scriptContent).toContain('--help'); expect(scriptContent).toContain('-h)'); }); }); describe('Documentation', () => { describe('ADR-063', () => { const adrPath = 'docs/adr/0063-pm2-namespace-implementation.md'; let adrContent: string; beforeAll(() => { if (fileExists(adrPath)) { adrContent = readFile(adrPath); } }); it('should exist', () => { expect(fileExists(adrPath)).toBe(true); }); it('should have proper ADR structure with Status section', () => { expect(adrContent).toContain('## Status'); expect(adrContent).toContain('Accepted'); }); it('should have Context section', () => { expect(adrContent).toContain('## Context'); }); it('should have Decision section', () => { expect(adrContent).toContain('## Decision'); }); it('should have Consequences section', () => { expect(adrContent).toContain('## Consequences'); }); it('should document all three namespaces', () => { expect(adrContent).toContain('flyer-crawler-prod'); expect(adrContent).toContain('flyer-crawler-test'); expect(adrContent).toContain('flyer-crawler-dev'); }); it('should document namespace at module.exports level', () => { expect(adrContent).toContain('module.exports'); expect(adrContent).toContain('namespace:'); }); it('should reference the ecosystem config files', () => { expect(adrContent).toContain('ecosystem.config.cjs'); expect(adrContent).toContain('ecosystem-test.config.cjs'); expect(adrContent).toContain('ecosystem.dev.config.cjs'); }); it('should document workflow command pattern with --namespace flag', () => { expect(adrContent).toContain('--namespace'); }); it('should reference related ADRs', () => { expect(adrContent).toContain('ADR-061'); }); it('should have Files Modified section', () => { expect(adrContent).toContain('## Files Modified'); }); it('should have Verification section', () => { expect(adrContent).toContain('## Verification'); }); }); describe('CLAUDE.md', () => { const claudeMdPath = 'CLAUDE.md'; let claudeMdContent: string; beforeAll(() => { if (fileExists(claudeMdPath)) { claudeMdContent = readFile(claudeMdPath); } }); it('should exist', () => { expect(fileExists(claudeMdPath)).toBe(true); }); it('should reference ADR-063 for PM2 namespaces', () => { expect(claudeMdContent).toContain('ADR-063'); expect(claudeMdContent).toContain('0063-pm2-namespace-implementation'); }); it('should document namespace isolation section', () => { expect(claudeMdContent).toContain('PM2 Namespace Isolation'); }); it('should document all three namespaces', () => { expect(claudeMdContent).toContain('flyer-crawler-prod'); expect(claudeMdContent).toContain('flyer-crawler-test'); expect(claudeMdContent).toContain('flyer-crawler-dev'); }); it('should show correct namespace examples', () => { // Check for correct usage examples expect(claudeMdContent).toContain('pm2 start ecosystem.config.cjs --namespace flyer-crawler-prod'); expect(claudeMdContent).toContain('pm2 start ecosystem-test.config.cjs --namespace flyer-crawler-test'); }); it('should warn against using pm2 commands without namespace', () => { // Check for warning about dangerous commands expect(claudeMdContent).toContain('NEVER'); expect(claudeMdContent).toMatch(/pm2\s+(stop|delete|restart)\s+all/); }); it('should document pm2 save requirement with namespace', () => { expect(claudeMdContent).toContain('pm2 save'); expect(claudeMdContent).toContain('--namespace'); }); it('should have correct ecosystem config file references', () => { expect(claudeMdContent).toContain('ecosystem.config.cjs'); expect(claudeMdContent).toContain('ecosystem-test.config.cjs'); expect(claudeMdContent).toContain('ecosystem.dev.config.cjs'); }); it('should NOT reference ADR-067 for PM2 namespaces (wrong number)', () => { // If ADR-067 is mentioned in context of PM2 namespaces, it would be incorrect // ADR-063 is the correct one const pm2Section = claudeMdContent.slice( claudeMdContent.indexOf('PM2 Namespace'), claudeMdContent.indexOf('PM2 Namespace') + 2000, ); // The PM2 namespace section should reference 063, not 067 if (pm2Section.includes('ADR-')) { expect(pm2Section).toContain('ADR-063'); } }); }); describe('Cross-Reference Validation', () => { it('should have consistent namespace names across all documentation', () => { const adrContent = readFile('docs/adr/0063-pm2-namespace-implementation.md'); const claudeMdContent = readFile('CLAUDE.md'); // All three should mention the same namespaces const namespaces = ['flyer-crawler-prod', 'flyer-crawler-test', 'flyer-crawler-dev']; for (const ns of namespaces) { expect(adrContent).toContain(ns); expect(claudeMdContent).toContain(ns); } }); it('should have consistent config file names across all documentation', () => { const adrContent = readFile('docs/adr/0063-pm2-namespace-implementation.md'); const claudeMdContent = readFile('CLAUDE.md'); const configFiles = [ 'ecosystem.config.cjs', 'ecosystem-test.config.cjs', 'ecosystem.dev.config.cjs', ]; for (const file of configFiles) { expect(adrContent).toContain(file); expect(claudeMdContent).toContain(file); } }); }); }); describe('End-to-End Consistency', () => { it('should have matching namespaces between configs and workflows', () => { const prodConfig = parseEcosystemConfig('ecosystem.config.cjs'); const testConfig = parseEcosystemConfig('ecosystem-test.config.cjs'); const devConfig = parseEcosystemConfig('ecosystem.dev.config.cjs'); const deployProdWorkflow = readWorkflowFile('.gitea/workflows/deploy-to-prod.yml'); const deployTestWorkflow = readWorkflowFile('.gitea/workflows/deploy-to-test.yml'); // Production namespace should match expect(prodConfig.namespace).toBe('flyer-crawler-prod'); expect(deployProdWorkflow).toContain('--namespace flyer-crawler-prod'); // Test namespace should match expect(testConfig.namespace).toBe('flyer-crawler-test'); expect(deployTestWorkflow).toContain('--namespace flyer-crawler-test'); // Dev namespace should be defined expect(devConfig.namespace).toBe('flyer-crawler-dev'); }); it('should have namespace flags in all PM2-related workflow steps', () => { const testWorkflow = readWorkflowFile('.gitea/workflows/deploy-to-test.yml'); const prodWorkflow = readWorkflowFile('.gitea/workflows/deploy-to-prod.yml'); // Count PM2 commands vs PM2 commands with namespace // In properly migrated workflows, most PM2 commands should have namespace const testPm2Commands = testWorkflow.match(/pm2\s+(list|jlist|save|stop|start|delete|restart|logs|describe|env|startOrReload)/g) || []; const testNamespacedCommands = testWorkflow.match(/pm2\s+\w+.*--namespace/g) || []; // Most PM2 commands should have namespace (allow some slack for inline JS) const testRatio = testNamespacedCommands.length / testPm2Commands.length; expect(testRatio).toBeGreaterThan(0.5); // At least 50% should have namespace const prodPm2Commands = prodWorkflow.match(/pm2\s+(list|jlist|save|stop|start|delete|restart|logs|describe|env|startOrReload)/g) || []; const prodNamespacedCommands = prodWorkflow.match(/pm2\s+\w+.*--namespace/g) || []; const prodRatio = prodNamespacedCommands.length / prodPm2Commands.length; expect(prodRatio).toBeGreaterThan(0.5); }); it('should have separate dump files per namespace after migration (documented)', () => { const adrContent = readFile('docs/adr/0063-pm2-namespace-implementation.md'); // ADR should document the dump file isolation expect(adrContent).toContain('dump'); expect(adrContent).toContain('namespace'); }); }); });