From a288110652e4ef6530c6a6a6274d9dab29d7b0be Mon Sep 17 00:00:00 2001 From: rosspeili Date: Wed, 24 Jun 2026 11:15:35 +0300 Subject: [PATCH 1/2] feat: add skillware test CLI for bundle tests (#83) Delegate to pytest on skills/**/test_skill.py with the same root resolution as list. Supports skill ID, --category, -v, and --no-header. --- CHANGELOG.md | 4 ++ docs/TESTING.md | 2 +- docs/usage/cli.md | 29 ++++++++- skillware/cli.py | 154 +++++++++++++++++++++++++++++++++++++++++++--- tests/test_cli.py | 121 ++++++++++++++++++++++++++++++++++++ 5 files changed, 298 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2f3e4..a3e0908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta - **Documentation**: Added a **Status** section to [TESTING.md](docs/TESTING.md) summarizing the current testing model and planned CLI work (#179). - **Documentation**: Post-release alignment — category tables, Python 3.10+ badge, dev install (`[dev,all]` vs `[dev]`), README configuration via `.env.example`, DeFi env vars in `.env.example`, framework env vars in [api_keys.md](docs/usage/api_keys.md) (#154). +### Added + +- **CLI**: `skillware test` runs bundle tests via pytest — all roots, by skill ID, or by `--category`; supports `-v` and `--no-header` (#83). + ## [0.3.7] - 2026-06-22 ### Added diff --git a/docs/TESTING.md b/docs/TESTING.md index e695694..3e64ffa 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -14,7 +14,7 @@ Tests fall into four layers: **bundle**, **framework**, **maintainer**, and **ex | Bundle tests mock network and model downloads in CI | Done | | Maintainer tests under `tests/skills/` (optional per skill) | Done | | `[all]` extra covers bundle-test runtime deps | Done | -| CLI `skillware test` for bundle discovery | Planned | +| CLI `skillware test` for bundle discovery | Done | Every pull request runs `black --check`, `flake8`, `pytest skills/`, and `pytest tests/`. Bundle tests gate merge the same as framework and maintainer tests. diff --git a/docs/usage/cli.md b/docs/usage/cli.md index 5cb3696..90b8000 100644 --- a/docs/usage/cli.md +++ b/docs/usage/cli.md @@ -76,8 +76,8 @@ Available commands: | Input | Command | Status | | :--- | :--- | :--- | | `1` / `list` | List all locally installed skills | Available | -| `2` / `paths` | Show and repair skill directory resolution paths | Coming in #81 | -| `3` / `test` | Run test_skill.py for one or all skills | Coming in #83 | +| `2` / `paths` | Show and repair skill directory resolution paths | Coming soon | +| `3` / `test` | Run bundle tests (`test_skill.py`) for one or all skills | Available | | `4` / `help` | Print rich-formatted help with commands, flags, and examples | Available | ## Commands @@ -114,6 +114,31 @@ Sample output: # Use a custom skills directory skillware list --skills-root /path/to/my/skills +### skillware test + +Run skill **bundle tests** (`test_skill.py`) via pytest. Uses the same skill roots as `skillware list` (`SKILLWARE_SKILL_PATH`, `--skills-root`, cwd `skills/`, bundled registry). + +Requires pytest (`pip install -e ".[dev]"` or `pip install -e ".[dev,all]"`). + + skillware test + skillware test finance/wallet_screening + skillware test --category compliance + skillware test --verbose + skillware test office/pdf_form_filler --no-header + +#### Arguments and flags + +| Input | Description | +| :--- | :--- | +| *(no args)* | Run bundle tests under all resolved skill roots | +| `/` | Run one skill's `test_skill.py` | +| `--category ` | Run all bundle tests in a category directory | +| `--skills-root ` | Override the skills directory for this command | +| `-v` / `--verbose` | Pass `-v` to pytest | +| `--no-header` | Pass `--no-header` to pytest | + +Exit code matches pytest (non-zero on failures or missing test paths). + ## Path resolution `skillware list` searches for skills in the same order as `SkillLoader`: diff --git a/skillware/cli.py b/skillware/cli.py index 092d503..bd94216 100644 --- a/skillware/cli.py +++ b/skillware/cli.py @@ -1,7 +1,9 @@ import argparse +import subprocess +import sys import yaml from pathlib import Path -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any, Optional, Tuple from rich.table import Table from rich.console import Console @@ -106,6 +108,92 @@ def _discover_skills( return skills +def _resolve_pytest_targets( + skills_root_override: Optional[Path] = None, + skill_id: Optional[str] = None, + category: Optional[str] = None, +) -> Tuple[List[Path], Optional[str]]: + """Build pytest path arguments for bundle tests (skills/**/test_skill.py).""" + if skill_id and category: + return [], "Use either a skill ID or --category, not both." + + roots = _get_skill_roots(skills_root_override) + if not roots: + return [], "No skill roots found. Check --skills-root or SKILLWARE_SKILL_PATH." + + if skill_id: + parts = skill_id.split("/") + if len(parts) != 2 or not all(parts): + return ( + [], + f"Invalid skill ID '{skill_id}'. Expected category/skill_name.", + ) + + category_name, skill_name = parts + searched: List[Path] = [] + for root in roots: + test_path = root / category_name / skill_name / "test_skill.py" + searched.append(test_path) + if test_path.is_file(): + return [test_path], None + + lines = [f"No bundle test found for '{skill_id}'."] + for path in searched: + lines.append(f" looked for: {path}") + return [], "\n".join(lines) + + if category: + targets: List[Path] = [] + searched: List[Path] = [] + for root in roots: + category_dir = root / category + searched.append(category_dir) + if category_dir.is_dir(): + targets.append(category_dir) + + if targets: + return targets, None + + lines = [f"No skills directory found for category '{category}'."] + for path in searched: + lines.append(f" looked for: {path}") + return [], "\n".join(lines) + + return roots, None + + +def cmd_test( + skills_root_override: Optional[Path] = None, + skill_id: Optional[str] = None, + category: Optional[str] = None, + verbose: bool = False, + no_header: bool = False, + console=None, +) -> int: + """Run bundle tests via pytest. Returns pytest's exit code.""" + if console is None: + console = Console(stderr=True) + + targets, error = _resolve_pytest_targets( + skills_root_override=skills_root_override, + skill_id=skill_id, + category=category, + ) + if error: + console.print(error, style="bold #FF9AA2") + return 2 if skill_id and category else 1 + + pytest_args = [sys.executable, "-m", "pytest"] + if verbose: + pytest_args.append("-v") + if no_header: + pytest_args.append("--no-header") + pytest_args.extend(str(path) for path in targets) + + result = subprocess.run(pytest_args, check=False) + return result.returncode + + def cmd_list( skills_root_override: Optional[Path] = None, category_filter: Optional[str] = None, @@ -173,13 +261,16 @@ def cmd_help(console=None) -> None: console.print(" skillware list --category — filter by category") console.print(" skillware list --issuer — filter by issuer") console.print(" skillware list --skills-root — override skills directory") + console.print(" skillware test — run all bundle tests") + console.print(" skillware test — run one skill bundle test") + console.print(" skillware test --category — run tests for a category") console.print(" skillware --version — print installed version") console.print() console.print(Text("Commands", style=f"bold {TABLE_STYLE}")) console.print(" list available now", style=ID_STYLE) - console.print(" paths coming in #81", style="dim") - console.print(" test coming in #83", style="dim") + console.print(" test available now", style=ID_STYLE) + console.print(" paths coming soon", style="dim") console.print() console.print(Text("Interactive mode", style=f"bold {TABLE_STYLE}")) @@ -192,8 +283,8 @@ def cmd_help(console=None) -> None: console.print(Text("Examples", style=f"bold {TABLE_STYLE}")) console.print(" skillware list --category compliance", style=MENU_STYLE) - console.print(" skillware list --issuer rosspeili", style=MENU_STYLE) - console.print(" skillware list --skills-root /path/to/skills", style=MENU_STYLE) + console.print(" skillware test finance/wallet_screening", style=MENU_STYLE) + console.print(" skillware test --category compliance -v", style=MENU_STYLE) console.print() console.print(Text("Install", style=f"bold {TABLE_STYLE}")) @@ -242,8 +333,8 @@ def cmd_interactive(console=None, parser=None) -> None: menu = [ ("1", "list", "discover and display all locally installed skills"), - ("2", "paths (soon, #81)", "show and repair skill directory resolution paths"), - ("3", "test (soon, #83)", "run test_skill.py for one or all skills"), + ("2", "paths (soon)", "show and repair skill directory resolution paths"), + ("3", "test", "run bundle tests (test_skill.py) for one or all skills"), ("4", "help", "usage guide for any command"), ] @@ -275,9 +366,11 @@ def cmd_interactive(console=None, parser=None) -> None: if command == "list": cmd_list(console=console) - elif command in ("paths", "test"): + elif command == "test": + cmd_test(console=console) + elif command == "paths": console.print( - f" '{command}' is not yet implemented. Coming in a future release.", + " 'paths' is not yet implemented. Coming in a future release.", style="dim", ) elif command == "help": @@ -331,6 +424,39 @@ def main() -> None: help="Filter skills by issuer GitHub handle or name.", ) + test_parser = subparsers.add_parser( + "test", + help="Run skill bundle tests (test_skill.py) via pytest.", + ) + test_parser.add_argument( + "skill_id", + nargs="?", + default=None, + help="Skill ID (category/skill_name) to test.", + ) + test_parser.add_argument( + "--skills-root", + type=Path, + default=None, + help="Override the skills directory path.", + ) + test_parser.add_argument( + "--category", + default=None, + help="Run bundle tests for all skills in a category.", + ) + test_parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Pass -v to pytest.", + ) + test_parser.add_argument( + "--no-header", + action="store_true", + help="Pass --no-header to pytest.", + ) + args = parser.parse_args() if args.help and args.command is None: @@ -343,6 +469,16 @@ def main() -> None: category_filter=args.category, issuer_filter=args.issuer, ) + elif args.command == "test": + raise SystemExit( + cmd_test( + skills_root_override=args.skills_root, + skill_id=args.skill_id, + category=args.category, + verbose=args.verbose, + no_header=args.no_header, + ) + ) else: cmd_interactive(parser=parser) diff --git a/tests/test_cli.py b/tests/test_cli.py index 2ac6c25..19018d1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,9 @@ from skillware.cli import ( _discover_skills, + _resolve_pytest_targets, cmd_list, cmd_interactive, + cmd_test, _short_description, cmd_help, ) @@ -279,3 +281,122 @@ def test_version_flag(capsys): captured = capsys.readouterr() assert "skillware" in captured.out.lower() + + +def _make_bundle(tmp_path, category, name, with_test=True): + skill_dir = tmp_path / category / name + skill_dir.mkdir(parents=True) + (skill_dir / "skill.py").touch() + (skill_dir / "manifest.yaml").write_text( + f"name: {name}\nversion: 0.1.0\ndescription: Test.\n" + ) + if with_test: + (skill_dir / "test_skill.py").touch() + return skill_dir + + +def test_resolve_pytest_targets_skill_id(tmp_path): + _make_bundle(tmp_path, "office", "pdf_form_filler") + targets, error = _resolve_pytest_targets( + skills_root_override=tmp_path, + skill_id="office/pdf_form_filler", + ) + assert error is None + assert targets == [tmp_path / "office" / "pdf_form_filler" / "test_skill.py"] + + +def test_resolve_pytest_targets_category(tmp_path): + _make_bundle(tmp_path, "office", "pdf_form_filler") + _make_bundle(tmp_path, "finance", "wallet_screening") + targets, error = _resolve_pytest_targets( + skills_root_override=tmp_path, + category="office", + ) + assert error is None + assert targets == [tmp_path / "office"] + + +def test_resolve_pytest_targets_all_roots(tmp_path): + _make_bundle(tmp_path, "office", "pdf_form_filler") + targets, error = _resolve_pytest_targets(skills_root_override=tmp_path) + assert error is None + assert targets == [tmp_path] + + +def test_resolve_pytest_targets_missing_skill(tmp_path): + targets, error = _resolve_pytest_targets( + skills_root_override=tmp_path, + skill_id="office/missing", + ) + assert targets == [] + assert "No bundle test found" in error + + +def test_resolve_pytest_targets_skill_id_and_category_conflict(): + targets, error = _resolve_pytest_targets( + skill_id="office/pdf_form_filler", + category="office", + ) + assert targets == [] + assert "not both" in error + + +def test_cmd_test_invokes_pytest(tmp_path, monkeypatch): + import sys + + _make_bundle(tmp_path, "office", "pdf_form_filler") + captured = {} + + def fake_run(cmd, check=False): + captured["cmd"] = cmd + + class Result: + returncode = 0 + + return Result() + + monkeypatch.setattr("skillware.cli.subprocess.run", fake_run) + + rc = cmd_test( + skills_root_override=tmp_path, + skill_id="office/pdf_form_filler", + ) + assert rc == 0 + assert captured["cmd"][0] == sys.executable + assert captured["cmd"][1:3] == ["-m", "pytest"] + assert ( + str(tmp_path / "office" / "pdf_form_filler" / "test_skill.py") + in captured["cmd"] + ) + + +def test_cmd_test_verbose_flag(tmp_path, monkeypatch): + _make_bundle(tmp_path, "office", "pdf_form_filler") + captured = {} + + def fake_run(cmd, check=False): + captured["cmd"] = cmd + + class Result: + returncode = 0 + + return Result() + + monkeypatch.setattr("skillware.cli.subprocess.run", fake_run) + + cmd_test( + skills_root_override=tmp_path, + skill_id="office/pdf_form_filler", + verbose=True, + no_header=True, + ) + assert "-v" in captured["cmd"] + assert "--no-header" in captured["cmd"] + + +def test_cmd_test_missing_bundle_returns_nonzero(tmp_path): + rc = cmd_test( + skills_root_override=tmp_path, + skill_id="office/missing", + ) + assert rc == 1 From 16a9baa8fd0cb9a131d6c58bf42bca615aacddf4 Mon Sep 17 00:00:00 2001 From: rosspeili Date: Wed, 24 Jun 2026 11:19:03 +0300 Subject: [PATCH 2/2] docs: cross-link skillware test across contributor guides Align CLI, TESTING, CONTRIBUTING, vision, usage index, template, and PR template so no follow-up doc pass is needed. --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CHANGELOG.md | 4 +-- CONTRIBUTING.md | 10 +++++- docs/TESTING.md | 14 ++++++-- docs/contributing/ai_native_workflow.md | 4 ++- docs/usage/README.md | 4 +-- docs/usage/cli.md | 2 ++ docs/vision.md | 4 +-- skillware/cli.py | 1 + templates/python_skill/README.md | 2 +- tests/test_cli.py | 43 +++++++++++++++++++++++-- 11 files changed, 76 insertions(+), 14 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c9ada7f..a2b2638 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -28,7 +28,7 @@ Humans: Please describe what this PR does and why it's needed. ## Checklist (all PRs) - [ ] My code follows the **Agent Code of Conduct**. -- [ ] I have run `python -m flake8 .`, `pytest skills/`, and `pytest tests/` locally (or the subset relevant to this change). +- [ ] I have run `python -m flake8 .`, `pytest skills/` (or `skillware test`), and `pytest tests/` locally (or the subset relevant to this change). - [ ] `CHANGELOG.md` updated under `[Unreleased]` if this PR changes user-visible behavior. - [ ] `examples/README.md` is updated if this PR adds, renames, or removes a runnable script under `examples/`. diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e0908..5bf35fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,12 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta - **Tests**: `tests/test_skill_issuer.py` now requires `test_skill.py` for every registry skill under `skills/` (#160). - **Documentation**: Clarified that bundle tests must mock network calls and model downloads in CI (#170). -- **Documentation**: Added a **Status** section to [TESTING.md](docs/TESTING.md) summarizing the current testing model and planned CLI work (#179). +- **Documentation**: Added a **Status** section to [TESTING.md](docs/TESTING.md) summarizing the current testing model (#179). - **Documentation**: Post-release alignment — category tables, Python 3.10+ badge, dev install (`[dev,all]` vs `[dev]`), README configuration via `.env.example`, DeFi env vars in `.env.example`, framework env vars in [api_keys.md](docs/usage/api_keys.md) (#154). ### Added -- **CLI**: `skillware test` runs bundle tests via pytest — all roots, by skill ID, or by `--category`; supports `-v` and `--no-header` (#83). +- **CLI**: `skillware test` runs bundle tests via pytest — all roots, by skill ID, or by `--category`; supports `-v` and `--no-header`. Documented in [cli.md](docs/usage/cli.md), [TESTING.md](docs/TESTING.md), and contributor guides (#83). ## [0.3.7] - 2026-06-22 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2b7b85..84fc478 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,12 +127,16 @@ Follow the [Agent Code of Conduct](CODE_OF_CONDUCT.md): deterministic skill outp python -m pytest tests/ ``` + Bundle tests can also be run with `skillware test` (see [CLI reference](docs/usage/cli.md#skillware-test)); requires `[dev]` or `[dev,all]`. + For a single skill: ```bash python -m pytest skills///test_skill.py ``` + Or: `skillware test /`. + - Install packages from that skill's `manifest.yaml` `requirements` when they are not covered by `[all]`. After adding a skill with new third-party deps, update the matching optional extra and `[all]` in `pyproject.toml` (see [Packaging](#packaging-pypi-and-pip-install)). - Wait for GitHub Actions CI to pass before requesting review. @@ -160,6 +164,8 @@ Agents must follow [Agent Contribution Workflow](docs/contributing/ai_native_wor pytest tests/ ``` + Or `skillware test` for bundle tests (see [CLI reference](docs/usage/cli.md#skillware-test)). + For skill work, also run: ```bash @@ -167,6 +173,8 @@ Agents must follow [Agent Contribution Workflow](docs/contributing/ai_native_wor pytest tests/test_skill_issuer.py ``` + Or `skillware test /` for the bundle test only. + 5. **Commit** — Clear imperative message, no emojis; include issue reference when appropriate. Do not add AI tools in `Co-authored-by:` trailers (see [Agent Code of Conduct](CODE_OF_CONDUCT.md#contribution-process)). 6. **Changelog** — If the PR is user-visible, add lines under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md) before opening the PR. 7. **Push** to your fork and open a PR into `ARPAHLS/skillware` `main`. @@ -257,7 +265,7 @@ The primary guide for the host LLM. - **Required** for every new registry skill (template: `templates/python_skill/test_skill.py`; enforced by `tests/test_skill_issuer.py`). - Unit tests for schema compliance and deterministic execution paths (offline; mock externals). - Ships inside the skill bundle via `pip install skillware`. -- Run: `pytest skills///test_skill.py` +- Run: `pytest skills///test_skill.py` or `skillware test /` - Optional extra depth for maintainers: `tests/skills//test_.py` — see [TESTING.md](docs/TESTING.md). - Mock network calls and first-run model downloads in bundle tests. diff --git a/docs/TESTING.md b/docs/TESTING.md index 3e64ffa..db14d10 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -156,6 +156,16 @@ python -m pytest skills/ python -m pytest tests/ ``` +Or use the CLI (same bundle paths; requires `pip install -e ".[dev]"` or `[dev,all]`): + +```bash +skillware test +skillware test / +skillware test --category +``` + +See [CLI reference](usage/cli.md#skillware-test). + Single skill bundle test: ```bash @@ -183,6 +193,6 @@ Before pushing your code, run the following commands: 1. `skillware list` (verify install and path resolution) 2. `python -m black --check .` (verify formatting; use `python -m black .` to fix) 3. `python -m flake8 .` (check quality) -4. `python -m pytest skills/` (bundle tests — same scope as CI) +4. `python -m pytest skills/` or `skillware test` (bundle tests — same scope as CI) 5. `python -m pytest tests/` (framework + maintainer tests — same scope as CI) -6. `python -m pytest skills///test_skill.py` when you want a single-skill subset +6. `python -m pytest skills///test_skill.py` or `skillware test /` for a single skill diff --git a/docs/contributing/ai_native_workflow.md b/docs/contributing/ai_native_workflow.md index 99c198e..42e1831 100644 --- a/docs/contributing/ai_native_workflow.md +++ b/docs/contributing/ai_native_workflow.md @@ -138,6 +138,8 @@ pytest skills/ pytest tests/ ``` +Bundle tests: `skillware test` is equivalent for `skills/**/test_skill.py` (see [CLI reference](../usage/cli.md#skillware-test)). + For a single skill: ```bash @@ -263,7 +265,7 @@ Complete the checklist that matches your issue during Stage 5. - [ ] `skill.py`: deterministic, JSON-serializable returns, safe error handling - [ ] `instructions.md`: when to use, how to interpret output, limitations - [ ] `card.json`: `issuer` matches manifest -- [ ] `test_skill.py` (bundle test) passes — `pytest skills///test_skill.py` +- [ ] `test_skill.py` (bundle test) passes — `pytest skills///test_skill.py` or `skillware test /` - [ ] Bundle tests mock all network calls and model downloads; CI does not download models. - [ ] `docs/skills/.md` and catalog row in `docs/skills/README.md` - [ ] **Usage Examples** on the catalog page (all five providers per [skill usage template](../usage/skill_usage_template.md)); link to `docs/usage/` and list skill `env_vars` without duplicating [api_keys.md](../usage/api_keys.md) diff --git a/docs/usage/README.md b/docs/usage/README.md index cff8428..af268d4 100644 --- a/docs/usage/README.md +++ b/docs/usage/README.md @@ -12,7 +12,7 @@ How to load Skillware skills and connect them to language models. Each guide cov For pip-installed apps, keep project skills in `./skills///` or set `SKILLWARE_SKILL_PATH` to your skills root. -To list all locally available skills from the terminal, see the [CLI reference](cli.md). +To list locally available skills or run bundle tests from the terminal, see the [CLI reference](cli.md). | Provider | Adapter | Guide | Agent API key (typical) | | :--- | :--- | :--- | :--- | @@ -21,7 +21,7 @@ To list all locally available skills from the terminal, see the [CLI reference]( | OpenAI (ChatGPT) | `to_openai_tool()` | [openai.md](openai.md) | `OPENAI_API_KEY` | | DeepSeek | `to_deepseek_tool()` | [deepseek.md](deepseek.md) | `DEEPSEEK_API_KEY` | | Ollama (prompt mode) | `to_ollama_prompt()` | [ollama.md](ollama.md) | (local; no cloud key) | -| CLI | `skillware list` | [cli.md](cli.md) | — | +| CLI | `skillware list`, `skillware test` | [cli.md](cli.md) | pytest in `[dev]` for `test` | Skill-specific **Usage Examples** (sample prompts and execute payloads) live on each [skill catalog page](../skills/README.md). diff --git a/docs/usage/cli.md b/docs/usage/cli.md index 90b8000..c03721b 100644 --- a/docs/usage/cli.md +++ b/docs/usage/cli.md @@ -17,6 +17,7 @@ After installation, the `skillware` command is available directly: skillware skillware list + skillware test skillware --version If `skillware` is not recognized, Python's `Scripts` directory may not be on @@ -35,6 +36,7 @@ as Python is installed): python -m skillware python -m skillware list + python -m skillware test finance/wallet_screening python -m skillware list --category compliance python -m skillware --help diff --git a/docs/vision.md b/docs/vision.md index ba9778e..54b8fd4 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -45,7 +45,7 @@ Multi-layer screening runs locally in one `execute()` call. No generated scraper Skillware is designed so agents and their operators can discover, vet, and integrate capabilities without reinventing the wheel. - **Manifests** declare inputs, outputs, dependencies, and constitution in `manifest.yaml`. -- **`skillware list`** (CLI) surfaces the local registry with categories, issuers, and descriptions. +- **`skillware list`** and **`skillware test`** (CLI) surface the local registry and run bundle tests. - **[Examples index](../examples/README.md)** maps runnable provider scripts to skills. - **[Usage guides](usage/README.md)** show the same load / tool-call / execute loop for Gemini, Claude, OpenAI, DeepSeek, and Ollama. - **[Agent contribution workflow](contributing/ai_native_workflow.md)** documents how supervised agents propose scoped changes and open PRs. @@ -106,7 +106,7 @@ Honest snapshot for **v0** (current v0.3.x line): - **Registry**: Skills under `skills/` with docs in [docs/skills/](skills/README.md). - **Loader**: Dynamic import, dependency checks, and adapters for major LLM tool formats. -- **CLI**: `skillware list` and an interactive menu, included with `pip install skillware`. +- **CLI**: `skillware list`, `skillware test`, and an interactive menu, included with `pip install skillware`. - **Active work**: Wallet screening enhancements ([RFC #115](https://github.com/ARPAHLS/skillware/issues/115)), CLI polish, contributor docs, and good first issues across docs and framework. Browse [open good first issues](https://github.com/ARPAHLS/skillware/issues?q=is%3Aopen+label%3A%22good+first+issue%22) if you want a low-risk entry point. diff --git a/skillware/cli.py b/skillware/cli.py index bd94216..f7ab925 100644 --- a/skillware/cli.py +++ b/skillware/cli.py @@ -283,6 +283,7 @@ def cmd_help(console=None) -> None: console.print(Text("Examples", style=f"bold {TABLE_STYLE}")) console.print(" skillware list --category compliance", style=MENU_STYLE) + console.print(" skillware list --issuer rosspeili", style=MENU_STYLE) console.print(" skillware test finance/wallet_screening", style=MENU_STYLE) console.print(" skillware test --category compliance -v", style=MENU_STYLE) console.print() diff --git a/templates/python_skill/README.md b/templates/python_skill/README.md index 2e43528..adc8583 100644 --- a/templates/python_skill/README.md +++ b/templates/python_skill/README.md @@ -10,7 +10,7 @@ Starter bundle under `skills///`. Copy this template from 4. **`skill.py`**: Implement deterministic logic; no LLM-generated code in the skill body. 5. **`instructions.md`**: Tell the agent when and how to use the tool. 6. **`card.json`**: Mirror `issuer` from the manifest; customize UI fields. -7. **`test_skill.py`**: Bundle test (required; enforced by `tests/test_skill_issuer.py`); offline, mock external services, including HTTP clients, LLM APIs, embedding/model loaders, and any first-run model downloads; run `pytest skills///test_skill.py`. See [TESTING.md](../../docs/TESTING.md). +7. **`test_skill.py`**: Bundle test (required; enforced by `tests/test_skill_issuer.py`); offline, mock external services, including HTTP clients, LLM APIs, embedding/model loaders, and any first-run model downloads; run `pytest skills///test_skill.py` or `skillware test /`. See [TESTING.md](../../docs/TESTING.md). 8. **`docs/skills/.md`**: Catalog page with **ID**, **Issuer**, and **Usage Examples** (all providers; see `docs/usage/skill_usage_template.md`). 9. **`docs/skills/README.md`**: Add a row to the skill library table. diff --git a/tests/test_cli.py b/tests/test_cli.py index 19018d1..05370fe 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -235,7 +235,7 @@ def test_main_module_invocation(): def test_cmd_help_includes_list_examples(capsys): - """cmd_help should include category and issuer examples.""" + """cmd_help should include category, test, and issuer examples.""" import io from rich.console import Console @@ -246,7 +246,7 @@ def test_cmd_help_includes_list_examples(capsys): output = buf.getvalue() assert "--category" in output assert "--issuer" in output - assert "--skills-root" in output + assert "skillware test" in output def test_interactive_help_dispatches_to_cmd_help(monkeypatch): @@ -400,3 +400,42 @@ def test_cmd_test_missing_bundle_returns_nonzero(tmp_path): skill_id="office/missing", ) assert rc == 1 + + +def test_main_test_subcommand_exits_with_cmd_test_code(monkeypatch): + import sys + from skillware.cli import main + + monkeypatch.setattr("skillware.cli.cmd_test", lambda **kwargs: 0) + + argv = sys.argv + sys.argv = ["skillware", "test", "office/pdf_form_filler"] + try: + with pytest.raises(SystemExit) as exc: + main() + assert exc.value.code == 0 + finally: + sys.argv = argv + + +def test_interactive_test_dispatch(tmp_path, monkeypatch): + """Entering 3 or test should dispatch to cmd_test.""" + import io + from rich.console import Console + + _make_bundle(tmp_path, "office", "test_skill") + captured = {} + + def fake_test(**kwargs): + captured["called"] = True + return 0 + + monkeypatch.setattr("skillware.cli.cmd_test", fake_test) + + responses = iter(["test", "q"]) + monkeypatch.setattr("builtins.input", lambda _: next(responses)) + + buf = io.StringIO() + cmd_interactive(console=Console(file=buf, force_terminal=False)) + + assert captured.get("called") is True