Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca8e446b11 | |||
| 9b77ef14cc | |||
| bb3fe5e108 | |||
| 1c04f827e2 | |||
| 752496682e | |||
| f8e8f7800f | |||
| 7a147b7418 | |||
| 9cf72407c9 | |||
| 421a299e7d | |||
| 8cc3b1b785 | |||
| 4cc7184070 | |||
| 1f99cf34bd | |||
| f62faec77b | |||
| a4985baf9a | |||
| 20c127fd50 | |||
| 7bbb92e0a7 | |||
| cef28c164a | |||
| 74c2a0aac4 | |||
| deee16a3ae | |||
| ba56952546 | |||
| 192dac9d4a |
@@ -0,0 +1,69 @@
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
validate:
|
||||
name: Plugin validation
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Canonical validator script fetched fresh on every run.
|
||||
# Single source of truth avoids the drift class where validator
|
||||
# changes weren't propagated to all 21 plugin repos.
|
||||
# Anonymous git clone to avoid Gitea 1.22.6 auth fallback issue.
|
||||
- name: Fetch molecule-ci canonical scripts
|
||||
run: git clone --depth 1 https://git.moleculesai.app/molecule-ai/molecule-ci.git .molecule-ci-canonical
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: "pip"
|
||||
cache-dependency-path: .molecule-ci-canonical/.molecule-ci/scripts/requirements.txt
|
||||
- run: pip install pyyaml -q
|
||||
- run: python3 .molecule-ci-canonical/.molecule-ci/scripts/validate-plugin.py
|
||||
- name: Check for secrets
|
||||
run: |
|
||||
python3 - << 'PYEOF'
|
||||
import os, re, sys
|
||||
from pathlib import Path
|
||||
|
||||
PATTERNS = [
|
||||
re.compile(r'''["']sk-ant-[a-zA-Z0-9]{50,}["']'''),
|
||||
re.compile(r'''["']ghp_[a-zA-Z0-9]{36,}["']'''),
|
||||
re.compile(r'''["']AKIA[A-Z0-9]{16}["']'''),
|
||||
re.compile(r'''["']Bearer\s+[a-zA-Z0-9_.-]{20,}["']'''),
|
||||
re.compile(r'''ghp_[a-zA-Z0-9]{36,}'''),
|
||||
re.compile(r'''sk-ant-[a-zA-Z0-9]{50,}'''),
|
||||
]
|
||||
SKIP_DIRS = {'.molecule-ci', '.molecule-ci-canonical', '.git', 'node_modules', '__pycache__'}
|
||||
EXTENSIONS = {'.yaml', '.yml', '.md', '.py', '.sh'}
|
||||
|
||||
def is_false_positive(line):
|
||||
ctx = line.lower()
|
||||
return '...' in ctx or '<example' in ctx or '</example' in ctx
|
||||
|
||||
root = Path(os.environ.get('GITHUB_WORKSPACE', '.'))
|
||||
warnings = []
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
||||
for filename in filenames:
|
||||
if Path(filename).suffix not in EXTENSIONS:
|
||||
continue
|
||||
filepath = Path(dirpath) / filename
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for lineno, line in enumerate(f.readlines(), 1):
|
||||
for pattern in PATTERNS:
|
||||
for match in pattern.finditer(line):
|
||||
if not is_false_positive(line):
|
||||
warnings.append(f" {filepath}:{lineno}: {match.group(0)[:40]}...")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if warnings:
|
||||
print("::error::Potential secret found in committed files:")
|
||||
for w in warnings:
|
||||
print(w)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("::notice::No secrets detected")
|
||||
PYEOF
|
||||
@@ -1,5 +0,0 @@
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
validate:
|
||||
uses: Molecule-AI/molecule-ci/.github/workflows/validate-plugin.yml@main
|
||||
+18
@@ -19,3 +19,21 @@
|
||||
# Workspace auth tokens
|
||||
.auth-token
|
||||
.auth_token
|
||||
# Python bytecode (append only — do not remove entries above)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.pytest_cache/
|
||||
# Python bytecode (append only — do not remove entries above)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.Python
|
||||
*.egg-info/
|
||||
*.egg
|
||||
.pytest_cache/
|
||||
build/
|
||||
dist/
|
||||
.eggs/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
"""Hermes adaptor — uses the generic rule+skill installer.
|
||||
|
||||
Hermes loads skills from /configs/skills/ via the shared skill_loader,
|
||||
which is runtime-agnostic. The AgentskillsAdaptor wires rules, skills,
|
||||
hooks, and commands for Claude Code-style harness environments. For Hermes,
|
||||
the same adaptor handles rules and skills; hooks/commands are no-ops
|
||||
that Hermes ignores gracefully.
|
||||
"""
|
||||
from plugins_registry.builtins import AgentskillsAdaptor as Adaptor # noqa: F401
|
||||
@@ -123,7 +123,7 @@ remote: error: pre-receive hook declined
|
||||
**Cause:** Either (a) branch protection requires PR review, or (b) CI failed on the latest commit.
|
||||
|
||||
**Fix:**
|
||||
1. Check CI status at: `https://github.com/Molecule-AI/molecule-ai-plugin-ecc/actions`
|
||||
1. Check CI status at: `https://git.moleculesai.app/molecule-ai/molecule-ai-plugin-ecc/actions`
|
||||
2. If CI failed, fix the failures and push again
|
||||
3. If CI passed but push still blocked, use a PR instead of pushing directly to `main`
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ Verify `plugins_registry` is installed in the active venv:
|
||||
```bash
|
||||
pip show plugins_registry
|
||||
# If not installed:
|
||||
pip install git+https://github.com/Molecule-AI/plugins_registry.git
|
||||
pip install git+https://git.moleculesai.app/molecule-ai/plugins_registry.git
|
||||
```
|
||||
|
||||
### CI failure on markdownlint
|
||||
|
||||
@@ -105,7 +105,7 @@ EOF
|
||||
```
|
||||
|
||||
Or via the GitHub web UI:
|
||||
1. Go to https://github.com/Molecule-AI/molecule-ai-plugin-ecc/releases/new
|
||||
1. Go to https://git.moleculesai.app/molecule-ai/molecule-ai-plugin-ecc/releases/new
|
||||
2. Select the tag `vX.Y.Z`
|
||||
3. Paste the changelog into the release body
|
||||
4. Publish release
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Test Coverage Rationale — molecule-ecc
|
||||
|
||||
## Why This Plugin Has Limited Unit-Test Coverage
|
||||
|
||||
`molecule-ecc` is a **skill+rules plugin** — it provides development guidelines and
|
||||
development skills (api-design, coding-standards, deep-research, security-review, tdd-workflow)
|
||||
via prose SKILL.md files and rules/*.md files.
|
||||
|
||||
There are no hooks, no Python business logic, and no testable adapters in this plugin.
|
||||
The "logic" is prose documentation.
|
||||
|
||||
## What We Test (and Why)
|
||||
|
||||
| What | Why |
|
||||
|------|-----|
|
||||
| `plugin.yaml` schema | Verifies all 5 skills and 3 rules are registered |
|
||||
| Rules files (3) | Each declared rule file exists and is non-empty |
|
||||
| Skills (5) | Each skill directory + SKILL.md exists with valid YAML frontmatter and `#` heading |
|
||||
| Adapters (2) | Claude Code + deepagents adapters are wired |
|
||||
| `known-issues.md` | Severity definitions present |
|
||||
| `validate-plugin.py` exit 0 | Smoke test — shared CI validator passes |
|
||||
|
||||
## What We Cannot Unit-Test Here
|
||||
|
||||
- **SKILL.md prose content** — the development guidelines are prose; their quality is
|
||||
a documentation review concern, not a unit-test concern.
|
||||
|
||||
- **Agent behavior when using skills** — write integration tests in `workspace-template/`.
|
||||
|
||||
## Integration Tests
|
||||
|
||||
If you want to test that agents actually use the ecc skills correctly, write
|
||||
integration tests that:
|
||||
1. Install `molecule-ecc` on a test workspace
|
||||
2. Ask the agent to use a specific skill (e.g., "use TDD workflow")
|
||||
3. Verify the agent follows the documented process
|
||||
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Smoke tests for molecule-ecc (Everything Claude Code).
|
||||
|
||||
Rationale: This is a skill+rules plugin — no hooks. The "logic" is prose in
|
||||
SKILL.md files and rules/*.md files. Smoke tests verify all artifacts exist,
|
||||
parse correctly, and document required sections. See tests/README.md.
|
||||
|
||||
Run: python tests/test_ecc_smoke.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(REPO_ROOT, '.molecule-ci', 'scripts'))
|
||||
|
||||
|
||||
def load_manifest():
|
||||
import yaml
|
||||
with open(os.path.join(REPO_ROOT, 'plugin.yaml')) as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
class TestPluginManifest(unittest.TestCase):
|
||||
"""Verify plugin.yaml is well-formed."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.manifest = load_manifest()
|
||||
|
||||
def test_plugin_yaml_loads(self):
|
||||
self.assertIsInstance(self.manifest, dict)
|
||||
|
||||
def test_name(self):
|
||||
self.assertEqual(self.manifest['name'], 'ecc')
|
||||
|
||||
def test_version_semver(self):
|
||||
import re
|
||||
v = self.manifest['version']
|
||||
self.assertRegex(v, r'^\d+\.\d+\.\d+$', f"Version {v!r} not semver")
|
||||
|
||||
def test_description_present(self):
|
||||
self.assertGreater(len(self.manifest.get('description', '')), 10)
|
||||
|
||||
def test_runtimes_include_claude_code(self):
|
||||
self.assertIn('claude_code', self.manifest.get('runtimes', []))
|
||||
|
||||
def test_rules_declared(self):
|
||||
rules = self.manifest.get('rules', [])
|
||||
self.assertIsInstance(rules, list)
|
||||
self.assertGreater(len(rules), 0)
|
||||
|
||||
def test_skills_declared(self):
|
||||
skills = self.manifest.get('skills', [])
|
||||
self.assertIsInstance(skills, list)
|
||||
self.assertGreater(len(skills), 0)
|
||||
|
||||
|
||||
class TestRules(unittest.TestCase):
|
||||
"""Verify all declared rules files exist and are non-empty."""
|
||||
|
||||
RULES_DIR = os.path.join(REPO_ROOT, 'rules')
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.rules = load_manifest().get('rules', [])
|
||||
|
||||
def test_rules_directory_exists(self):
|
||||
self.assertTrue(os.path.isdir(self.RULES_DIR))
|
||||
|
||||
def test_each_declared_rule_file_exists(self):
|
||||
for rule in self.rules:
|
||||
# plugin.yaml declares rules as 'rules/foo.md' — take basename
|
||||
filename = os.path.basename(rule)
|
||||
path = os.path.join(self.RULES_DIR, filename)
|
||||
self.assertTrue(
|
||||
os.path.isfile(path),
|
||||
f"Rule {rule!r} declared but file not found at {path}"
|
||||
)
|
||||
|
||||
def test_each_rule_file_is_nonempty(self):
|
||||
for rule in self.rules:
|
||||
filename = os.path.basename(rule)
|
||||
path = os.path.join(self.RULES_DIR, filename)
|
||||
size = os.path.getsize(path)
|
||||
self.assertGreater(size, 100, f"Rule {rule!r} is suspiciously small ({size} bytes)")
|
||||
|
||||
def test_guardrails_rule_has_content(self):
|
||||
path = os.path.join(self.RULES_DIR, 'everything-claude-code-guardrails.md')
|
||||
if os.path.isfile(path):
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
self.assertGreater(len(content), 500, "Guardrails rule should have substantive content")
|
||||
|
||||
|
||||
class TestSkills(unittest.TestCase):
|
||||
"""Verify all declared skills have SKILL.md with valid frontmatter."""
|
||||
|
||||
SKILLS_DIR = os.path.join(REPO_ROOT, 'skills')
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.skills = load_manifest().get('skills', [])
|
||||
|
||||
def test_skills_directory_exists(self):
|
||||
self.assertTrue(os.path.isdir(self.SKILLS_DIR))
|
||||
|
||||
def test_each_declared_skill_directory_exists(self):
|
||||
for skill in self.skills:
|
||||
path = os.path.join(self.SKILLS_DIR, skill)
|
||||
self.assertTrue(
|
||||
os.path.isdir(path),
|
||||
f"Skill {skill!r} declared but directory not found at {path}"
|
||||
)
|
||||
|
||||
def test_each_skill_has_skill_md(self):
|
||||
import yaml
|
||||
for skill in self.skills:
|
||||
path = os.path.join(self.SKILLS_DIR, skill, 'SKILL.md')
|
||||
self.assertTrue(os.path.isfile(path), f"Skill {skill!r} missing SKILL.md at {path}")
|
||||
|
||||
def test_each_skill_md_has_frontmatter(self):
|
||||
import yaml
|
||||
for skill in self.skills:
|
||||
path = os.path.join(self.SKILLS_DIR, skill, 'SKILL.md')
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
self.assertTrue(
|
||||
content.startswith('---'),
|
||||
f"{skill}: SKILL.md must have YAML frontmatter"
|
||||
)
|
||||
parts = content.split('---', 2)
|
||||
self.assertEqual(len(parts), 3, f"{skill}: SKILL.md must have opening and closing ---")
|
||||
_, frontmatter, _ = parts
|
||||
data = yaml.safe_load(frontmatter)
|
||||
self.assertIsInstance(data, dict)
|
||||
self.assertIn('name', data, f"{skill}: frontmatter must have 'name'")
|
||||
|
||||
def test_each_skill_body_has_heading(self):
|
||||
for skill in self.skills:
|
||||
path = os.path.join(self.SKILLS_DIR, skill, 'SKILL.md')
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
parts = content.split('---', 2)
|
||||
_, _, body = parts
|
||||
self.assertRegex(
|
||||
body.lstrip(), r'^# ',
|
||||
f"{skill}: SKILL.md body must start with # heading"
|
||||
)
|
||||
|
||||
|
||||
class TestAdapters(unittest.TestCase):
|
||||
"""Verify Claude Code and deepagents adapters exist."""
|
||||
|
||||
def test_claude_code_adapter_exists(self):
|
||||
path = os.path.join(REPO_ROOT, 'adapters', 'claude_code.py')
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
|
||||
def test_claude_code_adapter_imports_adaptor(self):
|
||||
path = os.path.join(REPO_ROOT, 'adapters', 'claude_code.py')
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
self.assertIn('Adaptor', content)
|
||||
|
||||
def test_deepagents_adapter_exists(self):
|
||||
path = os.path.join(REPO_ROOT, 'adapters', 'deepagents.py')
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
|
||||
|
||||
class TestKnownIssues(unittest.TestCase):
|
||||
"""Verify known-issues.md structure."""
|
||||
|
||||
KI_PATH = os.path.join(REPO_ROOT, 'known-issues.md')
|
||||
|
||||
def test_file_exists(self):
|
||||
self.assertTrue(os.path.isfile(self.KI_PATH))
|
||||
|
||||
def test_has_active_issues_section(self):
|
||||
with open(self.KI_PATH) as f:
|
||||
self.assertIn('Active Issues', f.read())
|
||||
|
||||
def test_has_severity_definitions(self):
|
||||
with open(self.KI_PATH) as f:
|
||||
content = f.read()
|
||||
self.assertIn('Severity Definitions', content)
|
||||
|
||||
|
||||
class TestValidatePlugin(unittest.TestCase):
|
||||
"""Smoke-test validate-plugin.py."""
|
||||
|
||||
def test_exits_zero(self):
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(REPO_ROOT, '.molecule-ci', 'scripts', 'validate-plugin.py')],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=REPO_ROOT,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}\nstderr: {result.stderr}")
|
||||
self.assertIn('ecc', result.stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user