Help us improve
Share bugs, ideas, or general feedback.
From prepare-delivery
Use when validating test coverage quality for new code. Checks that tests exist, are meaningful, and actually exercise new code - not just path matching.
npx claudepluginhub agent-sh/agentsys --plugin prepare-deliveryHow this skill is triggered — by the user, by Claude, or both
Slash command
/prepare-delivery:check-test-coverageThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Validate that new work has appropriate, meaningful test coverage.
Traces codepaths in git diffs, maps them against tests, scores coverage quality, and auto-generates tests for uncovered paths before shipping PRs.
Analyzes Istanbul, c8, lcov coverage reports to identify uncovered lines, branches, functions with risk-weighted gap detection. Use for gap analysis, branch comparisons, test prioritization.
Reviews test suites for assertion quality to detect coverage theater where high line/branch coverage masks weak tests that wouldn't catch regressions.
Share bugs, ideas, or general feedback.
Validate that new work has appropriate, meaningful test coverage. Advisory only - reports coverage gaps but does NOT block the workflow.
Validates test QUALITY, not just test EXISTENCE. A test file that exists but doesn't meaningfully exercise the new code is flagged as a gap.
Analyze files in: git diff --name-only origin/${BASE_BRANCH}..HEAD
BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
CHANGED_SOURCE=$(git diff --name-only origin/${BASE_BRANCH}..HEAD 2>/dev/null | \
grep -E '\.(js|ts|jsx|tsx|py|rs|go|rb|java|kt|swift|cpp|c|cs)$' | \
grep -v -E '(test|spec|_test|Test)\.')
CHANGED_TESTS=$(git diff --name-only origin/${BASE_BRANCH}..HEAD 2>/dev/null | \
grep -E '(test|spec|_test|Test)\.')
Optional - enriches coverage checks with historical risk signals.
const { binary } = require('@agentsys/lib');
const fs = require('fs');
const path = require('path');
const { getStateDirPath } = require('@agentsys/lib/platform/state-dir');
const cwd = process.cwd();
const mapFile = path.join(getStateDirPath(cwd), 'repo-intel.json');
let repoIntel = null;
if (fs.existsSync(mapFile)) {
repoIntel = {};
try {
repoIntel.testGaps = JSON.parse(binary.runAnalyzer([
'repo-intel', 'query', 'test-gaps', '--top', '20', '--map-file', mapFile, cwd
]));
} catch (e) { repoIntel.testGaps = null; }
try {
repoIntel.bugspots = JSON.parse(binary.runAnalyzer([
'repo-intel', 'query', 'bugspots', '--top', '20', '--map-file', mapFile, cwd
]));
} catch (e) { repoIntel.bugspots = null; }
}
const testGapSet = new Set((repoIntel?.testGaps || []).map(g => g.path));
const bugspotMap = new Map((repoIntel?.bugspots || []).map(b => [b.path, b]));
# Detect test directory
for dir in tests __tests__ test spec; do
[ -d "$dir" ] && echo "TEST_DIR=$dir" && break
done
# Detect naming convention
for pattern in "*.test.*" "*.spec.*" "test_*.*"; do
ls **/$pattern 2>/dev/null | head -1 && echo "TEST_PATTERN=$pattern" && break
done
function findTestFile(sourceFile) {
const basename = sourceFile.split('/').pop().replace(/\.[^.]+$/, '');
const dir = sourceFile.split('/').slice(0, -1).join('/');
const ext = sourceFile.split('.').pop();
return [
`tests/${basename}.test.${ext}`, `tests/${basename}.spec.${ext}`,
`test/${basename}.test.${ext}`, `__tests__/${basename}.test.${ext}`,
`${dir}/${basename}.test.${ext}`, `${dir}/${basename}.spec.${ext}`,
`${dir}/__tests__/${basename}.test.${ext}`,
`tests/test_${basename}.${ext}`, `test/test_${basename}.${ext}`,
`${dir}/${basename}_test.${ext}`
];
}
For each changed source file, find test file and check if it was updated.
const gaps = [];
const covered = [];
for (const sourceFile of changedSourceFiles) {
const testCandidates = findTestFile(sourceFile);
const existingTest = testCandidates.find(t => fileExists(t));
if (!existingTest) {
gaps.push({ file: sourceFile, reason: 'No test file found', candidates: testCandidates.slice(0, 3) });
continue;
}
const testModified = changedTestFiles.includes(existingTest);
if (!testModified) {
gaps.push({ file: sourceFile, reason: 'Source modified but test file not updated', testFile: existingTest });
} else {
covered.push({ file: sourceFile, testFile: existingTest });
}
}
When repo-intel data is available, apply stricter checks to historically risky files.
const riskIssues = [];
const trivialPatterns = [
/expect\s*\(\s*true\s*\)/, /expect\s*\(\s*1\s*\)\.toBe\s*\(\s*1\s*\)/,
/assert\s*\(\s*True\s*\)/, /\.toBeDefined\s*\(\s*\)/
];
for (const sourceFile of changedSourceFiles) {
const inTestGaps = testGapSet.has(sourceFile);
const bugspot = bugspotMap.get(sourceFile);
const existingTest = findTestFile(sourceFile).find(t => fileExists(t));
const testModified = existingTest && changedTestFiles.includes(existingTest);
if (inTestGaps && bugspot && !testModified) {
riskIssues.push({
file: sourceFile, severity: 'critical', type: 'high-risk-untested',
bugFixRate: bugspot.bugFixRate,
message: `${sourceFile} has ${(bugspot.bugFixRate * 100).toFixed(0)}% bug-fix rate AND no co-changing test - coverage required`
});
}
if (bugspot && bugspot.bugFixRate > 0.3 && testModified && existingTest) {
const testContent = await readFile(existingTest);
if (trivialPatterns.some(p => p.test(testContent))) {
riskIssues.push({
file: sourceFile, severity: 'warning', type: 'weak-test-for-risky-file',
bugFixRate: bugspot.bugFixRate, testFile: existingTest,
message: `${sourceFile} has ${(bugspot.bugFixRate * 100).toFixed(0)}% bug-fix rate but test only has trivial assertions`
});
}
}
}
async function findNewExports(file) {
const diff = await run('git', ['diff', `origin/${BASE_BRANCH}..HEAD`, '--', file]);
const newExports = [];
const patterns = [
/^\+\s*export\s+(function|const|class|async function)\s+(\w+)/gm,
/^\+\s*export\s+default\s+(function|class)\s*(\w*)/gm,
/^\+\s*module\.exports\s*=\s*\{([^}]+)\}/gm,
/^\+\s*def\s+(\w+)\(/gm,
/^\+\s*pub\s+fn\s+(\w+)/gm,
/^\+\s*func\s+(\w+)/gm
];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(diff)) !== null) {
newExports.push(match[2] || match[1]);
}
}
return newExports;
}
Verify tests actually exercise the new code.
async function validateTestQuality(sourceFile, testFile, newExports) {
const testContent = await readFile(testFile);
const issues = [];
// Check new exports are tested
for (const exportName of newExports) {
if (!testContent.match(new RegExp(exportName, 'g'))) {
issues.push({ type: 'untested-export', export: exportName,
message: `New export '${exportName}' not referenced in test file` });
}
}
// Check for trivial assertions
if (trivialPatterns.some(p => p.test(testContent))) {
issues.push({ type: 'trivial-assertion',
message: 'Test contains trivial assertions that do not validate behavior' });
}
// Check test structure
if (!/describe\s*\(/.test(testContent) && !/it\s*\(/.test(testContent)) {
issues.push({ type: 'no-test-structure', message: 'Test file lacks describe/it blocks' });
}
// Check edge case coverage
const edgeCasePatterns = ['null', 'undefined', 'empty', 'error', 'invalid', 'boundary'];
if (!edgeCasePatterns.some(p => testContent.toLowerCase().includes(p)) && newExports.length > 0) {
issues.push({ type: 'missing-edge-cases', message: 'Tests may lack edge case coverage', severity: 'warning' });
}
// Check source import
const sourceBasename = sourceFile.split('/').pop().replace(/\.[^.]+$/, '');
const importPatterns = [
new RegExp(`from\\s+['"][^'"]*${sourceBasename}['"]`),
new RegExp(`require\\s*\\(\\s*['"][^'"]*${sourceBasename}['"]`)
];
if (!importPatterns.some(p => p.test(testContent))) {
issues.push({ type: 'no-source-import', message: `Test does not import '${sourceBasename}'`, severity: 'critical' });
}
return {
testFile, sourceFile,
quality: issues.length === 0 ? 'good' : issues.some(i => i.severity === 'critical') ? 'poor' : 'needs-improvement',
issues
};
}
async function analyzeTestDepth(sourceFile, testFile, diff) {
const analysis = { sourceComplexity: 'unknown', testCoverage: 'unknown', suggestions: [] };
const branchPatterns = [/^\+.*if\s*\(/gm, /^\+.*else\s*\{/gm, /^\+.*\?\s*.*:/gm,
/^\+.*switch\s*\(/gm, /^\+.*case\s+/gm, /^\+.*catch\s*\(/gm];
const newBranches = branchPatterns.flatMap(p => diff.match(p) || []);
if (newBranches.length > 3) {
analysis.sourceComplexity = 'high';
analysis.suggestions.push('New code has multiple branches - ensure each path is tested');
}
if (/^\+.*async\s+|^\+.*await\s+/m.test(diff)) {
const testContent = await readFile(testFile);
if (!/\.rejects|\.resolves|async.*expect|try.*catch.*expect/i.test(testContent)) {
analysis.suggestions.push('New async code detected - add tests for promise rejection scenarios');
}
}
return analysis;
}
Return structured JSON between markers:
=== TEST_COVERAGE_RESULT ===
{
"scope": "new-work-only",
"coverage": { "filesAnalyzed": 5, "filesWithTests": 3, "filesMissingTests": 2, "coveragePercent": 60 },
"gaps": [...],
"qualityIssues": [...],
"covered": [...],
"riskIssues": [...],
"summary": { "status": "...", "recommendation": "...", "riskSummary": "..." }
}
=== END_RESULT ===