diff --git a/eslint.config.js b/eslint.config.js index 14a692a..92cbf7e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,7 +20,6 @@ module.exports = [ 'dist/**', 'build/**', 'coverage/**', - 'scripts/**', 'src/renderer/bundle.js', 'src/renderer/bundle.js.map', 'src/renderer/bundle.js.LICENSE.txt', @@ -31,6 +30,36 @@ module.exports = [ ], }, js.configs.recommended, + { + files: [ + 'scripts/**/*.js', + '*.config.js', + 'eslint.config.js', + '.eslintrc.js', + '.babelrc.js', + 'tests/.eslintrc.js', + ], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'commonjs', + globals: { + ...globals.node, + }, + }, + rules: { + 'no-unused-vars': 'off', + 'no-case-declarations': 'off', + 'no-useless-escape': 'off', + }, + }, + { + files: ['scripts/capture-ui-screenshot.js'], + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, { files: ['src/**/*.{js,jsx,ts,tsx}', 'tests/**/*.{js,jsx,ts,tsx}'], languageOptions: { @@ -58,7 +87,16 @@ module.exports = [ 'import/order': [ 'error', { - groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], 'newlines-between': 'always', alphabetize: { order: 'asc', caseInsensitive: true }, }, diff --git a/package.json b/package.json index ee47d6e..0e5c8e1 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,14 @@ "predev": "npm run build:ts && node scripts/clean-dev-assets.js", "dev": "node scripts/index.js dev", "clear-assets": "rimraf src/renderer/bundle.js src/renderer/bundle.js.map src/renderer/bundle.js.LICENSE.txt src/renderer/output.css", - "lint": "npm run format:check && eslint src tests --cache --max-warnings 0 && npm run lint:md && npm run test:catalog && npm run changelog:validate", + "lint": "npm run format:check && eslint src tests scripts eslint.config.js .eslintrc.js .babelrc.js babel.config.js jest.config.js playwright.config.ts postcss.config.js prettier.config.js tailwind.config.js webpack.config.js --cache --max-warnings 0 && npm run lint:md && npm run test:catalog && npm run changelog:validate", "lint:md": "npm run lint:md:links && npm run lint:md:style", "lint:md:links": "node scripts/lint-markdown-links.js", "lint:md:style": "markdownlint \"**/*.{md,mdx}\" --config .markdownlint.json --ignore node_modules --ignore dist", "changelog:validate": "node scripts/validate-changelog.js", "lint:tests": "eslint tests --cache --max-warnings 0", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,html,css}\"", - "format:check": "prettier --check --end-of-line auto \"**/*.{json,md,html,css}\"", + "format:check": "prettier --check --end-of-line auto \"**/*.{json,md,html,css}\" && prettier --check --end-of-line auto \"scripts/**/*.js\" \"*.config.js\" \"eslint.config.js\" \".eslintrc.js\" \".babelrc.js\" \"jest.config.js\" \"postcss.config.js\" \"prettier.config.js\" \"tailwind.config.js\" \"webpack.config.js\" \"playwright.config.ts\"", "test": "jest --config jest.config.js --passWithNoTests", "test:watch": "jest --watch --config jest.config.js --passWithNoTests", "test:stress": "jest --config jest.config.js --runInBand --testMatch=\"**/tests/stress/**/*.{js,jsx,ts,tsx}\" --verbose", @@ -77,7 +77,7 @@ "*.{json,md,html,css}": [ "prettier --write" ], - "{src,tests}/**/*.{js,jsx,ts,tsx}": [ + "{src,tests,scripts}/**/*.{js,jsx,ts,tsx}": [ "eslint --fix" ] }, diff --git a/scripts/audit-actions-freshness.js b/scripts/audit-actions-freshness.js index d292bc8..8c0ce50 100644 --- a/scripts/audit-actions-freshness.js +++ b/scripts/audit-actions-freshness.js @@ -121,8 +121,7 @@ function readWorkflowFiles(workflowDirectory) { const entries = fs .readdirSync(directoryPath, { withFileTypes: true }) .filter( - (entry) => - entry.isFile() && (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml')) + (entry) => entry.isFile() && (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml')) ) .map((entry) => entry.name) .sort((left, right) => left.localeCompare(right)); @@ -203,7 +202,9 @@ async function githubRequest({ endpoint, token, method = 'GET', body = null }) { if (!response.ok) { const detail = data && typeof data === 'object' && data.message ? data.message : responseText; - const error = new Error(`GitHub API ${method} ${endpoint} failed (${response.status}): ${detail}`); + const error = new Error( + `GitHub API ${method} ${endpoint} failed (${response.status}): ${detail}` + ); error.status = response.status; throw error; } @@ -406,7 +407,8 @@ async function ensureTrackingPullRequestBranch({ endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/git/ref/heads/${toGitHubRefPath(defaultBranch)}`, token, }); - const defaultBranchSha = defaultBranchRef && defaultBranchRef.object ? defaultBranchRef.object.sha : ''; + const defaultBranchSha = + defaultBranchRef && defaultBranchRef.object ? defaultBranchRef.object.sha : ''; if (!defaultBranchSha) { throw new Error(`Could not resolve latest commit on ${defaultBranch}.`); } diff --git a/scripts/capture-ui-screenshot.js b/scripts/capture-ui-screenshot.js index 7b967ab..c827476 100644 --- a/scripts/capture-ui-screenshot.js +++ b/scripts/capture-ui-screenshot.js @@ -14,7 +14,13 @@ const DEFAULT_SCREENSHOT_NAME = `ui-${process.platform}-${process.arch}.png`; const FIXED_MTIME = 1700000000000; function loadSecretScannerHelpers() { - const compiledSecretScannerPath = path.join(ROOT_DIR, 'build', 'ts', 'utils', 'secret-scanner.js'); + const compiledSecretScannerPath = path.join( + ROOT_DIR, + 'build', + 'ts', + 'utils', + 'secret-scanner.js' + ); try { return require(compiledSecretScannerPath); @@ -49,7 +55,10 @@ function sanitizeScreenshotName(nameCandidate) { typeof nameCandidate === 'string' && nameCandidate.trim() ? nameCandidate.trim() : DEFAULT_SCREENSHOT_NAME; - const baseName = path.basename(rawName).replace(/[^a-zA-Z0-9._-]/g, '-').replace(/^\.+/, ''); + const baseName = path + .basename(rawName) + .replace(/[^a-zA-Z0-9._-]/g, '-') + .replace(/^\.+/, ''); const withExtension = baseName.toLowerCase().endsWith('.png') ? baseName : `${baseName}.png`; if (!withExtension || withExtension === '.png') { @@ -361,7 +370,8 @@ async function setupMockElectronApi(page) { localStorage.setItem('configContent', mockConfig); const cloneTree = (treeItems) => JSON.parse(JSON.stringify(treeItems)); - const delay = (durationMs) => new Promise((resolve) => window.setTimeout(resolve, durationMs)); + const delay = (durationMs) => + new Promise((resolve) => window.setTimeout(resolve, durationMs)); window.electronAPI = { getDefaultConfig: async () => mockConfig, @@ -371,14 +381,19 @@ async function setupMockElectronApi(page) { typeof configContent === 'string' && configContent.trim() ? configContent : localStorage.getItem('configContent') || ''; - const excludeSensitiveFiles = !/(^|\n)\s*enable_secret_scanning\s*:\s*false\b/i.test( - activeConfig - ) && !/(^|\n)\s*exclude_suspicious_files\s*:\s*false\b/i.test(activeConfig); + const configLines = activeConfig + .split('\n') + .map((line) => line.trim().toLowerCase().replaceAll(' ', '').replaceAll('\t', '')); + const hasSecretScanningDisabled = configLines.includes('enable_secret_scanning:false'); + const hasSuspiciousFilesDisabled = configLines.includes('exclude_suspicious_files:false'); + const excludeSensitiveFiles = !hasSecretScanningDisabled && !hasSuspiciousFilesDisabled; const tree = excludeSensitiveFiles ? mockFilteredDirectoryTree : mockDirectoryTree; return cloneTree(tree); }, analyzeRepository: async (options = {}) => { - const selectedFilePaths = Array.isArray(options?.selectedFiles) ? options.selectedFiles : []; + const selectedFilePaths = Array.isArray(options?.selectedFiles) + ? options.selectedFiles + : []; const filesInfo = selectedFilePaths.map((filePath, index) => { const normalizedPath = String(filePath); const relativePath = normalizedPath.startsWith(`${mockRootPath}/`) @@ -402,7 +417,9 @@ async function setupMockElectronApi(page) { const filesInfo = inputFilesInfo.map((file, index) => ({ path: String(file?.path || `src/file-${index + 1}.ts`), tokens: - Number.isFinite(file?.tokens) && Number(file.tokens) > 0 ? Number(file.tokens) : 120 * (index + 1), + Number.isFinite(file?.tokens) && Number(file.tokens) > 0 + ? Number(file.tokens) + : 120 * (index + 1), isBinary: false, })); const totalTokens = filesInfo.reduce((sum, file) => sum + file.tokens, 0); @@ -422,7 +439,8 @@ async function setupMockElectronApi(page) { '# Repository Analysis', '', ...filesInfo.map( - (file) => `## ${file.path}\n\n\`\`\`ts\n// Preview for ${file.path}\n\`\`\`\nTokens: ${file.tokens}\n` + (file) => + `## ${file.path}\n\n\`\`\`ts\n// Preview for ${file.path}\n\`\`\`\nTokens: ${file.tokens}\n` ), '--END--', ].join('\n'); @@ -516,7 +534,10 @@ async function captureAppStateScreenshots(page) { }); await runStep('Verify secret files are hidden by default', async () => { - await page.waitForFunction((selector) => !document.querySelector(selector), UI_SELECTORS.secretFileEntry); + await page.waitForFunction( + (selector) => !document.querySelector(selector), + UI_SELECTORS.secretFileEntry + ); }); await runStep('Capture source tab screenshot', async () => { diff --git a/scripts/lib/actions-freshness.js b/scripts/lib/actions-freshness.js index cf7f478..047ba82 100644 --- a/scripts/lib/actions-freshness.js +++ b/scripts/lib/actions-freshness.js @@ -31,12 +31,9 @@ function extractUsesValue(line) { return ''; } - const withoutPrefix = normalized - .slice(USES_LINE_PATTERN.length) - .trimStart(); + const withoutPrefix = normalized.slice(USES_LINE_PATTERN.length).trimStart(); const commentStart = withoutPrefix.search(/\s#/); - const rawValue = - commentStart >= 0 ? withoutPrefix.slice(0, commentStart) : withoutPrefix; + const rawValue = commentStart >= 0 ? withoutPrefix.slice(0, commentStart) : withoutPrefix; return normalizeReferenceValue(rawValue.trim()); } @@ -131,10 +128,7 @@ function escapeMarkdownTableCell(value) { return ''; } - return String(value) - .replace(/\\/g, '\\\\') - .replace(/\r?\n/g, '
') - .replace(/\|/g, '\\|'); + return String(value).replace(/\\/g, '\\\\').replace(/\r?\n/g, '
').replace(/\|/g, '\\|'); } function buildMarkdownReport(report) { @@ -202,7 +196,9 @@ function buildMarkdownReport(report) { } if (report.staleCount === 0 && report.resolutionErrors.length === 0) { - lines.push('All pinned GitHub Actions references are current against latest upstream releases.'); + lines.push( + 'All pinned GitHub Actions references are current against latest upstream releases.' + ); lines.push(''); } diff --git a/scripts/lib/security.js b/scripts/lib/security.js index 2e8bddc..c21d7e0 100755 --- a/scripts/lib/security.js +++ b/scripts/lib/security.js @@ -99,7 +99,9 @@ function resolveCommand(command, localCandidates = []) { } for (const candidate of localCandidates) { - const absolutePath = path.isAbsolute(candidate) ? candidate : path.join(utils.ROOT_DIR, candidate); + const absolutePath = path.isAbsolute(candidate) + ? candidate + : path.join(utils.ROOT_DIR, candidate); if (fs.existsSync(absolutePath)) { return absolutePath; } @@ -285,7 +287,9 @@ function resolveTokenFromFile() { return ''; } - const tokenFilePath = path.isAbsolute(tokenFile) ? tokenFile : path.join(utils.ROOT_DIR, tokenFile); + const tokenFilePath = path.isAbsolute(tokenFile) + ? tokenFile + : path.join(utils.ROOT_DIR, tokenFile); if (!fs.existsSync(tokenFilePath)) { return ''; } @@ -508,7 +512,8 @@ async function runMendScan() { assertAllowedExecutable(mendPath); const pkg = readPackageMetadata(); - const project = process.env.MEND_PROJECT || process.env.BINARY_NAME || pkg.name || 'ai-code-fusion'; + const project = + process.env.MEND_PROJECT || process.env.BINARY_NAME || pkg.name || 'ai-code-fusion'; const version = process.env.MEND_PROJECT_VERSION || process.env.VERSION || pkg.version || '0.0.0'; const commandName = process.platform === 'win32' ? 'mend-scan.exe' : 'mend-scan'; const args = ['scan', '--project', project, '--version', version]; diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index 87550d2..3a385b8 100755 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -230,7 +230,9 @@ function printHelp() { console.log(' prometheus:verify - Verify pushed stress metrics in Prometheus'); console.log(' perf-test - Run stress tests, push metrics, and verify Prometheus'); console.log(' lint - Run linter'); - console.log(' lint:md - Validate markdown links, image paths, and no decorative icons'); + console.log( + ' lint:md - Validate markdown links, image paths, and no decorative icons' + ); console.log(' format - Format code'); console.log(' validate - Run all code quality checks'); console.log(' qa - Run lint + tests + security checks'); diff --git a/scripts/lint-markdown-links.js b/scripts/lint-markdown-links.js index 52dedc6..d02e82b 100755 --- a/scripts/lint-markdown-links.js +++ b/scripts/lint-markdown-links.js @@ -62,7 +62,11 @@ function normalizeTarget(rawTarget) { function resolveTargetPath(markdownFilePath, target) { const [pathWithoutAnchor] = target.split('#'); - if (!pathWithoutAnchor || isExternalTarget(pathWithoutAnchor) || pathWithoutAnchor.startsWith('#')) { + if ( + !pathWithoutAnchor || + isExternalTarget(pathWithoutAnchor) || + pathWithoutAnchor.startsWith('#') + ) { return null; } @@ -131,7 +135,11 @@ function lintMarkdownFile(markdownFilePath) { const rawTargets = extractTargetsFromLine(line); for (const rawTarget of rawTargets) { const normalizedTarget = normalizeTarget(rawTarget); - if (!normalizedTarget || isExternalTarget(normalizedTarget) || normalizedTarget.startsWith('#')) { + if ( + !normalizedTarget || + isExternalTarget(normalizedTarget) || + normalizedTarget.startsWith('#') + ) { continue; } @@ -177,12 +185,16 @@ function run() { const relativeFilePath = path.relative(ROOT_DIR, error.filePath); if (error.kind === 'decorative-icon') { - console.error(`- ${relativeFilePath}:${error.lineNumber} -> decorative icon found: ${error.lineText}`); + console.error( + `- ${relativeFilePath}:${error.lineNumber} -> decorative icon found: ${error.lineText}` + ); continue; } const relativeResolvedPath = path.relative(ROOT_DIR, error.resolvedPath); - console.error(`- ${relativeFilePath}:${error.lineNumber} -> ${error.target} (missing: ${relativeResolvedPath})`); + console.error( + `- ${relativeFilePath}:${error.lineNumber} -> ${error.target} (missing: ${relativeResolvedPath})` + ); } process.exit(1); } diff --git a/scripts/publish-stress-metrics.js b/scripts/publish-stress-metrics.js index d390450..5eadc80 100755 --- a/scripts/publish-stress-metrics.js +++ b/scripts/publish-stress-metrics.js @@ -200,7 +200,9 @@ function buildPrometheusPayload(records, options = {}) { } } - lines.push(`# HELP ${METRIC_PREFIX}_file_count Number of files exercised by the stress scenario.`); + lines.push( + `# HELP ${METRIC_PREFIX}_file_count Number of files exercised by the stress scenario.` + ); lines.push(`# TYPE ${METRIC_PREFIX}_file_count gauge`); for (const record of records) { diff --git a/scripts/run-perf-metrics-job.js b/scripts/run-perf-metrics-job.js index e8b2f6e..9adc1f1 100644 --- a/scripts/run-perf-metrics-job.js +++ b/scripts/run-perf-metrics-job.js @@ -127,10 +127,12 @@ async function runPerfMetricsJob(options = {}) { ); } - const jobName = (env.PUSHGATEWAY_JOB || DEFAULT_PUSHGATEWAY_JOB).trim() || DEFAULT_PUSHGATEWAY_JOB; + const jobName = + (env.PUSHGATEWAY_JOB || DEFAULT_PUSHGATEWAY_JOB).trim() || DEFAULT_PUSHGATEWAY_JOB; const instanceName = (env.PUSHGATEWAY_INSTANCE || '').trim() || buildDefaultInstanceName(nowFn(), hostName); - const strictMode = (env.PUSHGATEWAY_STRICT || 'true').trim().toLowerCase() === 'false' ? 'false' : 'true'; + const strictMode = + (env.PUSHGATEWAY_STRICT || 'true').trim().toLowerCase() === 'false' ? 'false' : 'true'; const timeoutMs = toFiniteNumber(env.PROMETHEUS_VERIFY_TIMEOUT_MS) || 60_000; const pollIntervalMs = toFiniteNumber(env.PROMETHEUS_VERIFY_POLL_INTERVAL_MS) || 5_000; const minPublishTimestampSeconds = Math.floor(nowFn() / 1000); diff --git a/scripts/sonar-scan.js b/scripts/sonar-scan.js index dc5ed83..1b4332a 100755 --- a/scripts/sonar-scan.js +++ b/scripts/sonar-scan.js @@ -203,7 +203,8 @@ function runWithNativeScanner(scannerOptions, token) { const scannerOptionsForArgs = { ...scannerOptions }; delete scannerOptionsForArgs['sonar.token']; const args = Object.entries(scannerOptionsForArgs).map(([key, value]) => `-D${key}=${value}`); - const useWindowsShell = process.platform === 'win32' && scannerBinary.toLowerCase().endsWith('.bat'); + const useWindowsShell = + process.platform === 'win32' && scannerBinary.toLowerCase().endsWith('.bat'); const scannerEnv = { ...process.env }; if (token) { scannerEnv.SONAR_TOKEN = token; @@ -278,7 +279,11 @@ function resolveNativeScannerPath() { function isNpmScannerWrapperPath(scannerPath) { const normalizedPath = path.normalize(scannerPath).toLowerCase(); const wrapperSuffix = path - .join('node_modules', '.bin', process.platform === 'win32' ? 'sonar-scanner.cmd' : 'sonar-scanner') + .join( + 'node_modules', + '.bin', + process.platform === 'win32' ? 'sonar-scanner.cmd' : 'sonar-scanner' + ) .toLowerCase(); return normalizedPath.endsWith(wrapperSuffix); } @@ -397,9 +402,7 @@ try { console.log( '2. Check if the project exists on the server or if you have permission to create it' ); - console.log( - '3. Verify the token has not expired and is valid for the specified project key' - ); + console.log('3. Verify the token has not expired and is valid for the specified project key'); process.exit(1); } else { console.log('SonarQube scan completed successfully!'); diff --git a/scripts/validate-test-catalog.js b/scripts/validate-test-catalog.js index 254e7a5..b69068e 100644 --- a/scripts/validate-test-catalog.js +++ b/scripts/validate-test-catalog.js @@ -218,7 +218,11 @@ function run() { try { catalogPath = resolvePathWithinRoot(catalogArg, DEFAULT_CATALOG_PATH, 'Catalog path'); - jestConfigPath = resolvePathWithinRoot(jestConfigArg, DEFAULT_JEST_CONFIG_PATH, 'Jest config path'); + jestConfigPath = resolvePathWithinRoot( + jestConfigArg, + DEFAULT_JEST_CONFIG_PATH, + 'Jest config path' + ); } catch (error) { console.error('Test catalog validation failed:'); console.error(`- ${error.message}`); diff --git a/scripts/verify-prometheus-metrics.js b/scripts/verify-prometheus-metrics.js index 67f9b2a..bbb6443 100644 --- a/scripts/verify-prometheus-metrics.js +++ b/scripts/verify-prometheus-metrics.js @@ -70,29 +70,25 @@ function requestJson(endpointUrl, options = {}) { } }; - const request = client.request( - endpointUrl, - { method: 'GET' }, - (response) => { - const responseChunks = []; - response.on('data', (chunk) => responseChunks.push(chunk)); - response.on('end', () => { - const responseBody = Buffer.concat(responseChunks).toString('utf8'); - if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) { - rejectOnce( - new Error(`Prometheus returned HTTP ${response.statusCode || 'unknown status'}`) - ); - return; - } + const request = client.request(endpointUrl, { method: 'GET' }, (response) => { + const responseChunks = []; + response.on('data', (chunk) => responseChunks.push(chunk)); + response.on('end', () => { + const responseBody = Buffer.concat(responseChunks).toString('utf8'); + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) { + rejectOnce( + new Error(`Prometheus returned HTTP ${response.statusCode || 'unknown status'}`) + ); + return; + } - try { - resolveOnce(JSON.parse(responseBody)); - } catch (error) { - rejectOnce(new Error('Failed to parse Prometheus response body as JSON')); - } - }); - } - ); + try { + resolveOnce(JSON.parse(responseBody)); + } catch (error) { + rejectOnce(new Error('Failed to parse Prometheus response body as JSON')); + } + }); + }); request.setTimeout(requestTimeoutMs, () => { request.destroy(new Error(`Prometheus request timed out after ${requestTimeoutMs}ms`)); @@ -170,7 +166,11 @@ async function waitForStressMetrics(options) { const parsedPollIntervalMs = toFiniteNumber(pollIntervalMs) || DEFAULT_POLL_INTERVAL_MS; const parsedRequestTimeoutMs = toFiniteNumber(requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS; const boundedRequestTimeoutMs = Math.max(1, Math.min(parsedRequestTimeoutMs, parsedTimeoutMs)); - const queries = buildMetricQueries(metricName, String(jobName).trim(), String(instanceName).trim()); + const queries = buildMetricQueries( + metricName, + String(jobName).trim(), + String(instanceName).trim() + ); const deadline = nowFn() + parsedTimeoutMs; let lastError = null; @@ -203,7 +203,9 @@ async function waitForStressMetrics(options) { } const errorSuffix = - lastError instanceof Error ? ` Last error: ${lastError.message}` : ' No Prometheus samples matched.'; + lastError instanceof Error + ? ` Last error: ${lastError.message}` + : ' No Prometheus samples matched.'; const attemptedQueries = queries.join(' | '); throw new Error( diff --git a/tests/catalog.md b/tests/catalog.md index bcf4a09..c1f3331 100644 --- a/tests/catalog.md +++ b/tests/catalog.md @@ -39,6 +39,7 @@ Purpose: quick map of what is covered, why it exists, and which command to run. | `tests/unit/scripts/security.test.js` | `scripts/lib/security.js` | Command safety validation, Windows path acceptance for approved executables | | `tests/unit/scripts/actions-freshness.test.js` | `scripts/lib/actions-freshness.js` | Workflow `uses:` reference parsing, pinning classification, freshness markdown report output | | `tests/unit/scripts/eslint-config.test.js` | `eslint.config.js` | Guard scoped unicorn/sonarjs strict-pack configuration and test exclusions | +| `tests/unit/scripts/lint-gates.test.js` | `package.json` + `eslint.config.js` | Ensure lint/format gates include scripts + config coverage and staged-lint scope | | `tests/unit/scripts/electron-eslint-rules.test.js` | `eslint-rules/electron-security.js` | Validate custom Electron safety lint rules (BrowserWindow flags, IPC channels, renderer bans) | | `tests/unit/scripts/sonar-options.test.js` | `scripts/lib/sonar-options.js` | Sonar scanner option merge behavior and CPD exclusion defaults | | `tests/unit/scripts/publish-stress-metrics.test.js` | `scripts/publish-stress-metrics.js` | Prometheus payload generation and Pushgateway publication safeguards | diff --git a/tests/unit/scripts/eslint-config.test.js b/tests/unit/scripts/eslint-config.test.js index fd8ef7b..6df3cb2 100644 --- a/tests/unit/scripts/eslint-config.test.js +++ b/tests/unit/scripts/eslint-config.test.js @@ -34,4 +34,11 @@ describe('eslint phase 2 strict packs config', () => { "'electron-security/no-electron-import-in-renderer': 'error'" ); }); + + test('includes scripts/config lint scope and does not globally ignore scripts', () => { + expect(eslintConfigSource).toContain("'scripts/**/*.js'"); + expect(eslintConfigSource).toContain("'*.config.js'"); + expect(eslintConfigSource).toContain("sourceType: 'commonjs'"); + expect(eslintConfigSource).not.toContain("'scripts/**',"); + }); }); diff --git a/tests/unit/scripts/lint-gates.test.js b/tests/unit/scripts/lint-gates.test.js new file mode 100644 index 0000000..d11981e --- /dev/null +++ b/tests/unit/scripts/lint-gates.test.js @@ -0,0 +1,31 @@ +const fs = jest.requireActual('fs'); +const path = jest.requireActual('path'); + +const packageJsonPath = path.resolve(__dirname, '../../../package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + +describe('lint and format gate scripts', () => { + test('lint command covers scripts and config files', () => { + const lintScript = packageJson.scripts.lint; + + expect(lintScript).toContain('eslint src tests scripts'); + expect(lintScript).toContain('eslint.config.js'); + expect(lintScript).toContain('.eslintrc.js'); + expect(lintScript).toContain('playwright.config.ts'); + expect(lintScript).toContain('--max-warnings 0'); + }); + + test('format:check enforces JS/TS formatting for scripts and configs', () => { + const formatCheckScript = packageJson.scripts['format:check']; + + expect(formatCheckScript).toContain('scripts/**/*.js'); + expect(formatCheckScript).toContain('*.config.js'); + expect(formatCheckScript).toContain('playwright.config.ts'); + }); + + test('lint-staged includes scripts path coverage', () => { + const lintStagedKeys = Object.keys(packageJson['lint-staged'] ?? {}); + + expect(lintStagedKeys).toContain('{src,tests,scripts}/**/*.{js,jsx,ts,tsx}'); + }); +});