107 lines
3.4 KiB
JavaScript
107 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execSync } from 'node:child_process'
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
const RULES = [
|
|
{
|
|
name: 'api',
|
|
source: /^src\/app\/api\//,
|
|
tests: [/^tests\/integration\/api\/contract\//, /^tests\/system\//, /^tests\/regression\//],
|
|
message: 'changing src/app/api/** requires a matching contract, system, or regression test change',
|
|
},
|
|
{
|
|
name: 'worker',
|
|
source: /^src\/lib\/workers\//,
|
|
tests: [/^tests\/unit\/worker\//, /^tests\/system\//, /^tests\/regression\//],
|
|
message: 'changing src/lib/workers/** requires a matching worker, system, or regression test change',
|
|
},
|
|
{
|
|
name: 'task',
|
|
source: /^src\/lib\/task\//,
|
|
tests: [/^tests\/unit\/task\//, /^tests\/system\//, /^tests\/regression\//],
|
|
message: 'changing src/lib/task/** requires a matching task, system, or regression test change',
|
|
},
|
|
{
|
|
name: 'media',
|
|
source: /^src\/lib\/media\//,
|
|
tests: [/^tests\/unit\//, /^tests\/system\//, /^tests\/regression\//],
|
|
message: 'changing src/lib/media/** requires a matching unit, system, or regression test change',
|
|
},
|
|
{
|
|
name: 'provider',
|
|
source: /^src\/lib\/(generator-api|generators|model-gateway|lipsync|providers)\//,
|
|
tests: [/^tests\/unit\/(providers|model-gateway|llm)\//, /^tests\/integration\/provider\//, /^tests\/system\//, /^tests\/regression\//],
|
|
message: 'changing provider/gateway code requires provider contract, system, or regression test change',
|
|
},
|
|
]
|
|
|
|
function normalizeChangedFiles(rawFiles) {
|
|
return rawFiles
|
|
.flatMap((item) => item.split(/[\n,]/))
|
|
.map((item) => item.trim())
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function readGitChangedFiles() {
|
|
try {
|
|
const output = execSync('git diff --name-only --cached', {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
})
|
|
return normalizeChangedFiles([output])
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function inspectChangedFiles(changedFiles) {
|
|
const changed = normalizeChangedFiles(changedFiles)
|
|
const changedTests = changed.filter((file) => file.startsWith('tests/'))
|
|
const violations = []
|
|
|
|
for (const rule of RULES) {
|
|
const impactedSources = changed.filter((file) => rule.source.test(file))
|
|
if (impactedSources.length === 0) continue
|
|
const hasMatchingTestChange = changedTests.some((file) => rule.tests.some((pattern) => pattern.test(file)))
|
|
if (!hasMatchingTestChange) {
|
|
violations.push(`${rule.name}: ${rule.message}; sources=${impactedSources.join(',')}`)
|
|
}
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
function fail(violations) {
|
|
console.error('\n[changed-file-test-impact-guard] Missing matching test changes')
|
|
for (const violation of violations) {
|
|
console.error(` - ${violation}`)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
|
|
function runCli() {
|
|
const inputFiles = process.argv.slice(2)
|
|
const changedFiles = inputFiles.length > 0
|
|
? normalizeChangedFiles(inputFiles)
|
|
: normalizeChangedFiles([process.env.TEST_IMPACT_CHANGED_FILES || '', ...readGitChangedFiles()])
|
|
|
|
if (changedFiles.length === 0) {
|
|
console.log('[changed-file-test-impact-guard] SKIP no changed files detected')
|
|
process.exit(0)
|
|
}
|
|
|
|
const violations = inspectChangedFiles(changedFiles)
|
|
if (violations.length > 0) {
|
|
fail(violations)
|
|
}
|
|
|
|
console.log(`[changed-file-test-impact-guard] OK files=${changedFiles.length}`)
|
|
}
|
|
|
|
const entryHref = process.argv[1] ? pathToFileURL(process.argv[1]).href : null
|
|
if (entryHref && import.meta.url === entryHref) {
|
|
runCli()
|
|
}
|