Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .claude/skills/add-feature/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: add-feature
description: >-
Use when asked to add a feature or fix an issue in this repository's source
code. Points to the development steps documented in AGENTS.md.
---

# add-feature

You've been asked to add a feature or fix an issue in the source code. All of
this information can be found in the AGENTS.md file in the root of the project.
Please read it and begin the development steps.

That is all!
5 changes: 0 additions & 5 deletions .claude/skills/add-feature/skill.md

This file was deleted.

15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,21 @@ Missing:
• parse: unit
```

## Claude / agent skill

testmap ships a [Claude Code](https://docs.claude.com/en/docs/claude-code) skill
that teaches an agent how to use the package — reading the report, tagging tests,
selecting which functions to map, and writing each test kind. Install it into a
project (or globally) with:

```bash
testmap install-skill # → ./.claude/skills/testmap-do/
testmap install-skill --user # → ~/.claude/skills/ (every project)
```

Agents can then act on requests like `/testmap-do fill out the map for functions`
or `/testmap-do skip private functions in this file`.

## Packages

This is a [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/)
Expand Down
41 changes: 41 additions & 0 deletions packages/testmap/src/testmap/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

import argparse
import json
import shutil
import tomllib
from importlib import resources
from pathlib import Path

from testmap.discover import discover
from testmap.report import build_report, load_config, render

# The skill directory name; also the slash-command name (`/testmap-do`).
SKILL_NAME = "testmap-do"


def _default_paths(pyproject: Path) -> list[Path]:
"""Testpaths from `[tool.pytest.ini_options]`, falling back to the cwd."""
Expand All @@ -27,9 +32,39 @@ def _load_records(path: Path | None, config_path: Path) -> list[dict[str, str]]:
return discover(paths)


def _install_skill(target: Path) -> Path:
"""Copy the bundled Claude skill into `<target>/.claude/skills/testmap-do/`."""
dest = target / ".claude" / "skills" / SKILL_NAME
dest.mkdir(parents=True, exist_ok=True)
src = resources.files("testmap") / "skill" / "SKILL.md"
shutil.copyfile(str(src), dest / "SKILL.md")
return dest


def main() -> None:
parser = argparse.ArgumentParser(prog="testmap")
sub = parser.add_subparsers(dest="command", required=True)

skill = sub.add_parser(
"install-skill", help="install the Claude skill so agents know how to use testmap"
)
group = skill.add_mutually_exclusive_group()
group.add_argument(
"--project",
dest="scope",
action="store_const",
const="project",
help="install into ./.claude/skills (default)",
)
group.add_argument(
"--user",
dest="scope",
action="store_const",
const="user",
help="install into ~/.claude/skills (available in every project)",
)
skill.set_defaults(scope="project")

report = sub.add_parser("report", help="render a testmap report from source or a JSON file")
report.add_argument(
"path",
Expand All @@ -47,6 +82,12 @@ def main() -> None:
)
args = parser.parse_args()

if args.command == "install-skill":
base = Path.home() if args.scope == "user" else Path.cwd()
dest = _install_skill(base)
print(f"Installed {SKILL_NAME} skill to {dest}")
return

config = load_config(args.config)
tests = _load_records(args.path, args.config)
result = build_report(tests, config)
Expand Down
93 changes: 93 additions & 0 deletions packages/testmap/src/testmap/skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
name: testmap-do
description: >-
Use in projects that have testmap installed (a [tool.testmap] table in
pyproject.toml, or @testmap-tagged tests). Covers running the report, tagging
tests, selecting which functions to map, and writing each test kind (unit,
integration, property, perf) simply. Trigger on requests like "fill out the
testmap", "map these functions", "add the missing test kinds", "skip private
functions".
---

# testmap-do

testmap tracks, per **feature**, which **kinds** of test exist (`unit`,
`integration`, `property`, `perf`, …) and flags required kinds that are missing.
A feature is any name you choose — usually a function or class.

## Read the current map first

```
testmap report # scan testpaths, print the matrix, exit 1 if gaps
testmap report --json # same, machine-readable
```

Rows are features; columns are kinds; the `Missing:` section lists the gaps to
fill. `n/a` means the kind is excluded for that feature. Always start here — fill
exactly what `Missing:` reports, nothing more.

## Tag a test

One decorator per test. `feature` and `kind` are required strings.

```python
from pytest_testmap import testmap

@testmap(feature="parser", kind="unit")
def test_parses_empty():
assert parse("") == []
```

- `feature` = the thing under test (match existing feature names in the report).
- `kind` = one of the taxonomy kinds in `[tool.testmap] kinds`.
- A test with no `@testmap` decorator is simply ignored by testmap.

## Which functions to map

The expected feature universe can be declared in `pyproject.toml` so untested
functions show up as all-missing rows instead of vanishing:

```toml
[[tool.testmap.generate]]
select = "functions" # or "classes"
from = "src/**/*.py" # glob, relative to pyproject.toml
where = "public" # public | private | all (private = leading underscore)
```

Only top-level defs count. When a user says **"map the functions"**, add/adjust a
generate block (or just tag tests for each public function). **"skip private
functions"** → `where = "public"`. **"only this file"** → narrow `from`. After
editing config, re-run `testmap report` to see the new gaps.

## Writing each kind, simply

Pick the feature and kind from `Missing:`, write the smallest honest test, tag it,
re-run `testmap report`. Keep tests minimal — the goal is evidence a kind was
considered, not exhaustive coverage.

- **unit** — call the function directly, assert on the return for a representative
input (and an edge case). The default kind.
- **integration** — exercise it through a real neighbor (its caller, the CLI, a
temp file, a real object it collaborates with) rather than in isolation.
- **property** — assert an invariant over generated inputs (e.g. round-trip
`decode(encode(x)) == x`, idempotence, sortedness). Use Hypothesis if present;
otherwise a small loop over a handful of inputs is fine.
- **perf** — either (a) assert a real bound (`assert elapsed < 0.1`, complexity
stays linear), or (b) if perf isn't meaningfully at risk here, write a passing
test whose body/comment records that you considered it and found it irrelevant:

```python
@testmap(feature="parser", kind="perf")
def test_perf():
# Pure string parsing on small inputs; no perf-sensitive path. Considered, N/A.
assert True
```

Prefer excluding the kind in config (`exclude = ["perf"]`) if a feature never
needs it, rather than writing stub tests for every one.

## Loop

1. `testmap report` → read `Missing:`.
2. For each gap, write one small tagged test (above).
3. `testmap report` again → confirm the row is `✓` (exit 0).
Loading