From 5df61aeeae75f6f983370889707dcec9653d39ed Mon Sep 17 00:00:00 2001 From: Levon Becker Date: Wed, 29 Jul 2026 16:30:07 -0700 Subject: [PATCH] Move check_agents to a pytest test; rename properties.yml.example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_agents moves from modules/tests/ (a click CLI module) to tests/test_check_agents.py (a pytest test), matching the pattern already used in template_ai_vault and ai_vault. Adds pytest/pytest-cov as dependencies, a tests.pytest invoke task, a pytest CI job, and excludes tests/ from ruff/pylint (checked by pytest instead) — same pyproject.toml shape as the other two repos. Also renames properties.yml.example to properties.example.yml so editors recognize and syntax-highlight it as YAML. Co-Authored-By: Claude Sonnet 5 --- .claude/commands/docs.md | 2 +- .claude/skills/docs/SKILL.md | 2 +- .clinerules/workflows/docs.md | 2 +- .github/instructions/index.instructions.md | 12 +- .github/instructions/modules.instructions.md | 4 +- .github/instructions/tasks.instructions.md | 6 +- .github/instructions/tests.instructions.md | 11 +- .github/prompts/docs.prompt.md | 2 +- .github/workflows/tests.yml | 22 ++++ README.md | 20 ++-- modules/README.md | 5 +- modules/tests/README.md | 23 ---- modules/tests/__init__.py | 1 - modules/tests/check_agents.py | 91 --------------- ...ties.yml.example => properties.example.yml | 0 pyproject.toml | 9 +- tasks/combos.py | 1 + tasks/tests.py | 15 ++- template.ignore.yml | 2 +- tests/__init__.py | 0 tests/test_check_agents.py | 41 +++++++ uv.lock | 109 ++++++++++++++++++ 22 files changed, 233 insertions(+), 147 deletions(-) delete mode 100644 modules/tests/README.md delete mode 100644 modules/tests/__init__.py delete mode 100644 modules/tests/check_agents.py rename properties.yml.example => properties.example.yml (100%) create mode 100644 tests/__init__.py create mode 100644 tests/test_check_agents.py diff --git a/.claude/commands/docs.md b/.claude/commands/docs.md index ef0dcc5..f33cff6 100644 --- a/.claude/commands/docs.md +++ b/.claude/commands/docs.md @@ -25,7 +25,7 @@ check: `.claude/commands/*.md`, `.claude/skills/*/SKILL.md`, `.clinerules/workflows/*.md` — these must describe the same behavior; if one changed, the other three need the matching edit (see `.github/instructions/prompts.instructions.md`). -6. **`properties.yml.example`** and any other example/config file describing setup — if `setup.sh`, +6. **`properties.example.yml`** and any other example/config file describing setup — if `setup.sh`, `setup.ps1`, or `modules/setup/properties.py` changed what gets generated. 7. Any other `*.md` file that references a file, command, module, or behavior touched by the diff. diff --git a/.claude/skills/docs/SKILL.md b/.claude/skills/docs/SKILL.md index 13e7b4c..5e31cfe 100644 --- a/.claude/skills/docs/SKILL.md +++ b/.claude/skills/docs/SKILL.md @@ -17,6 +17,6 @@ uv run --no-sync invoke repo.pr_diff ``` Then sweep every doc/AI-config surface the prompt lists (root `README.md`, module `README.md`s, -`.github/instructions/*.md`, `AGENTS.md`/`CLAUDE.md`, the synced command dirs, `properties.yml.example`) +`.github/instructions/*.md`, `AGENTS.md`/`CLAUDE.md`, the synced command dirs, `properties.example.yml`) and fix anything stale directly — this is a repo-local consistency sweep, so no confirmation is needed before editing. diff --git a/.clinerules/workflows/docs.md b/.clinerules/workflows/docs.md index 9912a35..e8c9b1a 100644 --- a/.clinerules/workflows/docs.md +++ b/.clinerules/workflows/docs.md @@ -22,7 +22,7 @@ check: `.claude/commands/*.md`, `.claude/skills/*/SKILL.md`, `.clinerules/workflows/*.md` — these must describe the same behavior; if one changed, the other three need the matching edit (see `.github/instructions/prompts.instructions.md`). -6. **`properties.yml.example`** and any other example/config file describing setup — if `setup.sh`, +6. **`properties.example.yml`** and any other example/config file describing setup — if `setup.sh`, `setup.ps1`, or `modules/setup/properties.py` changed what gets generated. 7. Any other `*.md` file that references a file, command, module, or behavior touched by the diff. diff --git a/.github/instructions/index.instructions.md b/.github/instructions/index.instructions.md index 4914a9e..e564736 100644 --- a/.github/instructions/index.instructions.md +++ b/.github/instructions/index.instructions.md @@ -25,17 +25,18 @@ scoped to one concern — load only the ones relevant to the task at hand: ## Project Structure ``` -pyproject.toml # Dependencies, ruff/pylint config +pyproject.toml # Dependencies, ruff/pylint/pytest config invoke.yml # Invoke config (auto_dash_names: false) setup.sh # Shell-based setup script (uv venv + uv sync) properties.yml # Project configuration (repo path/remote, template path/remote) template.ignore.yml # Paths /template pull/push must never touch (.gitignore handles the rest via git ls-files) +tests/ + test_check_agents.py # Verify .github/prompts/ mirrors are in sync, called by tests.check_agents modules/ common/ # cli.py, properties.py, route_utils.py, utils.py — shared helpers repo/ # pull.py, push.py, log.py, squash.py, rebase.py, pr_*.py — git/PR workflow modules setup/ # properties.py — creates/stamps properties.yml, called by setup.sh/setup.ps1 template/ # ignore.py, naming.py, pull.py, push.py, resolve.py, route.py, scope.py — sync shared tooling with the parent template repo for /template - tests/ # check_agents.py — verify .github/prompts/ mirrors are in sync, called by tests.check_agents versioning/ # libs.py, python.py, workflows.py, upgrade.py, project.py — check pyproject.toml deps & workflow action refs vs. latest releases, bump the repo's VERSION file tasks/ __init__.py # Wires the invoke Collection (debug, repo, ruff, setup, template, tests, upgrade, uv, versioning) plus top-level aliases (fix, test, update) @@ -45,7 +46,7 @@ tasks/ ruff.py # ruff.fix, ruff.format setup.py # setup.properties — creates/stamps properties.yml template.py # template.pull, template.pull_copy, template.push_diff, template.push_apply, template.push_create_pr - tests.py # tests.actionlint, tests.check_agents, tests.pylint, tests.rufflint, tests.yamllint + tests.py # tests.actionlint, tests.check_agents, tests.pylint, tests.pytest, tests.rufflint, tests.yamllint upgrade.py # upgrade (default), upgrade.python, upgrade.libs, upgrade.sync — installs; run ver.update first uv.py # uv.upgrade_bin, uv.upgrade_libs versioning.py # ver.libs, ver.python, ver.workflows, ver.all, ver.update, ver.upgrade, ver.project_bump_build, ver.project_bump_release @@ -53,7 +54,7 @@ tasks/ instructions/ # Copilot instruction files prompts/ # Copilot prompt files (/push, /pull, /squash, /rebase, /fix, /test, /docs, /pr-notes, /pr, /pr-cleanup, /ship-it, /template, /update, /upgrade, /repo, /setup) — source of truth for slash commands skills/ # Optional GitHub Copilot skills (SKILL.md), not a required mirror of every prompt — see skills.instructions.md - workflows/ # tests.yml — ruff + pylint + yamllint + actionlint + check_agents, on PRs to development/main + workflows/ # tests.yml — ruff + pylint + pytest + yamllint + actionlint + check_agents, on PRs to development/main .claude/ commands/ # Claude Code slash commands, hand-maintained mirror of .github/prompts/ (see prompts.instructions.md) skills/ # Claude Code skills (auto-discovered), hand-maintained mirror of .github/prompts/ (see prompts.instructions.md) @@ -83,6 +84,7 @@ addons/ - `invoke` — task runner - `ruff` — fast Python linter/formatter - `pylint` — deep static analysis (10/10 required to pass `invoke test`) +- `pytest` / `pytest-cov` — unit tests (`tests/`, e.g. `test_check_agents.py`) - `yamllint` — YAML linting - `actionlint-py` — GitHub Actions workflow linting (pip-installed, no Homebrew required) - `pyyaml` — reads `properties.yml` @@ -93,7 +95,7 @@ addons/ ## Running Tasks ```sh uv run --no-sync invoke # List all tasks (or: uv run --no-sync invoke -l) -uv run --no-sync invoke test # ruff + pylint + yamllint + actionlint + check_agents +uv run --no-sync invoke test # ruff + pylint + pytest + yamllint + actionlint + check_agents uv run --no-sync invoke fix # Ruff autocorrect + format uv run --no-sync invoke repo.pull # Pull from git remote (stash → pull --rebase → restore) uv run --no-sync invoke repo.push # Push to git remote (fix → test → commit → push) diff --git a/.github/instructions/modules.instructions.md b/.github/instructions/modules.instructions.md index f55e532..b744518 100644 --- a/.github/instructions/modules.instructions.md +++ b/.github/instructions/modules.instructions.md @@ -14,9 +14,11 @@ Modules provide reusable Python logic consumed by invoke tasks, prompts, and scr | `modules/repo/` | Git/PR workflow logic (pull, push, log, squash, rebase, pr) | | `modules/setup/` | Repo bootstrap logic called by `setup.sh`/`setup.ps1` (`properties.py`) | | `modules/template/` | Syncs shared, generic tooling with the parent template repo for `/template` | -| `modules/tests/` | Repo consistency checks called by `tasks/tests.py` (`check_agents.py`) | | `modules/versioning/` | Checks `pyproject.toml` deps and workflow action refs against latest releases, updates locks; bumps the repo's `VERSION` file for deploys/releases (`project.py`) | +Repo consistency checks (`check_agents`) live under root `tests/` as pytest tests, not `modules/` — +see `.github/instructions/tests.instructions.md`. + ## Module Conventions - One concern per file; filename matches the concern in snake_case - Use module-level functions, not classes, unless state genuinely requires it diff --git a/.github/instructions/tasks.instructions.md b/.github/instructions/tasks.instructions.md index 5d845cd..ed6e3f2 100644 --- a/.github/instructions/tasks.instructions.md +++ b/.github/instructions/tasks.instructions.md @@ -56,6 +56,7 @@ namespace.add_collection(my_new_module, name="my_new_module") tests.actionlint(context) tests.check_agents(context) tests.pylint(context) + tests.pytest(context) tests.rufflint(context) tests.yamllint(context) ``` @@ -71,14 +72,15 @@ namespace.add_collection(my_new_module, name="my_new_module") | Task | Command | Description | |------|---------|-------------| | Fix | `uv run --no-sync invoke fix` | Run all auto-fixes (ruff fix + format) | -| Test | `uv run --no-sync invoke test` | Run all tests (actionlint + check_agents + pylint + ruff + yamllint) | +| Test | `uv run --no-sync invoke test` | Run all tests (actionlint + check_agents + pylint + pytest + ruff + yamllint) | ### Test Tasks | Task | Command | Description | |------|---------|-------------| | actionlint | `uv run --no-sync invoke tests.actionlint` | GitHub Actions workflow validation | -| check_agents | `uv run --no-sync invoke tests.check_agents` | Verify `.github/prompts/` is mirrored into `.claude/commands/`, `.claude/skills/`, and `.clinerules/workflows/` | +| check_agents | `uv run --no-sync invoke tests.check_agents` | Verify `.github/prompts/` is mirrored into `.claude/commands/`, `.claude/skills/`, and `.clinerules/workflows/` (`pytest tests/test_check_agents.py`) | | pylint | `uv run --no-sync invoke tests.pylint` | Python code quality | +| pytest | `uv run --no-sync invoke tests.pytest` | Python unit test suite (`tests/`) | | rufflint | `uv run --no-sync invoke tests.rufflint` | Python linting and formatting | | yamllint | `uv run --no-sync invoke tests.yamllint` | YAML file validation | diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md index 6b5f7e9..88516b6 100644 --- a/.github/instructions/tests.instructions.md +++ b/.github/instructions/tests.instructions.md @@ -31,6 +31,7 @@ uv run --no-sync invoke test uv run --no-sync invoke tests.actionlint uv run --no-sync invoke tests.check_agents uv run --no-sync invoke tests.pylint +uv run --no-sync invoke tests.pytest uv run --no-sync invoke tests.rufflint uv run --no-sync invoke tests.yamllint @@ -44,7 +45,8 @@ Do not run `uv run invoke ...` without `--no-sync`. ## When to Run Tests Run tests if you modified: -- `*.py` — any Python file (pylint + ruff) +- `*.py` — any Python file (pylint + ruff); files under `tests/` are excluded from pylint/ruff and + checked by pytest instead - `*.yml` or `*.yaml` — any YAML file (yamllint) - `.github/workflows/*.yml` — GitHub Actions (actionlint + yamllint) - `.github/prompts/`, `.claude/commands/`, `.claude/skills/`, `.clinerules/workflows/` — command mirror @@ -56,10 +58,11 @@ Skip tests for: `*.md` (except the command mirror dirs above), config files, `*. 1. **actionlint** — GitHub Actions workflow validation 2. **check_agents** — `.github/prompts/` is mirrored into `.claude/commands/`, `.claude/skills/`, and - `.clinerules/workflows/` + `.clinerules/workflows/` (a pytest test under `tests/test_check_agents.py`) 3. **pylint** — Python code quality -4. **ruff** — Python linting and formatting -5. **yamllint** — YAML file validation +4. **pytest** — Python unit test suite (`tests/`) +5. **ruff** — Python linting and formatting +6. **yamllint** — YAML file validation ## Fix Issues — Never Disable Warnings diff --git a/.github/prompts/docs.prompt.md b/.github/prompts/docs.prompt.md index 7eda4ce..013ac74 100644 --- a/.github/prompts/docs.prompt.md +++ b/.github/prompts/docs.prompt.md @@ -25,7 +25,7 @@ check: `.claude/commands/*.md`, `.claude/skills/*/SKILL.md`, `.clinerules/workflows/*.md` — these must describe the same behavior; if one changed, the other three need the matching edit (see `.github/instructions/prompts.instructions.md`). -6. **`properties.yml.example`** and any other example/config file describing setup — if `setup.sh`, +6. **`properties.example.yml`** and any other example/config file describing setup — if `setup.sh`, `setup.ps1`, or `modules/setup/properties.py` changed what gets generated. 7. Any other `*.md` file that references a file, command, module, or behavior touched by the diff. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 25530a2..90d19bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,6 +75,28 @@ jobs: uv run --no-sync invoke debug.env uv run --no-sync invoke tests.pylint + pytest: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check Out Code + uses: actions/checkout@v7 + + - name: Setup uv + uses: astral-sh/setup-uv@v8.3.2 + with: + python-version: "3.14" + enable-cache: true + + - name: Install Dependencies + run: uv sync + + - name: Pytest + run: | + uv run --no-sync invoke debug.env + uv run --no-sync invoke tests.pytest + ruff_lint: runs-on: ubuntu-latest permissions: diff --git a/README.md b/README.md index e4a843a..58c9a72 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,18 @@ path before running tasks. ## Project Structure ``` -pyproject.toml # Dependencies (Python >=3.14), ruff/pylint config +pyproject.toml # Dependencies (Python >=3.14), ruff/pylint/pytest config invoke.yml # Invoke config (auto_dash_names: false) setup.sh # Shell-based setup script (uv venv + uv sync) properties.yml # Project configuration (repo path/remote, template path/remote) template.ignore.yml # Paths /template pull/push must never touch (.gitignore handles the rest via git ls-files) +tests/ + test_check_agents.py # Verify .github/prompts/ mirrors are in sync, called by tests.check_agents modules/ common/ # cli.py, properties.py, route_utils.py, utils.py — shared helpers repo/ # pull.py, push.py, log.py, squash.py, rebase.py, pr_*.py — git/PR workflow modules setup/ # properties.py — creates/stamps properties.yml, called by setup.sh/setup.ps1 template/ # ignore.py, naming.py, pull.py, push.py, resolve.py, route.py, scope.py — sync shared tooling with the parent template repo for /template - tests/ # check_agents.py — verify .github/prompts/ mirrors are in sync, called by tests.check_agents versioning/ # libs.py, python.py, workflows.py, upgrade.py, project.py — check pyproject.toml deps & workflow action refs vs. latest releases, bump the repo's VERSION file tasks/ __init__.py # Wires the invoke Collection (debug, repo, ruff, setup, template, tests, upgrade, uv, versioning) plus top-level aliases (fix, test, update) @@ -36,7 +37,7 @@ tasks/ ruff.py # ruff.fix, ruff.format setup.py # setup.properties — creates/stamps properties.yml template.py # template.pull, template.pull_copy, template.push_diff, template.push_apply, template.push_create_pr - tests.py # tests.actionlint, tests.check_agents, tests.pylint, tests.rufflint, tests.yamllint + tests.py # tests.actionlint, tests.check_agents, tests.pylint, tests.pytest, tests.rufflint, tests.yamllint upgrade.py # upgrade (default), upgrade.python, upgrade.libs, upgrade.sync — installs; run ver.update first uv.py # uv.upgrade_bin, uv.upgrade_libs versioning.py # ver.libs, ver.python, ver.workflows, ver.all, ver.update, ver.upgrade, ver.project_bump_build, ver.project_bump_release @@ -45,7 +46,7 @@ tasks/ prompts/ # /push, /pull, /squash, /rebase, /fix, /test, /docs, /pr-notes, /pr, /pr-cleanup, /ship-it, /template, /update, /upgrade, /repo, /setup — source of truth skills/ # Optional GitHub Copilot skills (SKILL.md), not a required mirror of every prompt workflows/ - tests.yml # CI: ruff + pylint + yamllint + actionlint + check_agents, on PRs to development/main + tests.yml # CI: ruff + pylint + pytest + yamllint + actionlint + check_agents, on PRs to development/main .claude/ commands/ # Claude Code slash commands, hand-maintained mirror of .github/prompts/ skills/ # Claude Code skills (auto-discovered), hand-maintained mirror of .github/prompts/ @@ -61,7 +62,7 @@ addons/ ## Invoke Tasks ```sh uv run --no-sync invoke # List all available tasks -uv run --no-sync invoke test # Ruff + Pylint + yamllint + actionlint + check_agents +uv run --no-sync invoke test # Ruff + Pylint + pytest + yamllint + actionlint + check_agents uv run --no-sync invoke fix # Ruff autocorrect + format uv run --no-sync invoke update # Run every version check (libs, python, workflows) — top-level alias for ver.update uv run --no-sync invoke repo.pull # Pull from git remote (stash → pull --rebase → restore) @@ -121,9 +122,12 @@ uv run --no-sync invoke setup.properties # Create/stamp properties.yml | [`modules/repo/`](modules/repo/README.md) | Git workflow and session logging | | [`modules/setup/`](modules/setup/README.md) | Creates/stamps `properties.yml`, called by `setup.sh`/`setup.ps1` | | [`modules/template/`](modules/template/README.md) | Sync shared, generic tooling with the parent template repo for `/template` | -| [`modules/tests/`](modules/tests/README.md) | Verify `.github/prompts/` is mirrored into `.claude/commands/`, `.claude/skills/`, and `.clinerules/workflows/` | | [`modules/versioning/`](modules/versioning/README.md) | Check `pyproject.toml` deps and workflow action refs vs. latest releases, update locks; bump the repo's `VERSION` file for deploys/releases | +`tests/test_check_agents.py` (not a `modules/` package) verifies `.github/prompts/` is mirrored +into `.claude/commands/`, `.claude/skills/`, and `.clinerules/workflows/` — run via +`uv run --no-sync invoke tests.check_agents`. + See [modules/README.md](modules/README.md) for full details. ## Addons @@ -144,6 +148,6 @@ they aren't meant to lint clean from this repo's root. | [`addons/shopfiy_dawn_theme/`](addons/shopfiy_dawn_theme/) | `dawn.list` / `dawn.upgrade` invoke tasks for tracking and staging upstream Shopify/dawn theme upgrades | `modules/dawn/` (`__init__.py`, `list.py`, `upgrade.py`, `README.md`), `tasks/dawn.py`, `.github/workflows/{deploy,promote,release,tests,upgrade}.yml`, `.github/instructions/dawn.instructions.md`, `.github/prompts/dawn.prompt.md`, `.claude/commands/dawn.md` | ## CI -GitHub Actions runs Ruff, Pylint, yamllint, actionlint, and check_agents on every pull request -targeting `development` or `main` via `.github/workflows/tests.yml`. +GitHub Actions runs Ruff, Pylint, pytest, yamllint, actionlint, and check_agents on every pull +request targeting `development` or `main` via `.github/workflows/tests.yml`. diff --git a/modules/README.md b/modules/README.md index 4f5324b..5cbdc0d 100644 --- a/modules/README.md +++ b/modules/README.md @@ -11,11 +11,13 @@ modules/ repo/ # pull, push, log, squash, rebase (git workflow) setup/ # creates/stamps properties.yml, called by setup.sh/setup.ps1 template/ # sync shared, generic tooling with the parent template repo for /template - tests/ # verify .github/prompts/ mirrors are in sync, called by tasks/tests.py versioning/ # check pyproject.toml deps and workflow action refs vs. latest releases, update locks; # bump the repo's VERSION file for deploys/releases ``` +Repo-consistency checks (`check_agents`) live under root `tests/` as pytest tests, not here — see +`../tests/test_check_agents.py` and `.github/instructions/tests.instructions.md`. + ## Submodules | Directory | Purpose | @@ -24,7 +26,6 @@ modules/ | [`repo/`](repo/README.md) | Git workflow, session logging, squash, and rebase | | [`setup/`](setup/README.md) | Creates/stamps `properties.yml`, called by `setup.sh`/`setup.ps1` | | [`template/`](template/README.md) | Sync shared, generic tooling with the parent template repo for `/template` | -| [`tests/`](tests/README.md) | Verify `.github/prompts/` is mirrored into `.claude/commands/`, `.claude/skills/`, and `.clinerules/workflows/` | | [`versioning/`](versioning/README.md) | Check `pyproject.toml` deps and workflow action refs vs. latest releases, update locks; bump the repo's `VERSION` file for deploys/releases | ## Conventions diff --git a/modules/tests/README.md b/modules/tests/README.md deleted file mode 100644 index bc7104a..0000000 --- a/modules/tests/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Tests Module -Repo-local consistency checks, called by `tasks/tests.py`. - -## Commands -```sh -uv run --no-sync invoke tests.check_agents -``` - -## What It Does -`check_agents.py` verifies that every `.github/prompts/*.prompt.md` file (the source of truth for -slash commands — see `.github/instructions/prompts.instructions.md`) has a matching mirror in each -of the other three tool-specific command dirs: - -- `.claude/commands/.md` -- `.claude/skills//SKILL.md` -- `.clinerules/workflows/.md` - -It reports any prompt missing a mirror, and any mirror file left over with no matching prompt -(e.g. after a command is renamed or removed), then exits non-zero if anything is out of sync. - -## Files -- `check_agents.py` — verifies `.github/prompts/` mirrors (used by `inv tests.check_agents`) -- `README.md` — this file diff --git a/modules/tests/__init__.py b/modules/tests/__init__.py deleted file mode 100644 index a4fb060..0000000 --- a/modules/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Repo consistency check modules.""" diff --git a/modules/tests/check_agents.py b/modules/tests/check_agents.py deleted file mode 100644 index c548463..0000000 --- a/modules/tests/check_agents.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Verify .github/prompts/ is mirrored into .claude/commands/, .claude/skills/, and .clinerules/workflows/.""" - -from __future__ import annotations - -import os -from pathlib import Path - -from ..common import cli as click -from ..common.route_utils import find_repo_root -from ..common.utils import error, success - -# This file's fixed depth below the repo root (modules/tests/check_agents.py -> repo root). -_REPO_ROOT_DEPTH = 2 - - -def _resolve_repo_root() -> Path: - """Return the repo root. - - In CI, properties.yml (the anchor find_repo_root() searches for) is gitignored and never - created, so resolve via this file's fixed position relative to the repo root instead. - Locally, prefer find_repo_root() so the check still works when invoked from a subdirectory. - """ - if os.environ.get("CI"): - return Path(__file__).resolve().parents[_REPO_ROOT_DEPTH] - return find_repo_root() - - -def _prompt_names(repo_path: Path) -> set[str]: - """Return the command names sourced from .github/prompts/*.prompt.md.""" - prompts_dir = repo_path / ".github" / "prompts" - return {p.name.removesuffix(".prompt.md") for p in prompts_dir.glob("*.prompt.md")} - - -def _command_names(repo_path: Path) -> set[str]: - """Return the command names mirrored into .claude/commands/*.md.""" - commands_dir = repo_path / ".claude" / "commands" - return {p.stem for p in commands_dir.glob("*.md")} - - -def _skill_names(repo_path: Path) -> set[str]: - """Return the command names mirrored into .claude/skills/*/SKILL.md.""" - skills_dir = repo_path / ".claude" / "skills" - return {p.parent.name for p in skills_dir.glob("*/SKILL.md")} - - -def _clinerules_names(repo_path: Path) -> set[str]: - """Return the command names mirrored into .clinerules/workflows/*.md.""" - workflows_dir = repo_path / ".clinerules" / "workflows" - return {p.stem for p in workflows_dir.glob("*.md")} - - -def _report_diff(label: str, expected: set[str], actual: set[str]) -> bool: - """Print any mismatch between the prompt set and a mirror's set; return whether they match.""" - missing = sorted(expected - actual) - extra = sorted(actual - expected) - if not missing and not extra: - return True - if missing: - click.echo(f" Missing from {label}: {', '.join(missing)}", err=True) - if extra: - click.echo(f" Orphaned in {label} (no matching prompt): {', '.join(extra)}", err=True) - return False - - -@click.command() -def main() -> None: - """Verify every .github/prompts/*.prompt.md has a matching mirror in the other three command dirs.""" - repo_path = _resolve_repo_root() - prompts = _prompt_names(repo_path) - - click.echo(f"Checking {len(prompts)} prompt(s) are mirrored into commands, skills, and clinerules...") - - mirrors = { - "commands": _command_names(repo_path), - "skills": _skill_names(repo_path), - "clinerules": _clinerules_names(repo_path), - } - - in_sync = True - for label, actual in mirrors.items(): - if not _report_diff(label, prompts, actual): - in_sync = False - - if not in_sync: - error("Command mirrors are out of sync — see .github/instructions/prompts.instructions.md.") - - success("All prompt mirrors are in sync") - - -if __name__ == "__main__": - main() # pylint: disable=no-value-for-parameter diff --git a/properties.yml.example b/properties.example.yml similarity index 100% rename from properties.yml.example rename to properties.example.yml diff --git a/pyproject.toml b/pyproject.toml index bae54ca..9c6d2eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ dependencies = [ "actionlint-py~=1.7.12.24", "invoke~=3.0.3", "pylint~=4.0.6", + "pytest~=9.1.1", + "pytest-cov~=7.1.0", "pyyaml~=6.0.3", "ruff~=0.15.22", "tomlkit~=0.15.1", @@ -27,7 +29,7 @@ repository = "https://github.com/LevonBecker/template_python" [tool.ruff] line-length = 120 target-version = "py314" -extend-exclude = ["addons"] +extend-exclude = ["addons", "tests/*"] [tool.ruff.lint] select = ["PL", "E", "W", "F", "I", "B", "A", "C4", "N", "UP", "T10", "EXE", "COM819", "C", "Q"] @@ -55,6 +57,10 @@ line-ending = "auto" docstring-code-format = false docstring-code-line-length = "dynamic" +[tool.pytest.ini_options] +testpaths = ["tests"] +norecursedirs = ["tmp", ".venv", ".ruff_cache", "__pycache__", "addons"] + [tool.pylint.main] py-version = "3.14" fail-under = 10 @@ -67,6 +73,7 @@ ignore-paths = [ "^tmp/.*$", ".*__pycache__.*$", "^addons/.*$", + "^tests/.*$", ] [tool.pylint.format] diff --git a/tasks/combos.py b/tasks/combos.py index 80c4f54..2498a53 100644 --- a/tasks/combos.py +++ b/tasks/combos.py @@ -16,6 +16,7 @@ def test(context): tests.actionlint(context) tests.check_agents(context) tests.pylint(context) + tests.pytest(context) tests.rufflint(context) tests.yamllint(context) diff --git a/tasks/tests.py b/tasks/tests.py index 4c3d6e1..4e3733c 100644 --- a/tasks/tests.py +++ b/tasks/tests.py @@ -1,7 +1,5 @@ from invoke import task -from modules.tests import check_agents as check_agents_module - @task def actionlint(context): @@ -13,12 +11,12 @@ def actionlint(context): @task -def check_agents(_context): +def check_agents(context): """Verify .github/prompts/ is mirrored into .claude/commands/, .claude/skills/, and .clinerules/workflows/""" print("\n------------") print("Check Agents") print("------------\n") - check_agents_module.main() + context.run("pytest tests/test_check_agents.py") @task @@ -30,6 +28,15 @@ def pylint(context): context.run("pylint --verbose --rcfile=pyproject.toml .") +@task +def pytest(context): + """Run Pytest Unit Test Suite""" + print("\n------------") + print("Pytest") + print("------------\n") + context.run("pytest") + + @task def rufflint(context): """Run Ruff Linter on Entire Repo""" diff --git a/template.ignore.yml b/template.ignore.yml index c66de28..39f478c 100644 --- a/template.ignore.yml +++ b/template.ignore.yml @@ -12,7 +12,7 @@ exclude: - .vscode/** - addons/** - properties.yml - - properties.yml.example + - properties.example.yml - README.md - LICENSE - uv.lock diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_check_agents.py b/tests/test_check_agents.py new file mode 100644 index 0000000..89a12db --- /dev/null +++ b/tests/test_check_agents.py @@ -0,0 +1,41 @@ +"""Verify .github/prompts/ is mirrored into .claude/commands/, .claude/skills/, and .clinerules/workflows/.""" + +from __future__ import annotations + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def _prompt_slugs() -> set[str]: + return {p.name.removesuffix(".prompt.md") for p in (REPO_ROOT / ".github" / "prompts").glob("*.prompt.md")} + + +def _command_slugs() -> set[str]: + return {p.stem for p in (REPO_ROOT / ".claude" / "commands").glob("*.md")} + + +def _skill_slugs() -> set[str]: + return {p.parent.name for p in (REPO_ROOT / ".claude" / "skills").glob("*/SKILL.md")} + + +def _clinerules_slugs() -> set[str]: + return {p.stem for p in (REPO_ROOT / ".clinerules" / "workflows").glob("*.md")} + + +def _assert_mirrored(label: str, expected: set[str], actual: set[str]) -> None: + missing = sorted(expected - actual) + extra = sorted(actual - expected) + assert not missing and not extra, f"{label} out of sync — missing={missing} extra={extra}" + + +def test_claude_commands_mirror_prompts() -> None: + _assert_mirrored("`.claude/commands/`", _prompt_slugs(), _command_slugs()) + + +def test_claude_skills_mirror_prompts() -> None: + _assert_mirrored("`.claude/skills/`", _prompt_slugs(), _skill_slugs()) + + +def test_clinerules_workflows_mirror_prompts() -> None: + _assert_mirrored("`.clinerules/workflows/`", _prompt_slugs(), _clinerules_slugs()) diff --git a/uv.lock b/uv.lock index 008b676..bf4fd8f 100644 --- a/uv.lock +++ b/uv.lock @@ -26,6 +26,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, +] + [[package]] name = "dill" version = "0.4.1" @@ -35,6 +74,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "invoke" version = "3.0.3" @@ -62,6 +110,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -80,6 +137,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pylint" version = "4.0.6" @@ -98,6 +173,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/da/acb2e7d4dbd2dfb792d38c0d850481f29ad7049b356d23f56c687d35203b/pylint-4.0.6-py3-none-any.whl", hash = "sha256:d11a0e1fdb7b1cd46ec5d6fc78fee8b95f28695b2d6140e5809925f61e32ea54", size = 538389, upload-time = "2026-06-14T14:43:24.873Z" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -132,6 +237,8 @@ dependencies = [ { name = "actionlint-py" }, { name = "invoke" }, { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, { name = "pyyaml" }, { name = "ruff" }, { name = "tomlkit" }, @@ -143,6 +250,8 @@ requires-dist = [ { name = "actionlint-py", specifier = "~=1.7.12.24" }, { name = "invoke", specifier = "~=3.0.3" }, { name = "pylint", specifier = "~=4.0.6" }, + { name = "pytest", specifier = "~=9.1.1" }, + { name = "pytest-cov", specifier = "~=7.1.0" }, { name = "pyyaml", specifier = "~=6.0.3" }, { name = "ruff", specifier = "~=0.15.22" }, { name = "tomlkit", specifier = "~=0.15.1" },