Skip to content
Back to skills

Test Audit

ASecurity

Static test-suite quality audit - coverage from lcov/Istanbul/Cobertura/go/tarpaulin reports, pyramid shape (unit/integration/e2e ratio), anti-patterns (.only leaks, skipped tests, no-assertion tests, hardcoded sleeps). Stack-aware across 11 supported stacks.

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 9, 2026
ai-agentspythonrustgojavarubyswiftkotlinbashnodetesting

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 9, 2026

npx -y skills add marcoguillermaz/Tierward --skill test-audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Test Audit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Test Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/marcoguillermaz-test-audit/badge)](https://www.skillsdirectory.com/skills/marcoguillermaz-test-audit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: test-audit
description: Static test-suite quality audit - coverage from lcov/Istanbul/Cobertura/go/tarpaulin reports, pyramid shape (unit/integration/e2e ratio), anti-patterns (.only leaks, skipped tests, no-assertion tests, hardcoded sleeps). Stack-aware across 11 supported stacks.
user-invocable: true
model: sonnet
context: fork
argument-hint: [target:path:<dir>|target:file:<glob>|target:coverage:<path>|mode:all]
allowed-tools: Read Glob Grep Bash
---

## Scope for v1

- **Static analysis only.** Parses coverage reports produced by the project's test runner. Does not execute tests, does not re-run the suite, does not query CI history.
- **Flaky-test detection is deferred.** Detecting non-deterministic failures requires multiple runs or CI-history access, both out of scope for the first ship. Tracked as future-work item `TEST-flake-detection`.
- **Live flake / performance probes**: revisit after real usage.

---

## Configuration (adapt before first run)

> Replace these placeholders:
> - `[TEST_PATH]` - primary location of test files (e.g. `src/**/*.test.ts`, `tests/`, `spec/`, `Tests/`, `src/test/`)
> - `[COVERAGE_PATH]` - location of coverage reports if non-standard (e.g. `coverage/lcov.info`, `reports/coverage.xml`)
> - `[APP_SOURCE_PATH]` - path to application source (for colocated-test detection) - e.g. `src/`, `app/`, `lib/`

---

## Step 0 - Target resolution

Parse `$ARGUMENTS` for a `target:` or `mode:` token.

| Pattern | Meaning |
|---|---|
| `target:path:<dir>` | Audit tests under a specific directory only (e.g. `target:path:src/auth`) |
| `target:file:<glob>` | Audit files matching the glob (e.g. `target:file:**/*.integration.test.ts`) |
| `target:coverage:<path>` | Force a specific coverage report path instead of auto-detection |
| `mode:all` / no argument | **Full audit - every discovered test file and auto-detected coverage report.** |

**STRICT PARSING**: derive target ONLY from explicit text in `$ARGUMENTS`. Do NOT infer from conversation context, recent blocks, or memory.

Announce: `Running test-audit - scope: [FULL | target: <resolved>] - stack: <pending detection>`

---

## Step 1 - Stack detection

Detect the primary stack in this priority order. First matching marker wins.

| Stack | Marker file(s) |
|---|---|
| **node-ts** | `tsconfig.json` + `package.json` |
| **node-js** | `package.json` (no `tsconfig.json`) |
| **python** | `pyproject.toml` or `requirements.txt` or `setup.py` |
| **go** | `go.mod` |
| **rust** | `Cargo.toml` |
| **swift** | `Package.swift` or `*.xcodeproj` / `*.xcworkspace` |
| **kotlin** | `build.gradle.kts` or `build.gradle` with `kotlin(` plugin |
| **dotnet** | `*.csproj` or `*.sln` |
| **ruby** | `Gemfile` |
| **java** | `pom.xml` or `build.gradle` (no Kotlin plugin) |
| **generic** | None of the above |

If stack is **generic**: run T1-T8 with generic patterns only, skip framework-specific coverage parsing, announce `No known stack detected - running generic test-quality checks only.`

Update announcement: `Running test-audit - scope: [...] - stack: <detected>`

---

## Step 2 - Framework detection

Within the detected stack, identify the test framework. Parses `package.json` scripts, config files, and dependency declarations.

| Stack | Framework detection |
|---|---|
| **node-ts / node-js** | (a) `vitest` in `devDependencies` or `vitest.config.*` present -> **vitest** (b) `jest` in devDeps or `jest.config.*` -> **jest** (c) `mocha` in devDeps or `.mocharc*` -> **mocha** (d) `"test": "node --test"` in scripts -> **node:test** (e) none -> **unknown** |
| **python** | (a) `pytest` in `[tool.pytest.ini_options]` of `pyproject.toml` or `pytest.ini` -> **pytest** (b) `import unittest` in any test file -> **unittest** (c) none -> **unknown** |
| **go** | Always **testing** (stdlib). Presence of `testify` in `go.mod` noted. |
| **rust** | Always **cargo test**. Tarpaulin presence in dev-dependencies noted. |
| **swift** | (a) `import Testing` in any test file -> **swift-testing** (b) `import XCTest` -> **XCTest** (c) both -> **hybrid** |
| **kotlin** | (a) `kotest-` in dependencies -> **Kotest** (b) `junit` -> **JUnit** (c) none -> **unknown** |
| **dotnet** | (a) `xunit` NuGet package -> **xUnit** (b) `NUnit` -> **NUnit** (c) `MSTest.TestFramework` -> **MSTest** (d) none -> **unknown** |
| **ruby** | (a) `rspec` in `Gemfile` -> **RSpec** (b) `minitest` -> **Minitest** (c) none -> **unknown** |
| **java** | (a) `junit-jupiter` in `pom.xml` or `build.gradle` -> **JUnit 5** (b) `junit` 4.x -> **JUnit 4** (c) `testng` -> **TestNG** (d) none -> **unknown** |

Record: `Framework: <name>` - used to select the correct grep patterns in Step 6.

---

## Step 3 - Test file discovery

Glob by stack convention. Include both co-located and directory-based tests.

| Stack | Glob patterns |
|---|---|
| **node-ts / node-js** | `**/*.test.{ts,tsx,js,jsx,mjs}`, `**/*.spec.{ts,tsx,js,jsx,mjs}`, `**/__tests__/**/*.{ts,tsx,js,jsx,mjs}`, `tests/**/*.{ts,tsx,js,jsx,mjs}` |
| **python** | `test_*.py`, `*_test.py`, `tests/**/*.py` |
| **go** | `**/*_test.go` |
| **rust** | `tests/**/*.rs`, any `src/**/*.rs` with `#[cfg(test)]` module |
| **swift** | `Tests/**/*.swift`, `*Tests/**/*.swift` |
| **kotlin** | `src/test/**/*.kt`, `src/androidTest/**/*.kt` |
| **dotnet** | `**/*Tests*.cs`, `**/*Test.cs`, under any `*.Tests.csproj` |
| **ruby** | `spec/**/*_spec.rb`, `test/**/*_test.rb` |
| **java** | `src/test/**/*.java` |

Respect `target:path:` / `target:file:` if provided.

Announce: `Discovered N test files across K layers.`

If `N == 0`: output `No test files found. Skipping static checks. Coverage reports still parsed if available.` and continue to Step 4.

---

## Step 4 - Coverage report parsing (C1-C3)

Auto-detect coverage report formats. Honor `target:coverage:<path>` if provided.

### C1 - lcov.info (node, rust with tarpaulin --out Lcov)

Search paths: `coverage/lcov.info`, `coverage/lcov-report/`, `lcov.info`, `reports/lcov.info`.

Parse by scanning lines:
- `SF:<file>` starts a file block.
- `LF:<n>` = lines found, `LH:<n>` = lines hit. File coverage = `LH / LF * 100`.
- `BRF:<n>` = branches found, `BRH:<n>` = branches hit.
- `end_of_record` terminates the file block.

Report: total line %, total branch %, files < 50% (top 10 by SLOC), files at 0% (top 10).

### C2 - Istanbul JSON (node)

Search paths: `coverage/coverage-summary.json`, `coverage/coverage-final.json`.

Parse JSON. For `coverage-summary.json`: read the `total` key (`lines.pct`, `branches.pct`, `functions.pct`, `statements.pct`). For `coverage-final.json`: aggregate per-file `s` (statements) / `b` (branches) / `f` (functions).

Report: line %, branch %, function %, statement %.

### C3 - Cobertura XML (python, java, dotnet)

Search paths: `coverage.xml`, `cobertura.xml`, `coverage/cobertura-coverage.xml`, `build/reports/cobertura/coverage.xml`.

Parse XML: `<coverage line-rate="0.873" branch-rate="0.712">` at root. Per-package and per-class rates in child elements.

Report: line %, branch %, top uncovered classes.

### C3 (go) - `coverage.out`

Search paths: `coverage.out`, `cover.out`, `.coverage/coverage.out`.

Parse format: `mode: <mode>\n<file>:<start>,<end> <stmts> <count>`. Total % = `stmts_hit / stmts_total`. Use `Bash` with `go tool cover -func=<file>` if `go` is on PATH for a cleaner summary.

### C3 (rust) - tarpaulin JSON

Search paths: `tarpaulin-report.json`, `target/tarpaulin/report.json`.

Parse JSON: `files[].coverage` percentages, aggregate weighted by file SLOC.

### C3 (swift) - xcresult bundle *(optional)*

If `*.xcresult` directory exists AND `xcrun` is on PATH: run `xcrun xccov view --report --json <file>.xcresult` and parse JSON output. Skip gracefully if xcrun is unavailable (CI or Linux).

### Report format

```
## Coverage (source: <format>)
- Total lines: <X>%
- Branches: <Y>% (if available)
- Functions: <Z>% (if available)
- Files below 50%: <N>
- Files at 0%: <N>
```

### Severity mapping

- Total < 50% line coverage -> **High**
- Total < 80% line coverage -> **Medium**
- Any changed file in the current block at 0% coverage -> **Critical** (requires git diff cross-check; if git history unavailable, report as **High**)
- No coverage report detected -> nudge: `No coverage report detected. Pass target:coverage:<path> or run <test-command> with coverage flag to generate one.` - no severity, just a note.

---

## Step 5 - Pyramid shape (P1-P3)

Classify each test file discovered in Step 3:

| ID | Layer | Path / content heuristic |
|---|---|---|
| **P1** | Unit | Colocated `*.test.*` / `*.spec.*` next to source, or under `tests/unit/**`, `__tests__/**`, `test/unit/**`, `spec/unit/**`, `src/test/**/unit/**`. Default bucket if nothing else matches. |
| **P2** | Integration | Path contains `integration/`, or filename matches `*.integration.test.*` / `*.integration.spec.*` / `*_integration_test.*`. |
| **P3** | E2E | Path contains `e2e/`, `end-to-end/`, `uat/`, `system/`, or filename matches `*.e2e.*`. File imports `@playwright/test`, `cypress`, `detox`, `selenium`, `webdriverio`, `appium`, or (python) `playwright`, `splinter`. |

Compute `N_unit / N_integration / N_e2e` and their percentages of total.

**Verdicts** (target pyramid: ~70/20/10):

| Condition | Severity | Note |
|---|---|---|
| `N_e2e > 30%` | **Medium** | Inverted pyramid - E2E-heavy suite is slow and brittle |
| `N_integration > 50%` | **Medium** | Middle-heavy - consider splitting into unit vs E2E |
| `N_unit == 0 AND total > 0` | **High** | No unit tests - every change goes through slow layers |
| Only one layer present | **Medium** | Narrow coverage - regressions in untested layers will slip through |
| Within 50-85% unit / 10-35% integration / 2-20% E2E | **OK** | Report as balanced |

Report:

```
## Pyramid
- Unit: <N> (<P>%)
- Integration: <N> (<P>%)
- E2E: <N> (<P>%)
- Verdict: [balanced | inverted | middle-heavy | narrow | unit-absent]
```

---

## Step 6 - Static anti-pattern checks (T1-T8)

Run per stack. Read `${CLAUDE_SKILL_DIR}/PATTERNS.md` for stack-specific grep patterns for all checks below. Checks without a matching pattern produce `N/A - skipped for <stack>`, not false positives.

### T1 - `.only` / `fit` / `fdescribe` committed

**Critical.** Focused tests in committed code break CI: suite passes locally but skips everything except the focused case. Grep for focused-test markers using patterns from PATTERNS.md → T1.

Report each match with `FILE:LINE`. If ≥ 2 files have matches, escalate to T8.

### T2 - Skipped tests

**Medium** per match; **High** if skipped tests > 10% of suite. Grep for skip markers using patterns from PATTERNS.md → T2.

Report: count and top 10 with file:line.

### T3 - `.todo` placeholders

**Low.** Placeholder tests that never fail. Flag when count grows; not actionable per instance. See PATTERNS.md → T3.

Report: count only.

### T4 - Empty test bodies

**High.** Empty tests always pass and create false confidence. Grep for empty test body patterns from PATTERNS.md → T4 (multiline regex - use `multiline: true` where supported).

Report: each match with file:line.

### T5 - Tests without assertions

**High.** Test that runs code paths without verifying outcomes.

Heuristic: within a detected test body, grep for at least one of the stack's assertion patterns from PATTERNS.md → T5. Flag any test body that lacks all of its framework's patterns.

Report: each match with file:line. Expect some false positives (helper-only tests, integration setup) - mark findings as `probable` when the body is very short or wraps another function.

### T6 - Hardcoded sleeps in tests

**Medium.** Sleep-based waits are the primary source of flaky tests. Grep for sleep/delay patterns (numeric sleep ≥ 500ms) using PATTERNS.md → T6.

Report: each match with file:line and duration.

### T7 - Debug output left in tests

**Low.** Doesn't affect correctness but adds noise. Grep test files for debug output patterns from PATTERNS.md → T7.

Report: count only. Do not list individually unless count > 20.

### T8 - Multiple `.only` across files (broken CI)

**Critical.** Derived from T1: if ≥ 2 distinct files contain T1 matches, the CI gate is broken in a non-obvious way (each file contributes a focused case, so several cases run but most are skipped).

Report: one aggregate finding with the list of affected files.

---

## Step 7 - Report and backlog decision gate

Generate the report using the template in `${CLAUDE_SKILL_DIR}/REPORT.md`. Apply the severity guide and backlog writing rules from the same file.

---

## Execution notes

- Do NOT modify any test file. Audit only.
- Do NOT execute tests or the test runner. Coverage reports must already exist on disk.
- `target:coverage:<path>` bypasses auto-detection; accept absolute or project-relative path.
- If git history is available, cross-reference C Critical findings against `git diff HEAD~1` (files changed in the most recent commit) to distinguish "0% on pre-existing file" (High) from "0% on file changed in this block" (Critical). If git is not available, downgrade all 0% findings to High.
- This skill complements the Phase 3 test execution (which runs `[TEST_COMMAND]` but never inspects the suite itself). Run in Phase 5d Track C after Phase 3 passes.
- After the report, ask: "Do you want me to prepare the corrections for the identified findings?" Reply with `yes` only after the user has signed off on the specific findings.

Files in this skill

  • PATTERNS.md5.5 KB
  • REPORT.md3.6 KB
  • SKILL.md13.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.