diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2a6bb..002b9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta ## [Unreleased] +### Added +- **Tests**: Backfilled `test_skill.py` for six registry skills (`mica_module`, `pii_masker`, `synthetic_generator`, `wallet_screening`, `pdf_form_filler`, `prompt_rewriter`); all registry skills now ship co-located bundle tests. Fixed `prompt_rewriter` package export so pytest can collect the bundle (#158). + ### Changed - **CI**: CodeQL GitHub Action upgraded from v3 to v4. - **Dependencies**: Extended `[all]` with registry skill runtime deps (`web3`, `fastembed`, `numpy`); added `[defi]` and `[embeddings]` optional extras. Documented manifest ↔ `pyproject.toml` convention in CONTRIBUTING and TESTING.md. diff --git a/skills/compliance/mica_module/test_skill.py b/skills/compliance/mica_module/test_skill.py new file mode 100644 index 0000000..cf9575e --- /dev/null +++ b/skills/compliance/mica_module/test_skill.py @@ -0,0 +1,67 @@ +import os + +import pytest +import yaml + +from .skill import MiCAModuleSkill + + +@pytest.fixture +def skill(): + return MiCAModuleSkill() + + +@pytest.fixture +def manifest(): + 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): + skill_manifest = skill.manifest + assert skill_manifest["name"] == manifest["name"] + assert skill_manifest["version"] == manifest["version"] + + +def test_stateless_rag_execution(skill): + result = skill.execute( + { + "user_prompt": ( + "I want to issue an asset-referenced token. " + "What are the authorization rules?" + ), + "run_evaluator": False, + } + ) + assert result["policy_status"] == "CAUTION" + assert "retrieved_sections" in result + assert "final_context_for_agent" in result + assert "Evaluator disabled" in result["gemini_evaluator_feedback"]["holes_found"] + + +def test_router_normalization(skill): + mock_corpus = [ + { + "title_num": "Title V", + "article_num": "Article 59", + "keywords": ["authorisation", "casp"], + "content": "CASP Authorization rules...", + } + ] + matched = skill._route_and_fetch("Authorization of a CASP", mock_corpus) + assert len(matched) > 0 + assert matched[0]["article_num"] == "Article 59" + + +def test_router_deduplication(skill): + mock_corpus = [ + { + "title_num": "Title V", + "article_num": "Article 59", + "keywords": ["authorisation", "casp"], + "content": "CASP Authorization rules...", + } + ] + matched = skill._route_and_fetch("authorisation casp", mock_corpus) + assert len(matched) == 1 diff --git a/skills/compliance/pii_masker/test_skill.py b/skills/compliance/pii_masker/test_skill.py new file mode 100644 index 0000000..736d7b2 --- /dev/null +++ b/skills/compliance/pii_masker/test_skill.py @@ -0,0 +1,58 @@ +import os + +import pytest +import yaml + +from .skill import PIIMaskerSkill + + +@pytest.fixture +def skill(): + return PIIMaskerSkill() + + +@pytest.fixture +def manifest(): + 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): + skill_manifest = skill.manifest + assert skill_manifest["name"] == manifest["name"] + assert skill_manifest["version"] == manifest["version"] + + +def test_pii_masker_modes(mocker, skill): + mock_response = ( + "Hello [PERSON_1], your wallet [CRYPTO_ADDRESS] and [EMAIL] have been verified." + ) + mocker.patch.object( + skill, + "_call_ollama", + return_value=(mock_response, ["PERSON_1", "CRYPTO_ADDRESS", "EMAIL"]), + ) + + sample_text = ( + "Hello John Doe, your wallet 0xabc and john@doe.com have been verified." + ) + + result_mask = skill.execute({"text": sample_text}) + assert ( + result_mask["sanitized_text"] + == "Hello [PERSON_1], your wallet [CRYPTO_ADDRESS] and [EMAIL] have been verified." + ) + assert "PERSON" in result_mask["metadata"]["detected_entities"] + assert "CRYPTO_ADDRESS" in result_mask["metadata"]["detected_entities"] + + result_redact = skill.execute({"text": sample_text, "mode": "redact"}) + assert ( + result_redact["sanitized_text"] + == "Hello XXXX, your wallet XXXX and XXXX have been verified." + ) + + result_remove = skill.execute({"text": sample_text, "mode": "remove"}) + assert ( + result_remove["sanitized_text"] == "Hello , your wallet and have been verified." + ) diff --git a/skills/data_engineering/synthetic_generator/test_skill.py b/skills/data_engineering/synthetic_generator/test_skill.py new file mode 100644 index 0000000..dba8d7a --- /dev/null +++ b/skills/data_engineering/synthetic_generator/test_skill.py @@ -0,0 +1,58 @@ +import os + +import pytest +import yaml + +from .skill import SyntheticGeneratorSkill + + +@pytest.fixture +def skill(): + return SyntheticGeneratorSkill() + + +@pytest.fixture +def manifest(): + 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): + skill_manifest = skill.manifest + assert skill_manifest["name"] == manifest["name"] + assert skill_manifest["version"] == manifest["version"] + + +def test_entropy_score(skill): + low_entropy_text = "test " * 100 + score_low = skill._calculate_entropy_score(low_entropy_text) + + high_text = "The brown fox jumps over the dog. Programming is fun!" + score_high = skill._calculate_entropy_score(high_text) + + assert score_high > score_low + + +def test_execute_success(mocker, skill): + mock_json_response = """```json +[ + {"instruction": "x", "input": "y", "output": "z"} +] +```""" + mocker.patch.object(skill, "_call_gemini", return_value=mock_json_response) + + result = skill.execute( + { + "domain": "test domain", + "num_samples": 1, + "diversity_prompt": "be diverse", + "model_provider": "gemini", + } + ) + + assert result["status"] == "success" + assert result["provider_used"] == "gemini" + assert result["samples_generated"] == 1 + assert "samples" in result + assert result["samples"][0]["instruction"] == "x" diff --git a/skills/finance/wallet_screening/test_skill.py b/skills/finance/wallet_screening/test_skill.py new file mode 100644 index 0000000..9ec4fab --- /dev/null +++ b/skills/finance/wallet_screening/test_skill.py @@ -0,0 +1,83 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest +import yaml + +from .skill import WalletScreeningSkill + + +@pytest.fixture +def skill(): + return WalletScreeningSkill() + + +@pytest.fixture +def manifest(): + 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_manifest_schema(manifest): + assert manifest["name"] == "wallet_screening" + assert "address" in manifest["parameters"]["properties"] + + +def test_invalid_address(skill): + result = skill.execute({"address": "invalid_addr"}) + assert "error" in result + assert "Invalid Ethereum address" in result["error"] + + +def test_missing_api_key(skill): + skill.etherscan_api_key = None + result = skill.execute({"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}) + assert "error" in result + assert "Missing ETHERSCAN_API_KEY" in result["error"] + + +@patch("skills.finance.wallet_screening.skill.requests.get") +def test_execute_success(mock_get, skill): + skill.etherscan_api_key = "dummy_key" + + mock_eth_balance = MagicMock() + mock_eth_balance.json.return_value = { + "status": "1", + "result": "1000000000000000000", + } + + mock_txs = MagicMock() + mock_txs.json.return_value = { + "status": "1", + "result": [ + { + "from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".lower(), + "to": "0x123", + "value": "500000000000000000", + "isError": "0", + "gasUsed": "21000", + "gasPrice": "1000000000", + } + ], + } + + mock_price = MagicMock() + mock_price.json.return_value = {"ethereum": {"usd": 2000.0, "eur": 1800.0}} + + def get_side_effect(url, **kwargs): + params = kwargs.get("params") or {} + if params.get("action") == "balance": + return mock_eth_balance + if params.get("action") == "txlist": + return mock_txs + return mock_price + + mock_get.side_effect = get_side_effect + + result = skill.execute({"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"}) + + assert "error" not in result + assert "summary" in result + assert result["summary"]["balance_eth"] == 1.0 + assert result["summary"]["balance_usd"] == 2000.0 diff --git a/skills/office/pdf_form_filler/test_skill.py b/skills/office/pdf_form_filler/test_skill.py new file mode 100644 index 0000000..b2d1940 --- /dev/null +++ b/skills/office/pdf_form_filler/test_skill.py @@ -0,0 +1,84 @@ +import os +from unittest.mock import MagicMock, patch + +import fitz +import pytest +import yaml + +from .skill import PDFFormFillerSkill + + +@pytest.fixture +def skill(): + return PDFFormFillerSkill() + + +@pytest.fixture +def manifest(): + 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): + skill_manifest = skill.manifest + assert skill_manifest["name"] == manifest["name"] + assert skill_manifest["version"] == manifest["version"] + + +def test_missing_pdf_returns_error(skill): + result = skill.execute( + { + "pdf_path": "/nonexistent/form.pdf", + "instructions": "Fill name with Alice", + } + ) + assert "error" in result + assert "PDF file not found" in result["error"] + + +def test_missing_instructions_returns_error(skill, tmp_path): + pdf_path = tmp_path / "blank.pdf" + doc = fitz.open() + doc.new_page() + doc.save(str(pdf_path)) + doc.close() + + result = skill.execute({"pdf_path": str(pdf_path), "instructions": ""}) + assert "error" in result + assert "No instructions provided" in result["error"] + + +@patch("skills.office.pdf_form_filler.skill.anthropic.Anthropic") +def test_execute_mocked(mock_anthropic_cls, tmp_path): + mock_client = mock_anthropic_cls.return_value + mock_message = MagicMock() + mock_message.content = [MagicMock(text='{"page0_test_field": "Hello World"}')] + mock_client.messages.create.return_value = mock_message + + skill = PDFFormFillerSkill() + + pdf_path = tmp_path / "form.pdf" + doc = fitz.open() + page = doc.new_page() + widget = fitz.Widget() + widget.rect = fitz.Rect(10, 10, 100, 30) + widget.field_name = "test_field" + widget.field_type = 7 + page.add_widget(widget) + doc.save(str(pdf_path)) + doc.close() + + result = skill.execute( + { + "pdf_path": str(pdf_path), + "instructions": "Fill test field with Hello World", + } + ) + + assert result["status"] == "success" + assert "page0_test_field" in result["filled_fields"] + assert os.path.exists(result["output_path"]) + + if os.path.exists(result["output_path"]): + os.remove(result["output_path"]) diff --git a/skills/optimization/prompt_rewriter/__init__.py b/skills/optimization/prompt_rewriter/__init__.py index d479929..265a85e 100644 --- a/skills/optimization/prompt_rewriter/__init__.py +++ b/skills/optimization/prompt_rewriter/__init__.py @@ -1,3 +1,3 @@ -from .skill import MyAwesomeSkill +from .skill import PromptRewriter -__all__ = ["MyAwesomeSkill"] +__all__ = ["PromptRewriter"] diff --git a/skills/optimization/prompt_rewriter/test_skill.py b/skills/optimization/prompt_rewriter/test_skill.py new file mode 100644 index 0000000..07f3cbf --- /dev/null +++ b/skills/optimization/prompt_rewriter/test_skill.py @@ -0,0 +1,55 @@ +import os + +import pytest +import yaml + +from .skill import PromptRewriter + + +@pytest.fixture +def skill(): + return PromptRewriter() + + +@pytest.fixture +def manifest(): + 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): + skill_manifest = skill.manifest + assert skill_manifest["name"] == manifest["name"] + assert skill_manifest["version"] == manifest["version"] + + +def test_rewriter_execution_low(skill): + result = skill.execute( + { + "raw_text": "This is a very\n\n\nspaced out prompt.", + "compression_aggression": "low", + } + ) + assert result["compressed_text"] == "This is a very spaced out prompt." + assert result["original_tokens"] >= result["new_tokens"] + + +def test_rewriter_execution_high(skill): + result = skill.execute( + { + "raw_text": "Please make sure to read this and analyze the data.", + "compression_aggression": "high", + } + ) + assert "Please" not in result["compressed_text"] + assert "make sure to" not in result["compressed_text"] + assert result["tokens_saved"] > 0 + assert "new_tokens" in result + assert "original_tokens" in result + + +def test_empty_string_returns_error(skill): + result = skill.execute({"raw_text": ""}) + assert "error" in result + assert result["error"] == "raw_text cannot be empty."