From 4ffe6a04fab6359ceae29c793bbd4a80da1b23a3 Mon Sep 17 00:00:00 2001 From: Polimixanos Date: Tue, 21 Apr 2026 11:44:07 +0300 Subject: [PATCH 1/3] feat: add standard testing scaffolding to python_skill template (#19) This update introduces a pytest-based test_skill.py stub, aligns manifest.yaml with core parameters/outputs standards, and ensures all mandatory boilerplate files (including instructions.md) are present in the template. --- CONTRIBUTING.md | 9 +++-- templates/python_skill/instructions.md | 14 ++++++++ templates/python_skill/manifest.yaml | 18 +++++++--- templates/python_skill/skill.py | 23 ++++++++++--- templates/python_skill/test_skill.py | 47 ++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 templates/python_skill/instructions.md create mode 100644 templates/python_skill/test_skill.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17321e3..7302a90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,13 +75,12 @@ This is the most critical file. It is the "driver" for the LLM. *Wait for approval/feedback before writing code.* 2. **Fork** the repository. 3. **Create** your skill folder: `skills///`. -4. **Implement** the 4 required files (`manifest.yaml`, `skill.py`, `instructions.md`, `card.json`). -5. **Add** a test script in `examples/`. -6. **Verify**: Run linting and tests. See [TESTING.md](docs/TESTING.md) for details. +4. **Implement** the 5 required files (`manifest.yaml`, `skill.py`, `instructions.md`, `card.json`, `test_skill.py`). +5. **Verify**: Run linting and tests locally. + * `pytest skills///test_skill.py` * `python -m black .` * `python -m flake8 .` - * `python -m pytest tests/` -7. **Submit** PR. +6. **Submit** PR. --- diff --git a/templates/python_skill/instructions.md b/templates/python_skill/instructions.md new file mode 100644 index 0000000..72c85bb --- /dev/null +++ b/templates/python_skill/instructions.md @@ -0,0 +1,14 @@ +# Instructions: My Awesome Skill + +You are an agent equipped with the **My Awesome Skill**. + +### When to use this tool +- Use this tool when the user asks for [describe primary use case]. +- Do not use this tool for [describe anti-patterns]. + +### How to interpret the output +- The tool returns a `result` field containing [describe result]. +- If you see an error message, explain to the user that [describe error handling]. + +### Examples +- User: "Run my awesome skill with value X" -> Call `my-awesome-skill(param1="X")` diff --git a/templates/python_skill/manifest.yaml b/templates/python_skill/manifest.yaml index 8daddba..2f2cdeb 100644 --- a/templates/python_skill/manifest.yaml +++ b/templates/python_skill/manifest.yaml @@ -2,14 +2,22 @@ name: "my-awesome-skill" version: "0.1.0" description: "A short description of what this skill does." category: "utility" -inputs: - param1: - type: "string" - description: "Description of the first parameter" +parameters: + type: object + properties: + param1: + type: string + description: "Description of the first parameter" + required: + - param1 outputs: result: - type: "string" + type: string description: "Description of the output" +requirements: [] +constitution: | + 1. Always provide helpful and accurate results. + 2. Do not include sensitive information in the output. presentation: icon: "star" color: "#3498DB" diff --git a/templates/python_skill/skill.py b/templates/python_skill/skill.py index 8adfc82..b30e64e 100644 --- a/templates/python_skill/skill.py +++ b/templates/python_skill/skill.py @@ -5,10 +5,23 @@ class MyAwesomeSkill(BaseSkill): @property def manifest(self) -> Dict[str, Any]: - # You can load this from manifest.yaml or define it here - return {"name": "my-awesome-skill", "version": "0.1.0"} + """ + Returns the skill's manifest. In a production skill, + you can load this from manifest.yaml using SkillLoader. + """ + return { + "name": "my-awesome-skill", + "version": "0.1.0", + "description": "A short description of what this skill does.", + } - def execute(self, params: Dict[str, Any]) -> Any: - # Your skill logic goes here - param1 = params.get("param1") + def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: + """ + The main execution logic for the skill. + Expects 'param1' in params as defined in manifest.yaml. + """ + param1 = params.get("param1", "default") + + # Implement your logic here + return {"result": f"Executed with {param1}"} diff --git a/templates/python_skill/test_skill.py b/templates/python_skill/test_skill.py new file mode 100644 index 0000000..2afd5da --- /dev/null +++ b/templates/python_skill/test_skill.py @@ -0,0 +1,47 @@ +import pytest +import yaml +import os +from .skill import MyAwesomeSkill + +@pytest.fixture +def skill(): + """Fixture to initialize the skill class.""" + return MyAwesomeSkill() + +@pytest.fixture +def manifest(): + """Fixture to load the manifest.yaml for validation.""" + manifest_path = os.path.join(os.path.dirname(__file__), "manifest.yaml") + with open(manifest_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + +def test_skill_manifest_consistency(skill, manifest): + """Verify the skill's internal manifest matches the manifest.yaml file basics.""" + skill_manifest = skill.manifest + assert skill_manifest["name"] == manifest["name"] + assert skill_manifest["version"] == manifest["version"] + +def test_skill_execution(skill, manifest): + """Test the skill execution and validate output schema.""" + # 1. Prepare dummy input + params = {"param1": "test-value"} + + # 2. Execute + result = skill.execute(params) + + # 3. Validate result is a dictionary (JSON serializable) + assert isinstance(result, dict), "Execution result must be a dictionary" + + # 4. Validate against 'outputs' defined in manifest.yaml + expected_outputs = manifest.get("outputs", {}) + for key, spec in expected_outputs.items(): + assert key in result, f"Missing expected output key: '{key}'" + + # Optional: Basic type checking based on manifest + expected_type = spec.get("type") + if expected_type == "string": + assert isinstance(result[key], str), f"Output '{key}' should be a string" + elif expected_type == "integer": + assert isinstance(result[key], int), f"Output '{key}' should be an integer" + elif expected_type == "boolean": + assert isinstance(result[key], bool), f"Output '{key}' should be a boolean" From 2d77d930c96e386414a658ced8b397be2203c274 Mon Sep 17 00:00:00 2001 From: Polimixanos Date: Tue, 21 Apr 2026 11:51:23 +0300 Subject: [PATCH 2/3] docs: update README and TESTING.md to reflect new skill-local test structure --- README.md | 3 ++- docs/TESTING.md | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cace344..7980981 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ Skillware/ │ └── skill_name/ # The Skill bundle │ ├── manifest.yaml # Definition, schema, and constitution │ ├── skill.py # Executable Python logic -│ └── instructions.md # Cognitive map for the LLM +│ ├── instructions.md # Cognitive map for the LLM +│ └── test_skill.py # Unit tests & schema validation ├── skillware/ # Core Framework Package │ └── core/ │ ├── base_skill.py # Abstract Base Class for skills diff --git a/docs/TESTING.md b/docs/TESTING.md index 6bbc832..2f326e6 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -59,9 +59,16 @@ Run the full test suite: python -m pytest tests/ ``` +### Testing Individual Skills +Every skill now comes with a `test_skill.py` boilerplate. You can run tests for a specific skill without running the entire suite: + +```bash +python -m pytest skills///test_skill.py +``` + ### Writing Tests -- Place tests in the `tests/` directory. -- Mimic the structure of the `skills/` or `skillware/` directory you are testing. +- **Global Tests**: Place core framework tests in the `tests/` directory. +- **Skill Tests**: Place skill-specific logic tests in a `test_skill.py` file within the skill's own directory. - Use `conftest.py` for shared fixtures (e.g., mocking LLM clients). ## Pre-Commit Checklist @@ -70,4 +77,5 @@ Before pushing your code, run the following commands to ensure your changes are 1. `python -m black .` (Format code) 2. `python -m flake8 .` (Check quality) -3. `python -m pytest tests/` (Verify functionality) +3. `python -m pytest tests/` (Verify framework functionality) +4. `python -m pytest skills/` (Verify all skills pass their local tests) From 21729f91818940eaf62972e5b146c5b3fbfd15b1 Mon Sep 17 00:00:00 2001 From: Polimixanos Date: Tue, 21 Apr 2026 11:55:27 +0300 Subject: [PATCH 3/3] fix: resolve flake8 linting issues in python_skill template --- templates/python_skill/skill.py | 6 +++--- templates/python_skill/test_skill.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/templates/python_skill/skill.py b/templates/python_skill/skill.py index b30e64e..ce865ad 100644 --- a/templates/python_skill/skill.py +++ b/templates/python_skill/skill.py @@ -6,7 +6,7 @@ class MyAwesomeSkill(BaseSkill): @property def manifest(self) -> Dict[str, Any]: """ - Returns the skill's manifest. In a production skill, + Returns the skill's manifest. In a production skill, you can load this from manifest.yaml using SkillLoader. """ return { @@ -21,7 +21,7 @@ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: Expects 'param1' in params as defined in manifest.yaml. """ param1 = params.get("param1", "default") - + # Implement your logic here - + return {"result": f"Executed with {param1}"} diff --git a/templates/python_skill/test_skill.py b/templates/python_skill/test_skill.py index 2afd5da..2b4a3a2 100644 --- a/templates/python_skill/test_skill.py +++ b/templates/python_skill/test_skill.py @@ -3,11 +3,13 @@ import os from .skill import MyAwesomeSkill + @pytest.fixture def skill(): """Fixture to initialize the skill class.""" return MyAwesomeSkill() + @pytest.fixture def manifest(): """Fixture to load the manifest.yaml for validation.""" @@ -15,28 +17,30 @@ def manifest(): with open(manifest_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) + def test_skill_manifest_consistency(skill, manifest): """Verify the skill's internal manifest matches the manifest.yaml file basics.""" skill_manifest = skill.manifest assert skill_manifest["name"] == manifest["name"] assert skill_manifest["version"] == manifest["version"] + def test_skill_execution(skill, manifest): """Test the skill execution and validate output schema.""" # 1. Prepare dummy input params = {"param1": "test-value"} - + # 2. Execute result = skill.execute(params) - + # 3. Validate result is a dictionary (JSON serializable) assert isinstance(result, dict), "Execution result must be a dictionary" - + # 4. Validate against 'outputs' defined in manifest.yaml expected_outputs = manifest.get("outputs", {}) for key, spec in expected_outputs.items(): assert key in result, f"Missing expected output key: '{key}'" - + # Optional: Basic type checking based on manifest expected_type = spec.get("type") if expected_type == "string":