cc-haha/scripts/git-hooks/install.test.ts
程序员阿江(Relakkes) b156be8d8d feat: make PR quality verification self-enforcing
Contributors and coding agents need one local command that both reports and enforces the quality contract. This change turns the PR gate into the shared verification entrypoint, adds path-selected local lanes, tightens coverage accounting around changed lines, and documents the repair loop in contributor and agent-facing guidance.

Constraint: Ordinary PR verification must stay non-live and runnable without provider credentials
Constraint: Coverage policy updates in this commit require maintainer approval before push/merge
Rejected: Keep quality guidance only in docs | agents need executable scripts and AGENTS.md instructions to follow the loop consistently
Confidence: high
Scope-risk: broad
Directive: Do not bypass `bun run verify` for production changes; fix failed lanes and coverage reports instead of lowering thresholds
Tested: bun run check:policy
Tested: ALLOW_CLI_CORE_CHANGE=1 ALLOW_COVERAGE_BASELINE_CHANGE=1 bun run verify
Not-tested: live provider baseline; no provider credentials were required for this non-live PR gate
2026-05-06 22:33:43 +08:00

100 lines
3.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { installPrePushHook } from './install'
function runGit(rootDir: string, args: string[]) {
const proc = Bun.spawnSync(['git', ...args], {
cwd: rootDir,
stdout: 'pipe',
stderr: 'pipe',
})
if (proc.exitCode !== 0) {
throw new Error(new TextDecoder().decode(proc.stderr))
}
return new TextDecoder().decode(proc.stdout).trim()
}
describe('installPrePushHook', () => {
test('copies the tracked hook and makes it executable', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'git-hook-install-test-'))
try {
const sourcePath = join(tempDir, 'source-pre-push')
const hookPath = join(tempDir, 'hooks', 'pre-push')
writeFileSync(sourcePath, '#!/usr/bin/env bash\necho quality\n')
const result = installPrePushHook({
rootDir: tempDir,
sourcePath,
hookPath,
})
expect(result.hookPath).toBe(hookPath)
expect(result.liveConfigured).toBe(false)
expect(readFileSync(hookPath, 'utf8')).toContain('echo quality')
expect(statSync(hookPath).mode & 0o111).toBeGreaterThan(0)
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
test('refuses to overwrite an unrelated existing hook unless forced', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'git-hook-install-test-'))
try {
const sourcePath = join(tempDir, 'source-pre-push')
const hookPath = join(tempDir, 'hooks', 'pre-push')
writeFileSync(sourcePath, '#!/usr/bin/env bash\necho new\n')
mkdirSync(join(tempDir, 'hooks'), { recursive: true })
writeFileSync(hookPath, '#!/usr/bin/env bash\necho old\n')
expect(() => installPrePushHook({
rootDir: tempDir,
sourcePath,
hookPath,
})).toThrow('Refusing to overwrite existing hook')
installPrePushHook({
rootDir: tempDir,
sourcePath,
hookPath,
force: true,
})
expect(readFileSync(hookPath, 'utf8')).toContain('echo new')
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
test('stores live gate settings in local git config', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'git-hook-install-test-'))
try {
runGit(tempDir, ['init'])
const sourcePath = join(tempDir, 'source-pre-push')
writeFileSync(sourcePath, '#!/usr/bin/env bash\necho live\n')
const result = installPrePushHook({
rootDir: tempDir,
sourcePath,
liveProviderModels: ['codingplan:main:codingplan-main'],
liveMode: 'baseline',
allowCliCoreChange: true,
allowCoverageBaselineChange: true,
})
expect(result.liveConfigured).toBe(true)
expect(readFileSync(result.hookPath, 'utf8')).toContain('echo live')
expect(runGit(tempDir, ['config', '--local', '--get', 'quality.prePushLive'])).toBe('true')
expect(runGit(tempDir, ['config', '--local', '--get', 'quality.prePushProviderModels'])).toBe('codingplan:main:codingplan-main')
expect(runGit(tempDir, ['config', '--local', '--get', 'quality.prePushLiveMode'])).toBe('baseline')
expect(runGit(tempDir, ['config', '--local', '--get', 'quality.allowCliCoreChange'])).toBe('true')
expect(runGit(tempDir, ['config', '--local', '--get', 'quality.allowCoverageBaselineChange'])).toBe('true')
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
})