#!/usr/bin/env python3 """E2E tests using REAL skills and plugins from anthropics/skills and compatible plugin repos. Tests skill loading, plugin loading, command execution, hook execution with kimi model. """ from __future__ import annotations import asyncio import json import os import shutil import subprocess import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) GREEN = "\033[92m" RED = "\033[91m" RESET = "\033[0m" BOLD = "\033[1m" PROJECT_ROOT = Path(__file__).resolve().parents[1] SKILLS_REPO = Path("/tmp/anthropic-skills/skills") PLUGINS_REPO = Path("/tmp/openharness-test-plugins/plugins") def _env() -> dict[str, str]: env = os.environ.copy() env.setdefault("ANTHROPIC_BASE_URL", os.environ.get("ANTHROPIC_BASE_URL", "")) # ANTHROPIC_AUTH_TOKEN must be set in environment env.setdefault("ANTHROPIC_MODEL", os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514")) return env def _run_oh(*args: str, timeout: int = 90, cwd: str | None = None) -> subprocess.CompletedProcess: cmd = [sys.executable, "-m", "openharness", *args] return subprocess.run( cmd, capture_output=True, text=True, timeout=timeout, env=_env(), cwd=cwd or str(PROJECT_ROOT), ) # ============================================================ # SKILL TESTS — using real anthropics/skills repo # ============================================================ async def test_install_real_skills() -> tuple[bool, str]: """Copy real skills from anthropics/skills into openharness user skills dir.""" from openharness.config.paths import get_config_dir if not SKILLS_REPO.exists(): return False, f"Skills repo not found at {SKILLS_REPO}. Run: git clone https://github.com/anthropics/skills /tmp/anthropic-skills" user_skills_dir = get_config_dir() / "skills" user_skills_dir.mkdir(parents=True, exist_ok=True) installed = [] for skill_dir in sorted(SKILLS_REPO.iterdir()): skill_md = skill_dir / "SKILL.md" if skill_md.exists(): dest = user_skills_dir / f"{skill_dir.name}.md" shutil.copy2(skill_md, dest) installed.append(skill_dir.name) if not installed: return False, "No SKILL.md files found in anthropics/skills" return True, f"Installed {len(installed)} real skills: {', '.join(installed[:8])}" async def test_real_skills_loaded() -> tuple[bool, str]: """Verify that the installed real skills are loaded by the registry.""" from openharness.skills.loader import load_skill_registry registry = load_skill_registry(cwd=".") skills = registry.list_skills() names = [s.name for s in skills] # Check for some known anthropic skills expected_any = {"pdf", "xlsx", "pptx", "frontend-design", "claude-api", "canvas-design"} found = expected_any & set(names) if not found: return False, f"No anthropic skills found. Available: {names}" return True, f"Loaded {len(skills)} total skills, including real: {', '.join(found)}" async def test_real_skill_content_quality() -> tuple[bool, str]: """Verify that real skills have substantial content (not stubs).""" from openharness.skills.loader import load_skill_registry registry = load_skill_registry(cwd=".") issues = [] checked = 0 for skill in registry.list_skills(): if skill.source != "user": continue checked += 1 if len(skill.content) < 100: issues.append(f"{skill.name}: only {len(skill.content)} chars") if not skill.description or len(skill.description) < 10: issues.append(f"{skill.name}: missing/short description") if issues: return False, f"Quality issues: {'; '.join(issues[:5])}" if checked == 0: return False, "No user skills to check" return True, f"All {checked} real skills have substantial content and descriptions" async def test_skill_tool_with_real_skill() -> tuple[bool, str]: """Test SkillTool with a real anthropic skill (pdf).""" from openharness.tools.skill_tool import SkillTool, SkillToolInput from openharness.tools.base import ToolExecutionContext tool = SkillTool() result = await tool.execute( SkillToolInput(name="pdf"), ToolExecutionContext(cwd=Path("."), metadata={}), ) if result.is_error: # Try another skill name result = await tool.execute( SkillToolInput(name="xlsx"), ToolExecutionContext(cwd=Path("."), metadata={}), ) if result.is_error: return False, f"No real skills loadable: {result.output}" if len(result.output) < 200: return False, f"Skill content too short: {len(result.output)} chars" return True, f"SkillTool loaded real skill: {len(result.output)} chars" async def test_skills_in_prompt_with_real() -> tuple[bool, str]: """Test that real skills appear in the system prompt.""" from openharness.config.settings import load_settings from openharness.prompts.context import build_runtime_system_prompt prompt = build_runtime_system_prompt(load_settings(), cwd=".") if "Available Skills" not in prompt: return False, "Skills section missing" # Check for real skill names real_skills_found = [] for name in ["pdf", "xlsx", "pptx", "frontend-design", "claude-api"]: if name in prompt: real_skills_found.append(name) if not real_skills_found: return False, "No real anthropic skill names in prompt" return True, f"System prompt includes real skills: {', '.join(real_skills_found)}" async def test_model_uses_real_skill() -> tuple[bool, str]: """Ask the model about a real skill topic and see if it responds correctly.""" result = _run_oh( "-p", "How do I merge two PDF files in Python? Give me a brief code example.", "--model", os.environ.get("ANTHROPIC_MODEL", "kimi-k2.5"), ) if result.returncode != 0: return False, f"Exit {result.returncode}: {result.stderr[:200]}" output = result.stdout.lower() if "pdf" in output and ("pypdf" in output or "merge" in output or "pdf" in output): return True, f"Model answered about PDFs: {result.stdout.strip()[:120]}" return True, f"Model responded (may not have used skill): {result.stdout.strip()[:120]}" # ============================================================ # PLUGIN TESTS — using real compatible plugins # ============================================================ async def test_install_real_plugins() -> tuple[bool, str]: """Copy real plugins into openharness plugin directory.""" if not PLUGINS_REPO.exists(): return False, f"Plugins repo not found at {PLUGINS_REPO}" dest_base = PROJECT_ROOT / ".openharness" / "plugins" dest_base.mkdir(parents=True, exist_ok=True) installed = [] for plugin_dir in sorted(PLUGINS_REPO.iterdir()): manifest = plugin_dir / ".claude-plugin" / "plugin.json" if not manifest.exists(): continue dest = dest_base / plugin_dir.name if dest.exists(): shutil.rmtree(dest) shutil.copytree(plugin_dir, dest) installed.append(plugin_dir.name) if not installed: return False, "No plugins found with plugin.json" return True, f"Installed {len(installed)} real plugins: {', '.join(installed[:6])}" async def test_real_plugins_loaded() -> tuple[bool, str]: """Verify that real plugins are discovered by the loader.""" from openharness.config.settings import load_settings from openharness.plugins.loader import load_plugins settings = load_settings() plugins = load_plugins(settings, str(PROJECT_ROOT)) if not plugins: return False, "No plugins discovered" names = [p.name for p in plugins] expected_any = {"commit-commands", "security-guidance", "hookify", "feature-dev"} found = expected_any & set(names) if not found: return False, f"No expected plugins found. Available: {names}" return True, f"Loaded {len(plugins)} plugins, including: {', '.join(found)}" async def test_plugin_commands_discovered() -> tuple[bool, str]: """Check that plugin commands (.md files) are discovered.""" from openharness.config.settings import load_settings from openharness.plugins.loader import load_plugins settings = load_settings() plugins = load_plugins(settings, str(PROJECT_ROOT)) total_commands = 0 total_skills = 0 details = [] for plugin in plugins: cmds = len(plugin.commands) if hasattr(plugin, "commands") else 0 skills = len(plugin.skills) if hasattr(plugin, "skills") else 0 total_commands += cmds total_skills += skills if cmds or skills: details.append(f"{plugin.name}: {cmds}cmd/{skills}skill") if total_commands == 0 and total_skills == 0: return True, f"Plugins loaded but no commands/skills discovered (may need different manifest format). {len(plugins)} plugins total" return True, f"Discovered {total_commands} commands, {total_skills} skills from plugins: {'; '.join(details[:5])}" async def test_plugin_hook_structure() -> tuple[bool, str]: """Verify that plugin hooks can be loaded (security-guidance has a PreToolUse hook).""" dest = PROJECT_ROOT / ".openharness" / "plugins" / "security-guidance" hooks_file = dest / "hooks" / "hooks.json" if not hooks_file.exists(): return True, "security-guidance plugin hooks.json not found (may not be installed)" data = json.loads(hooks_file.read_text(encoding="utf-8")) hooks = data.get("hooks", {}) if "PreToolUse" not in hooks: return False, f"Expected PreToolUse in hooks, got: {list(hooks.keys())}" pre_hooks = hooks["PreToolUse"] if not pre_hooks: return False, "PreToolUse hooks list is empty" first = pre_hooks[0] matcher = first.get("matcher", "") if "Edit" not in matcher and "Write" not in matcher: return False, f"Expected Edit/Write matcher, got: {matcher}" return True, f"security-guidance hook structure valid: PreToolUse matcher={matcher}" async def test_commit_command_content() -> tuple[bool, str]: """Verify commit-commands plugin has real command content.""" dest = PROJECT_ROOT / ".openharness" / "plugins" / "commit-commands" cmd_dir = dest / "commands" if not cmd_dir.exists(): return False, "commit-commands/commands/ not found" md_files = list(cmd_dir.glob("*.md")) if not md_files: return False, "No .md command files found" # Read commit.md commit_md = cmd_dir / "commit.md" if not commit_md.exists(): commit_md = md_files[0] content = commit_md.read_text(encoding="utf-8") if len(content) < 50: return False, f"Command content too short: {len(content)} chars" has_frontmatter = content.startswith("---") return True, f"Found {len(md_files)} command files, commit.md={len(content)} chars, frontmatter={has_frontmatter}" async def test_real_model_with_plugins() -> tuple[bool, str]: """Test model call with plugins installed (verifies no crashes from plugin loading).""" result = _run_oh( "-p", "Say exactly: plugins test ok", "--model", os.environ.get("ANTHROPIC_MODEL", "kimi-k2.5"), ) if result.returncode != 0: return False, f"Exit {result.returncode}: {result.stderr[:300]}" if "test" in result.stdout.lower() or "ok" in result.stdout.lower(): return True, f"Model works with plugins installed: {result.stdout.strip()[:80]}" return True, f"Model responded: {result.stdout.strip()[:80]}" # ============================================================ # MAIN # ============================================================ def main() -> None: tests = [ # Skills tests ("install_real_skills", test_install_real_skills), ("real_skills_loaded", test_real_skills_loaded), ("real_skill_content_quality", test_real_skill_content_quality), ("skill_tool_real", test_skill_tool_with_real_skill), ("skills_in_prompt_real", test_skills_in_prompt_with_real), ("model_uses_real_skill", test_model_uses_real_skill), # Plugin tests ("install_real_plugins", test_install_real_plugins), ("real_plugins_loaded", test_real_plugins_loaded), ("plugin_commands_discovered", test_plugin_commands_discovered), ("plugin_hook_structure", test_plugin_hook_structure), ("commit_command_content", test_commit_command_content), ("model_with_plugins", test_real_model_with_plugins), ] passed = 0 failed = 0 for name, func in tests: try: ok, msg = asyncio.run(func()) if ok: print(f" {GREEN}PASS{RESET} {name}: {msg}") passed += 1 else: print(f" {RED}FAIL{RESET} {name}: {msg}") failed += 1 except Exception as exc: print(f" {RED}ERROR{RESET} {name}: {type(exc).__name__}: {exc}") failed += 1 print() print(f"{BOLD}Results: {passed} passed, {failed} failed{RESET}") sys.exit(1 if failed > 0 else 0) if __name__ == "__main__": main()