diff --git a/.claude/skills/add-feature/SKILL.md b/.claude/skills/add-feature/SKILL.md new file mode 100644 index 0000000..330f6ad --- /dev/null +++ b/.claude/skills/add-feature/SKILL.md @@ -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! diff --git a/.claude/skills/add-feature/skill.md b/.claude/skills/add-feature/skill.md deleted file mode 100644 index 51a2aeb..0000000 --- a/.claude/skills/add-feature/skill.md +++ /dev/null @@ -1,5 +0,0 @@ -# How to Do - -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 and begin the development steps. - -That is all! \ No newline at end of file diff --git a/README.md b/README.md index 763fb5d..815cea0 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/packages/testmap/src/testmap/cli.py b/packages/testmap/src/testmap/cli.py index 1b5a27d..4e9e3aa 100644 --- a/packages/testmap/src/testmap/cli.py +++ b/packages/testmap/src/testmap/cli.py @@ -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.""" @@ -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 `/.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", @@ -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) diff --git a/packages/testmap/src/testmap/skill/SKILL.md b/packages/testmap/src/testmap/skill/SKILL.md new file mode 100644 index 0000000..85bf1d8 --- /dev/null +++ b/packages/testmap/src/testmap/skill/SKILL.md @@ -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).