/** * config_validation.test.mjs — Validate build configuration integrity. * * Prevents regressions like the phantom entry module issue that kfj001 * hit: vite.config.ts referenced probeFn.js and scrapeFn.js but those * files were never created. */ import { describe, it } from './runner.mjs'; import { readFileSync, existsSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, '..'); describe('Config: build integrity', () => { it('vite.config.ts entry modules must exist as source files', () => { const viteConfig = readFileSync(resolve(root, 'vite.config.ts'), 'utf8'); const matches = viteConfig.matchAll(/resolve\(__dirname,\s*'([^']+)'\)/g); const inputEntries = [...matches] .map(m => m[1]) .filter(p => p.startsWith('src/')); const missing = inputEntries.filter(p => !existsSync(resolve(root, p))); if (missing.length > 0) { throw new Error( `vite.config.ts references ${missing.length} missing file(s): ${missing.join(', ')}` ); } if (inputEntries.length === 0) { throw new Error('No entry points found in vite.config.ts'); } }); it('content.js must NOT use inline script injection (textContent)', () => { const contentJs = readFileSync(resolve(root, 'src/content/content.js'), 'utf8'); // Strip comments before checking for textContent usage const codeOnly = contentJs .replace(/\/\*[\s\S]*?\*\//g, '') // block comments .replace(/\/\/.*$/gm, ''); // line comments if (codeOnly.includes('textContent')) { throw new Error( 'content.js uses inline script injection (textContent). ' + 'Some sites block inline scripts via CSP. Use script.src instead.' ); } if (!codeOnly.includes('script.src') || !codeOnly.includes('chrome.runtime.getURL')) { throw new Error( 'content.js does not use CSP-safe script.src = chrome.runtime.getURL(...)' ); } }); it('interceptors.js must be a standalone file', () => { const path = resolve(root, 'src/inject/interceptors.js'); if (!existsSync(path)) { throw new Error('src/inject/interceptors.js does not exist'); } const content = readFileSync(path, 'utf8'); if (!content.includes('(function ()') || !content.includes('})();')) { throw new Error('interceptors.js should be a self-contained IIFE'); } }); it('manifest.json content_scripts must reference content.js', () => { const manifest = JSON.parse( readFileSync(resolve(root, 'manifest.json'), 'utf8') ); const cs = manifest.content_scripts; if (!cs || !Array.isArray(cs)) { throw new Error('manifest.json has no content_scripts'); } const hasContentScript = cs.some(s => s.js && s.js.includes('content.js')); if (!hasContentScript) { throw new Error('No content_scripts entry references content.js'); } }); it('manifest.json must declare interceptors.js in web_accessible_resources', () => { const manifest = JSON.parse( readFileSync(resolve(root, 'manifest.json'), 'utf8') ); const war = manifest.web_accessible_resources; if (!war || !Array.isArray(war)) { throw new Error( 'manifest.json has no web_accessible_resources. ' + 'interceptors.js needs this for chrome.runtime.getURL() to work.' ); } const allResources = war.flatMap(entry => entry.resources || []); if (!allResources.includes('interceptors.js')) { throw new Error( 'manifest.json web_accessible_resources must include interceptors.js. ' + 'Without it, the browser blocks chrome.runtime.getURL("interceptors.js").' ); } }); });