Spaces:
Runtime error
Runtime error
| import { execFileSync } from "node:child_process"; | |
| import { mkdirSync, writeFileSync } from "node:fs"; | |
| import path from "node:path"; | |
| const SOURCE_ROOTS = ["src/", "open-sse/", "electron/", "bin/"]; | |
| const TEST_PATTERNS = [/^tests\//, /(?:^|\/)__tests__\//, /\.(?:test|spec)\.[cm]?[jt]sx?$/]; | |
| // Test files for specific source types (e.g., Python validation scripts for i18n) | |
| const TEST_FILE_PATTERNS = { | |
| "src/i18n/messages/": [ | |
| /\/scripts\/validate_translation\.py$/, | |
| /\/scripts\/check_translations\.py$/, | |
| ], | |
| }; | |
| // Exclude directories that don't require tests (i18n has Python validation, docs, config) | |
| const EXCLUDED_PATTERNS = [ | |
| /\/i18n\/messages\//, // i18n files have their own Python test scripts | |
| /\.md$/, // Documentation | |
| /\.yaml$/, // Config files | |
| /\.yml$/, // Config files | |
| /(?:^|\/)package(?:-lock)?\.json$/, // Dependency manifests/lockfiles (e.g. Dependabot bumps) — not testable code | |
| ]; | |
| function getArg(name, fallbackValue = "") { | |
| const index = process.argv.indexOf(name); | |
| if (index === -1 || index === process.argv.length - 1) { | |
| return fallbackValue; | |
| } | |
| return process.argv[index + 1]; | |
| } | |
| function runGit(args) { | |
| return execFileSync("git", args, { encoding: "utf8" }).trim(); | |
| } | |
| function isSourceFile(filePath) { | |
| // Exclude patterns that don't require tests | |
| if (EXCLUDED_PATTERNS.some((pattern) => pattern.test(filePath))) { | |
| return false; | |
| } | |
| return SOURCE_ROOTS.some((root) => filePath.startsWith(root)); | |
| } | |
| function isTestFile(filePath) { | |
| // Check standard test patterns | |
| if (TEST_PATTERNS.some((pattern) => pattern.test(filePath))) { | |
| return true; | |
| } | |
| // Check custom test file patterns for specific directories | |
| for (const [dir, patterns] of Object.entries(TEST_FILE_PATTERNS)) { | |
| if (filePath.startsWith(dir) && patterns.some((pattern) => pattern.test(filePath))) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function buildReport(lines) { | |
| return `${lines.join("\n")}\n`; | |
| } | |
| const summaryFile = getArg("--summary-file", ""); | |
| const baseRef = process.env.GITHUB_BASE_REF; | |
| if (!baseRef) { | |
| const report = buildReport([ | |
| "## PR Test Policy", | |
| "", | |
| "Skipped: not running in a pull request context.", | |
| ]); | |
| if (summaryFile) { | |
| mkdirSync(path.dirname(summaryFile), { recursive: true }); | |
| writeFileSync(summaryFile, report); | |
| } | |
| process.stdout.write(report); | |
| process.exit(0); | |
| } | |
| const baseTarget = process.env.GITHUB_BASE_SHA || `origin/${baseRef}`; | |
| const changedFiles = runGit(["diff", "--name-only", "--diff-filter=ACMR", `${baseTarget}...HEAD`]) | |
| .split(/\r?\n/) | |
| .map((line) => line.trim()) | |
| .filter(Boolean); | |
| const changedSourceFiles = changedFiles.filter(isSourceFile); | |
| const changedTestFiles = changedFiles.filter(isTestFile); | |
| const hasRequiredTests = changedSourceFiles.length === 0 || changedTestFiles.length > 0; | |
| const reportLines = [ | |
| "## PR Test Policy", | |
| "", | |
| `Base ref: \`${baseRef}\``, | |
| `Changed production files: ${changedSourceFiles.length}`, | |
| `Changed automated test files: ${changedTestFiles.length}`, | |
| "", | |
| ]; | |
| if (changedSourceFiles.length > 0) { | |
| reportLines.push("### Production files in scope", ""); | |
| for (const filePath of changedSourceFiles.slice(0, 20)) { | |
| reportLines.push(`- \`${filePath}\``); | |
| } | |
| reportLines.push(""); | |
| } | |
| if (changedTestFiles.length > 0) { | |
| reportLines.push("### Tests in this PR", ""); | |
| for (const filePath of changedTestFiles.slice(0, 20)) { | |
| reportLines.push(`- \`${filePath}\``); | |
| } | |
| reportLines.push(""); | |
| } | |
| if (hasRequiredTests) { | |
| reportLines.push("Result: PASS"); | |
| } else { | |
| reportLines.push( | |
| "Result: FAIL", | |
| "", | |
| "This PR changes production code under `src/`, `open-sse/`, `electron/`, or `bin/` but does not add or update automated tests." | |
| ); | |
| } | |
| const report = buildReport(reportLines); | |
| if (summaryFile) { | |
| mkdirSync(path.dirname(summaryFile), { recursive: true }); | |
| writeFileSync(summaryFile, report); | |
| } | |
| process.stdout.write(report); | |
| if (!hasRequiredTests) { | |
| process.exit(1); | |
| } | |