2026-02-05 04:39:45 -08:00
/ * *
* Check Coverage Tool
*
* Custom OpenCode tool to analyze test coverage and report on gaps .
* Supports common coverage report formats .
* /
2026-02-09 01:32:22 -08:00
import { tool } from "@opencode-ai/plugin/tool"
2026-02-05 04:39:45 -08:00
import * as path from "path"
import * as fs from "fs"
export default tool ( {
description :
"Check test coverage against a threshold and identify files with low coverage. Reads coverage reports from common locations." ,
args : {
threshold : tool.schema
. number ( )
. optional ( )
. describe ( "Minimum coverage percentage required (default: 80)" ) ,
showUncovered : tool.schema
. boolean ( )
. optional ( )
. describe ( "Show list of uncovered files (default: true)" ) ,
format : tool.schema
. enum ( [ "summary" , "detailed" , "json" ] )
. optional ( )
. describe ( "Output format (default: summary)" ) ,
} ,
async execute ( args , context ) {
const threshold = args . threshold ? ? 80
const showUncovered = args . showUncovered ? ? true
const format = args . format ? ? "summary"
const cwd = context . worktree || context . directory
// Look for coverage reports
const coveragePaths = [
"coverage/coverage-summary.json" ,
"coverage/lcov-report/index.html" ,
"coverage/coverage-final.json" ,
".nyc_output/coverage.json" ,
]
let coverageData : CoverageSummary | null = null
let coverageFile : string | null = null
for ( const coveragePath of coveragePaths ) {
const fullPath = path . join ( cwd , coveragePath )
if ( fs . existsSync ( fullPath ) && coveragePath . endsWith ( ".json" ) ) {
try {
const content = JSON . parse ( fs . readFileSync ( fullPath , "utf-8" ) )
coverageData = parseCoverageData ( content )
coverageFile = coveragePath
break
} catch {
// Continue to next file
}
}
}
if ( ! coverageData ) {
2026-02-09 01:32:22 -08:00
return JSON . stringify ( {
2026-02-05 04:39:45 -08:00
success : false ,
error : "No coverage report found" ,
suggestion :
"Run tests with coverage first: npm test -- --coverage" ,
searchedPaths : coveragePaths ,
2026-02-09 01:32:22 -08:00
} )
2026-02-05 04:39:45 -08:00
}
const passed = coverageData . total . percentage >= threshold
const uncoveredFiles = coverageData . files . filter (
( f ) = > f . percentage < threshold
)
const result : CoverageResult = {
success : passed ,
threshold ,
coverageFile ,
total : coverageData.total ,
passed ,
}
if ( format === "detailed" || ( showUncovered && uncoveredFiles . length > 0 ) ) {
result . uncoveredFiles = uncoveredFiles . slice ( 0 , 20 ) // Limit to 20 files
result . uncoveredCount = uncoveredFiles . length
}
if ( format === "json" ) {
result . rawData = coverageData
}
if ( ! passed ) {
result . suggestion = ` Coverage is ${ coverageData . total . percentage . toFixed ( 1 ) } % which is below the ${ threshold } % threshold. Focus on these files: \ n ${ uncoveredFiles
. slice ( 0 , 5 )
. map ( ( f ) = > ` - ${ f . file } : ${ f . percentage . toFixed ( 1 ) } % ` )
. join ( "\n" ) } `
}
2026-02-09 01:32:22 -08:00
return JSON . stringify ( result )
2026-02-05 04:39:45 -08:00
} ,
} )
interface CoverageSummary {
total : {
lines : number
covered : number
percentage : number
}
files : Array < {
file : string
lines : number
covered : number
percentage : number
} >
}
interface CoverageResult {
success : boolean
threshold : number
coverageFile : string | null
total : CoverageSummary [ "total" ]
passed : boolean
uncoveredFiles? : CoverageSummary [ "files" ]
uncoveredCount? : number
rawData? : CoverageSummary
suggestion? : string
}
function parseCoverageData ( data : unknown ) : CoverageSummary {
// Handle istanbul/nyc format
if ( typeof data === "object" && data !== null && "total" in data ) {
const istanbulData = data as Record < string , unknown >
const total = istanbulData . total as Record < string , { total : number ; covered : number } >
const files : CoverageSummary [ "files" ] = [ ]
for ( const [ key , value ] of Object . entries ( istanbulData ) ) {
if ( key !== "total" && typeof value === "object" && value !== null ) {
const fileData = value as Record < string , { total : number ; covered : number } >
if ( fileData . lines ) {
files . push ( {
file : key ,
lines : fileData.lines.total ,
covered : fileData.lines.covered ,
percentage : fileData.lines.total > 0
? ( fileData . lines . covered / fileData . lines . total ) * 100
: 100 ,
} )
}
}
}
return {
total : {
lines : total.lines?.total || 0 ,
covered : total.lines?.covered || 0 ,
percentage : total.lines?.total
? ( total . lines . covered / total . lines . total ) * 100
: 0 ,
} ,
files ,
}
}
// Default empty result
return {
total : { lines : 0 , covered : 0 , percentage : 0 } ,
files : [ ] ,
}
}