mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
Implemented three-layer defense mechanism to prevent AI-generated code from breaking the codebase: - Layer 1: AGENTS.md with universal AI agent instructions - Layer 2: Husky pre-commit hooks with test file checking - Layer 3: GitHub Actions CI with TypeScript, ESLint, tests, and coverage Includes design document, test infrastructure, and coverage thresholds. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* Pre-commit hook script that checks if staged source files have corresponding test files.
|
|
* This is an advisory check - it warns but does not block commits.
|
|
*/
|
|
|
|
import { execSync } from 'child_process'
|
|
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
|
|
// ANSI color codes
|
|
const colors = {
|
|
yellow: '\x1b[33m',
|
|
green: '\x1b[32m',
|
|
red: '\x1b[31m',
|
|
reset: '\x1b[0m',
|
|
bold: '\x1b[1m'
|
|
}
|
|
|
|
// Source directories that require tests
|
|
const SOURCE_DIRS = ['src/main', 'src/shared']
|
|
|
|
// Mapping from source paths to test paths
|
|
function getExpectedTestPath(sourcePath: string): string | null {
|
|
// src/main/conductor/Conductor.ts -> tests/unit/main/conductor/Conductor.test.ts
|
|
// src/shared/types.ts -> tests/unit/shared/types.test.ts
|
|
|
|
for (const sourceDir of SOURCE_DIRS) {
|
|
if (sourcePath.startsWith(sourceDir)) {
|
|
const relativePath = sourcePath.slice(sourceDir.length + 1) // +1 for the slash
|
|
const dirName = sourceDir.split('/')[1] // 'main' or 'shared'
|
|
const testPath = `tests/unit/${dirName}/${relativePath.replace(/\.ts$/, '.test.ts')}`
|
|
return testPath
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Get list of staged files
|
|
function getStagedFiles(): string[] {
|
|
try {
|
|
const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
encoding: 'utf-8'
|
|
})
|
|
return output.trim().split('\n').filter(Boolean)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
// Check if a file is a TypeScript source file (not a test, not a type definition)
|
|
function isSourceFile(filePath: string): boolean {
|
|
if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx')) {
|
|
return false
|
|
}
|
|
if (filePath.includes('.test.') || filePath.includes('.spec.')) {
|
|
return false
|
|
}
|
|
if (filePath.endsWith('.d.ts')) {
|
|
return false
|
|
}
|
|
return SOURCE_DIRS.some((dir) => filePath.startsWith(dir))
|
|
}
|
|
|
|
// Check if a file is a test file
|
|
function isTestFile(filePath: string): boolean {
|
|
return filePath.includes('.test.') || filePath.includes('.spec.')
|
|
}
|
|
|
|
// Main function
|
|
function main(): void {
|
|
const stagedFiles = getStagedFiles()
|
|
|
|
if (stagedFiles.length === 0) {
|
|
console.log(`${colors.green}No staged files to check.${colors.reset}`)
|
|
return
|
|
}
|
|
|
|
const sourceFiles = stagedFiles.filter(isSourceFile)
|
|
const testFiles = stagedFiles.filter(isTestFile)
|
|
|
|
if (sourceFiles.length === 0) {
|
|
console.log(`${colors.green}No source files modified.${colors.reset}`)
|
|
return
|
|
}
|
|
|
|
// Check which source files are missing corresponding test files
|
|
const missingTests: { source: string; expectedTest: string }[] = []
|
|
|
|
for (const sourceFile of sourceFiles) {
|
|
const expectedTestPath = getExpectedTestPath(sourceFile)
|
|
if (!expectedTestPath) continue
|
|
|
|
// Check if the test file is staged or already exists
|
|
const testFileStaged = testFiles.some(
|
|
(tf) =>
|
|
tf === expectedTestPath ||
|
|
tf.includes(path.basename(sourceFile).replace(/\.tsx?$/, '.test.'))
|
|
)
|
|
|
|
const testFileExists = fs.existsSync(expectedTestPath)
|
|
|
|
if (!testFileStaged && !testFileExists) {
|
|
missingTests.push({ source: sourceFile, expectedTest: expectedTestPath })
|
|
}
|
|
}
|
|
|
|
if (missingTests.length > 0) {
|
|
console.log(
|
|
`\n${colors.yellow}${colors.bold}Warning: Source files modified without corresponding tests${colors.reset}\n`
|
|
)
|
|
console.log(' Modified source files:')
|
|
for (const { source } of missingTests) {
|
|
console.log(` ${colors.yellow}- ${source}${colors.reset}`)
|
|
}
|
|
console.log('\n Expected test files (not found):')
|
|
for (const { expectedTest } of missingTests) {
|
|
console.log(` ${colors.yellow}- ${expectedTest}${colors.reset}`)
|
|
}
|
|
console.log(`\n ${colors.bold}Consider adding tests for your changes.${colors.reset}`)
|
|
console.log(` Run 'pnpm test:run' to verify existing tests still pass.\n`)
|
|
} else {
|
|
console.log(`${colors.green}All modified source files have corresponding tests.${colors.reset}`)
|
|
}
|
|
}
|
|
|
|
main()
|