required-files.test.js
1 /** 2 * Tests that all non-code data files required at runtime are present. 3 * 4 * If this test fails after a commit, it means a file the pipeline depends on 5 * was accidentally deleted. Restore it from git history. 6 * 7 * The manifest (REQUIRED_FILES below) is the single source of truth. 8 * A pre-commit hook auto-adds new data/, prompts/, and docs/ files to it. 9 */ 10 11 import { test, describe } from 'node:test'; 12 import assert from 'node:assert/strict'; 13 import { existsSync, readdirSync, lstatSync } from 'fs'; 14 import { join } from 'path'; 15 16 const ROOT = join(import.meta.dirname, '..', '..'); 17 18 // ── Countries defined in src/config/countries.js ──────────────────────────── 19 // Lowercase versions used for data/{cc}/ and data/franchises/{cc}.txt 20 const COUNTRIES = [ 21 'at', 'au', 'be', 'ca', 'ch', 'cn', 'de', 'dk', 'es', 'fr', 22 'id', 'ie', 'in', 'it', 'jp', 'kr', 'mx', 'nl', 'no', 'nz', 23 'pl', 'se', 'sg', 'uk', 'us', 24 ]; 25 26 // Uppercase versions used for data/templates/{CC}/ 27 const TEMPLATE_COUNTRIES_UPPER = [ 28 'AT', 'AU', 'BE', 'CA', 'CH', 'CN', 'DE', 'DK', 'ES', 'FR', 29 'GB', 'ID', 'IE', 'IN', 'IT', 'JP', 'KR', 'MX', 'NL', 'NO', 30 'NZ', 'PL', 'SE', 'US', 'ZA', 31 ]; 32 33 // Countries with native-script keyword files (loaded by keyword-manager.js) 34 const NATIVE_SCRIPT_COUNTRIES = ['cn', 'jp', 'kr']; 35 36 // ── Required files manifest ───────────────────────────────────────────────── 37 // Each entry: relative path from project root. 38 // Grouped by category for readability; the test iterates the flat list. 39 40 const REQUIRED_FILES = [ 41 // ── Prompts (loaded via readFileSync at module-init or by orchestrator) ── 42 'prompts/AUDIT-REPORT-SCORING.md', 43 'prompts/CONTACT-EXTRACTION.md', 44 'prompts/CONVERSION-RESCORING.md', 45 'prompts/CONVERSION-RESCORING-VISION.md', 46 'prompts/CONVERSION-SCORING-NOVIS.md', 47 'prompts/CONVERSION-SCORING-VISION.md', 48 'prompts/ENRICHMENT.md', 49 'prompts/ENRICHMENT-VISION.md', 50 'prompts/EVIDENCE-COLLECT.md', 51 'prompts/EVIDENCE-MERGE.md', 52 'prompts/FORM-CLASSIFY-FIELDS.md', 53 'prompts/FORM-GUESS-VALUES.md', 54 'prompts/FORM-SELECT-BEST.md', 55 'prompts/HAIKU-ANALYZE.md', 56 'prompts/HAIKU-POLISH.md', 57 'prompts/NAME-EXTRACTOR.md', 58 'prompts/PROOFREAD.md', 59 'prompts/PROPOSAL.md', 60 'prompts/REPLIES.md', 61 'prompts/VISION.md', 62 'prompts/autoresponder.md', 63 'prompts/autoresponder-2step.md', 64 65 // ── Agent prompts (loaded by orchestrator run_checked_gated) ───────────── 66 'prompts/agents/CHECK-DOCS.md', 67 'prompts/agents/CODE-REVIEW.md', 68 'prompts/agents/MONITOR-HEALTH.md', 69 'prompts/agents/TRIAGE-ERRORS.md', 70 71 // ── Compliance data (loaded at module init) ────────────────────────────── 72 'data/compliance/requirements.json', 73 'data/compliance/blocked-channels.json', 74 'data/compliance/paused-languages.json', 75 76 // ── Form / captcha data ────────────────────────────────────────────────── 77 'data/form-builder-templates.json', 78 'data/field-label-corrections.json', 79 'data/captcha-provider-benchmark.json', 80 81 // ── Best-practices docs (loaded by orchestrator as context files) ──────── 82 'docs/05-outreach/email-best-practices.md', 83 'docs/05-outreach/sms-best-practices.md', 84 85 // ── DB schema ──────────────────────────────────────────────────────────── 86 'db/schema.sql', 87 88 // ── Per-country keyword CSVs (loaded by keyword-manager.js) ────────────── 89 ...COUNTRIES.flatMap(cc => [ 90 `data/${cc}/businesses-final-filtered.csv`, 91 `data/${cc}/regions-final-filtered.csv`, 92 ]), 93 94 // ── Native-script keyword files (JP, CN, KR only) ─────────────────────── 95 ...NATIVE_SCRIPT_COUNTRIES.flatMap(cc => [ 96 `data/${cc}/businesses-native.txt`, 97 `data/${cc}/regions-native.txt`, 98 ]), 99 100 // ── Franchise blocklists (loaded by site-filters.js) ───────────────────── 101 ...COUNTRIES.map(cc => `data/franchises/${cc}.txt`), 102 103 // ── Per-country outreach templates (email.json + sms.json minimum) ─────── 104 ...TEMPLATE_COUNTRIES_UPPER.flatMap(cc => { 105 // Detect lang-subfolder vs flat layout 106 const ccDir = join(ROOT, 'data', 'templates', cc); 107 if (!existsSync(ccDir)) return [`data/templates/${cc}/email.json`, `data/templates/${cc}/sms.json`]; 108 const entries = readdirSync(ccDir); 109 const hasLangDir = entries.some(e => { 110 const full = join(ccDir, e); 111 return lstatSync(full).isDirectory(); 112 }); 113 if (hasLangDir) { 114 // Return lang-specific paths for each language subfolder 115 return entries 116 .filter(e => lstatSync(join(ccDir, e)).isDirectory()) 117 .flatMap(lang => [ 118 `data/templates/${cc}/${lang}/email.json`, 119 `data/templates/${cc}/${lang}/sms.json`, 120 ]); 121 } 122 return [`data/templates/${cc}/email.json`, `data/templates/${cc}/sms.json`]; 123 }), 124 125 // UK symlink → GB 126 'data/templates/UK', 127 ]; 128 129 // ── Tests ─────────────────────────────────────────────────────────────────── 130 131 describe('required data files', () => { 132 const missing = []; 133 134 test('all required files exist', () => { 135 for (const rel of REQUIRED_FILES) { 136 const abs = join(ROOT, rel); 137 if (!existsSync(abs)) { 138 missing.push(rel); 139 } 140 } 141 if (missing.length > 0) { 142 assert.fail( 143 `${missing.length} required file(s) missing:\n${ 144 missing.map(f => ` - ${f}`).join('\n') 145 }\n\nRestore from git: git checkout HEAD~1 -- <path>` 146 ); 147 } 148 }); 149 150 test('UK template symlink points to GB', () => { 151 const ukPath = join(ROOT, 'data', 'templates', 'UK'); 152 assert.ok(existsSync(ukPath), 'data/templates/UK should exist'); 153 assert.ok(lstatSync(ukPath).isSymbolicLink(), 'data/templates/UK should be a symlink'); 154 }); 155 });