skeleton-test-coverage-uplift
Write test suites for SKELETON packages (zero test coverage) to uplift them to DEVELOPMENT maturity. Handles non-importable modules, optional dependencies, separate sub-repos, and Flask/web packages without runtime.
Best use case
skeleton-test-coverage-uplift is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Write test suites for SKELETON packages (zero test coverage) to uplift them to DEVELOPMENT maturity. Handles non-importable modules, optional dependencies, separate sub-repos, and Flask/web packages without runtime.
Teams using skeleton-test-coverage-uplift should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/skeleton-test-coverage-uplift/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How skeleton-test-coverage-uplift Compares
| Feature / Agent | skeleton-test-coverage-uplift | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Write test suites for SKELETON packages (zero test coverage) to uplift them to DEVELOPMENT maturity. Handles non-importable modules, optional dependencies, separate sub-repos, and Flask/web packages without runtime.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
SKILL.md Source
# SKELETON → DEVELOPMENT Test Coverage Uplift
Write test suites for packages with zero test coverage to move them from SKELETON to DEVELOPMENT maturity.
## When to Use
- Maturity matrix shows SKELETON packages (source files but 0 tests)
- Issue calls for test coverage on untested packages
- Overnight batch prompt targets coverage uplift
## Process
### 1. Read ALL source files first
Read every .py file in the package before writing tests. Understand:
- What classes/functions exist and their signatures
- What external dependencies are required (matplotlib, Flask, etc.)
- Whether modules are importable (some are raw scripts with undefined globals)
- What dataclasses/Pydantic models exist for validation tests
### 2. Check what already exists
Before creating files, check if test directories/files already exist:
```bash
git status --short tests/{package_name}/
```
The test files may already be tracked but have bugs (wrong arg names, etc.). Fix rather than overwrite.
### 3. Categorize modules by testability
| Category | Strategy |
|----------|----------|
| Pure functions (math, dataclasses) | Direct unit tests with known inputs/outputs |
| Classes with optional deps | `pytest.importorskip("dep")` at module level |
| Raw scripts (undefined globals) | Source analysis tests (read file, check patterns) |
| Web/Flask modules | Source pattern analysis without Flask runtime |
| Modules with external I/O | Mock everything; test structure not behavior |
### 4. Write tests following this priority
1. **Import tests** — verify package is importable
2. **Class instantiation** — create objects with valid params
3. **Config/schema validation** — if Pydantic/dataclass models exist
4. **Pure function tests** — known-input/known-output for math functions
5. **Edge cases** — invalid inputs, zero/negative values, missing keys
6. **Source analysis** — when module can't be imported, read source and verify patterns
## Key Pitfalls
### Optional dependencies cause collection errors
If a test file has `import matplotlib` at top level and matplotlib isn't in the venv, the ENTIRE test module fails to collect (not just individual tests).
**Fix:** Use `pytest.importorskip()` instead of bare imports:
```python
# BAD — fails entire module if missing
import matplotlib
matplotlib.use("Agg")
# GOOD — skips entire module gracefully
matplotlib = pytest.importorskip("matplotlib")
matplotlib.use("Agg")
```
### Wrong function signatures in tests
Read the actual function signature before writing tests. Common mistakes:
- `current_velocity_m_s` vs `current_velocity_ms` (underscore convention)
- Missing required params (e.g., `water_depth_m` for monopile scour)
- Wrong keyword names from docstring vs actual parameter names
**Fix:** Always read the source, don't guess from naming conventions.
### Sub-repos vs workspace repos
The workspace-hub pattern has sub-repos (e.g., `digitalmodel/`) that are gitignored in the parent. You must:
- `cd` into the sub-repo to commit
- Use `git pull origin main` from inside the sub-repo
- `git add` + `git commit` from inside the sub-repo, not the parent
### `from __future__ import annotations` breaks dataclass import via importlib
When a script uses `from __future__ import annotations` AND `@dataclass`, importing it via `importlib.util.spec_from_file_location()` fails on Python 3.11 with `AttributeError: 'NoneType' object has no attribute '__dict__'` in `dataclasses._is_type()`. This happens because the module isn't registered in `sys.modules` during dynamic import, so deferred annotation evaluation can't resolve type hints.
**Fix:** In scripts that will be dynamically imported by tests:
- Remove `from __future__ import annotations`
- Use `from typing import Dict, List, Optional` with explicit `List[str]`, `Dict[str, int]`, `Optional[int]` instead of `list[str]`, `dict[str, int]`, `int | None`
- Or register the module in `sys.modules` before `exec_module()`:
```python
spec = importlib.util.spec_from_file_location("my_module", path)
mod = importlib.util.module_from_spec(spec)
sys.modules["my_module"] = mod # <-- fixes the issue
spec.loader.exec_module(mod)
```
### Finding actual collection errors in noisy pytest output
When running `pytest --co` on a large monorepo, finding the ACTUAL collection errors is hard because:
1. `grep "ERROR"` matches hundreds of test names containing "error" (e.g., `test_invalid_units_raises_value_error`)
2. The `short test summary info` section with `ERROR tests/path/file.py` lines may not appear in stdout due to capture bugs
3. The INTERNALERROR traceback at the end can mask the real error section
4. Error counts vary between runs when `pytest-randomly` is active
**Reliable technique:**
```bash
# Step 1: Disable noisy plugins, disable capture to get full output
uv run pytest --co -p no:randomly -p no:sugar -p no:capture tests/ 2>&1 > /tmp/co.txt
# Step 2: Find the short test summary (the only reliable error list)
grep "^ERROR tests/" /tmp/co.txt
# Step 3: Find INTERNALERROR lines (plugin bugs, not test errors)
grep "^INTERNALERROR>" /tmp/co.txt | head -5
# Step 4: Get the summary line
grep "collected.*error" /tmp/co.txt
```
Key: `-p no:capture` prevents the `ValueError: I/O operation on closed file` crash that hides the error section. The `^ERROR tests/` pattern with anchored regex avoids matching test names.
**Scoping pitfall:** In monorepos with nested sub-repos (e.g., `digitalmodel/` inside `workspace-hub/`), `pytest --co` from the sub-repo may collect BOTH sub-repo AND parent tests via conftest.py chaining. Always pass the explicit `tests/` path argument.
### Per-package pytest for large monorepos
Running `pytest digitalmodel/tests/` on a large monorepo with 6000+ tests and 150+ collection errors fails — it hits `--maxfail=50` and stops, or with `--maxfail=0 --continue-on-collection-errors` it crashes with `ValueError: I/O operation on closed file`. No individual test results are produced.
**Fix:** Run pytest per subdirectory to isolate collection errors:
```python
for sub in sorted(tests_dir.iterdir()):
if not sub.is_dir() or sub.name.startswith("_"):
continue
result = subprocess.run(
[sys.executable, "-m", "pytest", str(sub), "-v", "--tb=no", "-q"],
capture_output=True, text=True, timeout=120,
)
# Parse the summary line from result.stdout
```
This gives per-package pass/fail/skip/error counts even when some packages have collection errors. The monolithic run approach is unreliable for repos with many optional dependencies.
### Sandbox overlay vs mounted filesystem paths
When working with repos on mounted paths (e.g., `/mnt/local-analysis/workspace-hub/digitalmodel/`), `write_file` and `patch` tools write to the sandbox overlay, NOT the actual mount. Git sees the mount's version. Two approaches:
1. **Direct `open()` in `execute_code`** — bypasses sandbox, writes to the real mount:
```python
from hermes_tools import terminal
with open("/mnt/local-analysis/.../file.py", 'w') as f:
f.write(content)
```
2. **Do NOT use `read_file` output as file content** — it includes line number prefixes (` 1|"""`) that corrupt the file. If you need to copy content between overlay and mount, use raw `open()` for both reading and writing within `execute_code`.
**Pitfall sequence:** `write_file` creates clean file in overlay → pytest sees overlay and passes → `git add` sees mount (old file) → commit captures wrong content → `__init__.py` import fails at runtime with `IndentationError` because line numbers are embedded.
**Safe workflow for mounted repos:**
- Use `write_file`/`patch` for editing (tests run against overlay fine)
- Before `git commit`, verify mount has correct content: `head -3 /mnt/.../file.py`
- If corrupted, rewrite via `execute_code` with raw `open()`
### __init__.py import chains break ALL package tests
When `package/__init__.py` eagerly imports from submodules (e.g., `from .sn_curves import ...`), and ANY submodule requires an uninstalled dependency, EVERY test that touches the package fails — even tests that only import a different submodule. The error is a `ModuleNotFoundError` during `__init__.py` collection.
**Diagnosis:** The traceback will show:
```
tests/fatigue/test_sn_library_api.py:14: in <module>
from digitalmodel.fatigue.sn_library_api import ...
src/digitalmodel/fatigue/__init__.py:18: in <module>
from .sn_curves import get_sn_curve, DNV_CURVES
ModuleNotFoundError: No module named 'pylife'
```
**Fixes (pick one):**
1. **Install the missing dep:** `uv pip install pylife` — preferred if it's a legitimate dependency
2. **Lazy import in __init__.py:** wrap optional imports in try/except
3. **Direct submodule import in tests:** `from digitalmodel.fatigue.sn_library import ...` instead of going through `__init__.py` (fragile — breaks if other code imports the package normally)
**Key insight:** Before running tests on a package for the first time, check `__init__.py` imports and verify all deps are installed. One missing dep blocks the entire package test suite.
### Bytecode compilation timeout on first run
The first `uv run pytest` in a session compiles ~32K bytecode files (2+ minutes). Set `timeout=300` for the first pytest invocation. Subsequent runs take ~20-25s. If a test run times out, just re-run — the compilation is cached.
### Real fixture data may vary from assumptions
When using real spec.yml files as test fixtures (e.g., L00 validation cases), don't hard-assert a single expected value for polymorphic fields like `analysis_type`. Some cases use `diffraction`, others `full_qtf`. Use `assert x in (expected_set)` instead.
### Non-importable modules (raw scripts) — refactoring to importable
Some files (e.g., `reservoir/stratigraphic.py`) use undefined global variables like `WELL1`, `df_logs`. These cannot be imported. When refactoring these:
1. Wrap all logic in functions with proper signatures
2. Add input validation that raises `ValueError` on bad inputs
3. Add `if __name__ == '__main__':` guard
4. Write tests using `sys.modules` mock injection for missing deps
**Mocking matplotlib when not installed** — use autouse fixture to inject mocks into `sys.modules` AND invalidate cached imports of the target module so it re-imports with mocks:
```python
@pytest.fixture(autouse=True)
def mock_matplotlib(monkeypatch):
"""Inject mock matplotlib when it's not installed in the test venv."""
import sys
mock_mpl = MagicMock()
mock_pyplot = MagicMock()
mock_colors = MagicMock()
mock_colors.ListedColormap.return_value = MagicMock()
mock_mpl.pyplot = mock_pyplot
mock_mpl.colors = mock_colors
monkeypatch.setitem(sys.modules, "matplotlib", mock_mpl)
monkeypatch.setitem(sys.modules, "matplotlib.pyplot", mock_pyplot)
monkeypatch.setitem(sys.modules, "matplotlib.colors", mock_colors)
# CRITICAL: invalidate cached imports of the module under test
for key in list(sys.modules.keys()):
if "stratigraphic" in key:
monkeypatch.delitem(sys.modules, key, raising=False)
return mock_pyplot
```
**Pitfall:** After this mock, `patch.object(module.plt, "subplots", ...)` won't work because `module.plt` IS the mock. Instead, configure the mock directly:
```python
# BAD — patch.object on an already-mocked attribute
with patch.object(stratigraphic.plt, "subplots", return_value=(mock_fig, mock_axes)):
...
# GOOD — configure the mock's return value directly
stratigraphic.plt.subplots.return_value = (mock_fig, mock_axes)
stratigraphic.plt.Normalize.return_value = MagicMock()
fig = stratigraphic.create_cross_section(wells_list, df_logs, statdata)
```
### Web/Flask packages without Flask installed
When Flask is not in the venv (69 source files, 0 testable at runtime):
**Strategy: Source pattern analysis tests**
- Verify directory structure (all blueprint dirs exist, have __init__.py)
- Read source files and check for Blueprint declarations, route patterns
- Use `@pytest.mark.parametrize` over all expected blueprints for coverage
- Check for auth patterns, CRUD operations, template references
This generated 102 tests from 5 files for a 69-file Flask app with zero Flask dependency:
```python
EXPECTED_BLUEPRINTS = ["todorestful", "BuoySALM", "GoMFields", ...]
@pytest.mark.parametrize("bp_name", EXPECTED_BLUEPRINTS)
def test_blueprint_directory_exists(self, bp_name):
bp_dir = os.path.join(_dtf_root(), bp_name)
assert os.path.isdir(bp_dir)
```
### Shared conftest.py for mocked external APIs
When testing modules that depend on licensed/unavailable APIs (OrcFxAPI, ANSYS, etc.), create a shared `conftest.py` with:
- Fake type classes (FakeLine, FakeModel, etc.) that mimic real API objects
- A fixture that injects fake module into `sys.modules` via `monkeypatch.setitem`
- A fixture that returns a pre-built fake model instance
This lets pytest auto-discover fixtures across all test files without duplication:
```python
# tests/orcaflex/conftest.py
@pytest.fixture
def fake_orcfxapi(monkeypatch):
fake = types.SimpleNamespace(Line=FakeLine, Model=FakeModel, oeEndA=1)
monkeypatch.setitem(sys.modules, "OrcFxAPI", fake)
return fake
@pytest.fixture
def fake_model():
return FakeModel()
```
### YAML safe_load parses timestamps as datetime objects
`yaml.safe_load()` converts ISO 8601 timestamps (e.g., `2026-04-01T12:00:00Z`) to `datetime` objects, NOT strings. Tests that compare parsed YAML timestamps must handle both types:
```python
# BAD — fails because safe_load returns datetime, not str
assert health["last_completed_at"] == "2026-04-01T12:00:00Z"
# GOOD — convert to string for comparison
last = str(health["last_completed_at"])
assert "2026-04-01" in last and "12:00:00" in last
```
### HTML class counting includes CSS style blocks
When asserting HTML content, `html.count("section-card")` counts occurrences everywhere — including CSS `.section-card { ... }` rules. Use the full attribute selector:
```python
# BAD — counts CSS references too
assert html.count("section-card") == 8
# GOOD — counts only actual element class attributes
div_count = html.count('class="section-card"')
```
### Route matching patterns
Flask routes may include `methods=["GET"]` which changes the string match:
```python
# Source has: @dtf.route("/", methods=["GET"])
# This assertion FAILS:
assert '@dtf.route("/")' in src
# This assertion PASSES:
assert '@dtf.route("/", methods=["GET"])' in src
```
## Test file template
```python
# ABOUTME: Tests for digitalmodel.{package} — SKELETON → DEVELOPMENT uplift.
"""Tests for {package} — imports, calculations, edge cases."""
import os
import pytest
class TestPackageImport:
def test_import_package(self):
import digitalmodel.{package}
assert digitalmodel.{package} is not None
class TestModuleExistence:
def _package_dir(self) -> str:
import digitalmodel.{package}
return os.path.dirname(digitalmodel.{package}.__file__)
def test_expected_module_exists(self):
assert os.path.isfile(os.path.join(self._package_dir(), "module.py"))
```
## Acceptance criteria (from maturity rules)
SKELETON → DEVELOPMENT requires:
- ≥1 test file per package
- ≥3 test functions recommended (import + instantiation + edge case minimum)
- All tests pass without external resources
- Tests are in subdirectory format: `tests/{package_name}/test_{package}.py`Related Skills
test-oversized-skill
A test fixture skill that exceeds 200 lines with multiple H2/H3 sections for split testing.
llm-wiki-source-extraction-coverage
Doc-type-aware extraction contract for llm-wiki source ingestion with measurable coverage and source-anchored traceability. Use when (1) ingesting a PDF, DOCX, XLSX, PPTX, HTML, or scanned-image source into a wiki `sources/` page, (2) computing the pre-extraction estimate (what fraction of the source we expect to recover) and post-extraction yield (what fraction we actually recovered), (3) anchoring wiki claims back to specific page / paragraph / cell / slide positions in the source so a reviewer can re-verify or revise against the actual document, (4) deciding whether OCR fallback or manual transcription is needed. Codifies workspace-hub's existing OCR fallback chain and python-docx / openpyxl / trafilatura patterns into a format-specific routing table. Companion to research/llm-wiki-page-shape-contract (Rule 7 input-layer pages) and research/llm-wiki — this skill is the defense against silent extraction failure.
digitalmodel-worktree-test-execution-with-shared-venv
Run digitalmodel tests from isolated worktrees without uv editable-dependency failures by using the main repo's existing virtualenv and PYTHONPATH.
blender-worktree-test-hardening
Recover and harden digitalmodel Blender automation work in isolated worktrees when uv/editable dependency paths break and local machines lack a Blender executable.
work-around-merge-conflicts-in-test-execution
Run tests when repo has unresolved merge conflicts in config files by bypassing broken configs and executing tests directly
test-driven-hook-debugging
Debugging and fixing shell hooks by writing isolated test suites first, then using test failures to pinpoint logic bugs
preserved-plan-refile-with-attested-review-wave
Reopen a previously closed GitHub issue with a preserved local plan, rewrite it into a conservative draft, and drive iterative attested adversarial review waves until it is truly approval-ready.
dummy-backfill-test
Temporary validation skill used to test skill backfill and commit flows.
test-driven-development
Use when implementing any feature or bugfix, before writing implementation code. Enforces RED-GREEN-REFACTOR cycle with test-first approach.
webapp-testing
Web application testing toolkit using Playwright with Python. Use for verifying frontend functionality, debugging UI behavior, capturing browser screenshots, viewing browser logs, and automating web interactions.
testing-tdd-london
TDD London School (mockist) specialist for mock-driven, outside-in development. Use for behavior verification testing, contract-driven development, testing object collaborations, or when focusing on HOW objects interact rather than WHAT they contain.
testing-production
Production validation specialist ensuring applications are fully implemented and deployment-ready. Use to verify no mock/stub/fake implementations remain, validate against real databases and APIs, perform end-to-end testing with actual systems, and confirm production readiness.