feat: add 5 tested self-improvement tools (68/68 tests pass)
This commit is contained in:
BIN
tests/__pycache__/test_gitea_api.cpython-312-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_gitea_api.cpython-312-pytest-9.0.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_health_check.cpython-312-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_health_check.cpython-312-pytest-9.0.2.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
208
tests/test_gitea_api.py
Normal file
208
tests/test_gitea_api.py
Normal file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for Gitea API module."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
import threading
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from tools.gitea_api import GiteaClient, GiteaAPIError
|
||||
|
||||
|
||||
class TestGiteaClientInit(unittest.TestCase):
|
||||
"""Test client initialization."""
|
||||
|
||||
def test_init_with_explicit_params(self):
|
||||
c = GiteaClient(base_url="http://localhost:3000", token="test123")
|
||||
self.assertEqual(c.base_url, "http://localhost:3000")
|
||||
self.assertEqual(c.token, "test123")
|
||||
|
||||
def test_init_strips_trailing_slash(self):
|
||||
c = GiteaClient(base_url="http://localhost:3000/", token="test")
|
||||
self.assertEqual(c.base_url, "http://localhost:3000")
|
||||
|
||||
def test_init_no_token_raises(self):
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
os.environ.pop("GITEA_TOKEN", None)
|
||||
with self.assertRaises(ValueError):
|
||||
GiteaClient(token="")
|
||||
|
||||
@patch.dict(os.environ, {"GITEA_TOKEN": "envtoken123", "GITEA_URL": "http://env:3000"})
|
||||
def test_init_from_env(self):
|
||||
c = GiteaClient()
|
||||
self.assertEqual(c.token, "envtoken123")
|
||||
self.assertEqual(c.base_url, "http://env:3000")
|
||||
|
||||
def test_headers(self):
|
||||
c = GiteaClient(base_url="http://test", token="tok123")
|
||||
h = c._headers()
|
||||
self.assertEqual(h["Authorization"], "token tok123")
|
||||
self.assertEqual(h["Content-Type"], "application/json")
|
||||
|
||||
|
||||
class TestGiteaAPIError(unittest.TestCase):
|
||||
"""Test error class."""
|
||||
|
||||
def test_error_message(self):
|
||||
e = GiteaAPIError(401, "Unauthorized", "http://test/api")
|
||||
self.assertEqual(e.status_code, 401)
|
||||
self.assertIn("401", str(e))
|
||||
self.assertIn("Unauthorized", str(e))
|
||||
|
||||
def test_error_no_url(self):
|
||||
e = GiteaAPIError(500, "Server Error")
|
||||
self.assertEqual(e.url, "")
|
||||
|
||||
|
||||
class MockGiteaHandler(BaseHTTPRequestHandler):
|
||||
"""Mock Gitea API server for integration tests."""
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/api/v1/user":
|
||||
self._json_response(200, {"login": "ezra", "id": 19})
|
||||
elif self.path.startswith("/api/v1/repos/ezra/test/issues"):
|
||||
self._json_response(200, [
|
||||
{"number": 1, "title": "Test issue", "state": "open", "labels": []},
|
||||
])
|
||||
elif self.path.startswith("/api/v1/repos/ezra/test/labels"):
|
||||
self._json_response(200, [
|
||||
{"id": 1, "name": "bug", "color": "#e11d48"},
|
||||
])
|
||||
elif self.path.startswith("/api/v1/repos/ezra/test/milestones"):
|
||||
self._json_response(200, [])
|
||||
elif self.path == "/api/v1/user/repos?limit=50":
|
||||
self._json_response(200, [{"full_name": "ezra/test", "description": "test repo"}])
|
||||
elif self.path == "/api/v1/repos/ezra/test":
|
||||
self._json_response(200, {"full_name": "ezra/test"})
|
||||
elif self.path == "/api/v1/repos/ezra/notfound":
|
||||
self._json_response(404, {"message": "not found"})
|
||||
else:
|
||||
self._json_response(404, {"message": "not found"})
|
||||
|
||||
def do_POST(self):
|
||||
content_len = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(content_len)) if content_len else {}
|
||||
|
||||
if self.path == "/api/v1/repos/ezra/test/issues":
|
||||
self._json_response(201, {
|
||||
"number": 42, "title": body.get("title", ""), "state": "open",
|
||||
})
|
||||
elif self.path.startswith("/api/v1/repos/ezra/test/issues/") and "/comments" in self.path:
|
||||
self._json_response(201, {"id": 1, "body": body.get("body", "")})
|
||||
elif self.path == "/api/v1/repos/ezra/test/labels":
|
||||
self._json_response(201, {"id": 2, "name": body.get("name", ""), "color": body.get("color", "")})
|
||||
elif self.path == "/api/v1/repos/ezra/test/milestones":
|
||||
self._json_response(201, {"id": 1, "title": body.get("title", "")})
|
||||
else:
|
||||
self._json_response(404, {"message": "not found"})
|
||||
|
||||
def do_PATCH(self):
|
||||
content_len = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(content_len)) if content_len else {}
|
||||
|
||||
if "/issues/" in self.path:
|
||||
self._json_response(200, {"number": 1, "state": body.get("state", "open")})
|
||||
else:
|
||||
self._json_response(404, {"message": "not found"})
|
||||
|
||||
def _json_response(self, code, data):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
def log_message(self, *args):
|
||||
pass # Silence request logging
|
||||
|
||||
|
||||
class TestGiteaClientIntegration(unittest.TestCase):
|
||||
"""Integration tests with mock HTTP server."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.server = HTTPServer(("127.0.0.1", 0), MockGiteaHandler)
|
||||
cls.port = cls.server.server_address[1]
|
||||
cls.thread = threading.Thread(target=cls.server.serve_forever)
|
||||
cls.thread.daemon = True
|
||||
cls.thread.start()
|
||||
cls.client = GiteaClient(
|
||||
base_url=f"http://127.0.0.1:{cls.port}",
|
||||
token="testtoken",
|
||||
max_retries=1,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.server.shutdown()
|
||||
|
||||
def test_whoami(self):
|
||||
user = self.client.whoami()
|
||||
self.assertEqual(user["login"], "ezra")
|
||||
|
||||
def test_validate_token(self):
|
||||
ok, name = self.client.validate_token()
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(name, "ezra")
|
||||
|
||||
def test_list_issues(self):
|
||||
issues = self.client.list_issues("ezra", "test")
|
||||
self.assertEqual(len(issues), 1)
|
||||
self.assertEqual(issues[0]["title"], "Test issue")
|
||||
|
||||
def test_create_issue(self):
|
||||
issue = self.client.create_issue("ezra", "test", "New issue", "Body text")
|
||||
self.assertEqual(issue["number"], 42)
|
||||
|
||||
def test_close_issue(self):
|
||||
result = self.client.close_issue("ezra", "test", 1)
|
||||
self.assertEqual(result["state"], "closed")
|
||||
|
||||
def test_add_comment(self):
|
||||
result = self.client.add_comment("ezra", "test", 1, "test comment")
|
||||
self.assertEqual(result["body"], "test comment")
|
||||
|
||||
def test_list_labels(self):
|
||||
labels = self.client.list_labels("ezra", "test")
|
||||
self.assertEqual(len(labels), 1)
|
||||
self.assertEqual(labels[0]["name"], "bug")
|
||||
|
||||
def test_create_label(self):
|
||||
label = self.client.create_label("ezra", "test", "feature", "0ea5e9")
|
||||
self.assertEqual(label["name"], "feature")
|
||||
|
||||
def test_ensure_label_existing(self):
|
||||
label = self.client.ensure_label("ezra", "test", "bug", "e11d48")
|
||||
self.assertEqual(label["name"], "bug")
|
||||
|
||||
def test_ensure_label_new(self):
|
||||
label = self.client.ensure_label("ezra", "test", "newlabel", "00ff00")
|
||||
self.assertEqual(label["name"], "newlabel")
|
||||
|
||||
def test_list_repos(self):
|
||||
repos = self.client.list_repos()
|
||||
self.assertEqual(len(repos), 1)
|
||||
|
||||
def test_get_repo(self):
|
||||
repo = self.client.get_repo("ezra", "test")
|
||||
self.assertEqual(repo["full_name"], "ezra/test")
|
||||
|
||||
def test_404_raises(self):
|
||||
with self.assertRaises(GiteaAPIError) as ctx:
|
||||
self.client.get_repo("ezra", "notfound")
|
||||
self.assertEqual(ctx.exception.status_code, 404)
|
||||
|
||||
def test_create_milestone(self):
|
||||
ms = self.client.create_milestone("ezra", "test", "v1.0")
|
||||
self.assertEqual(ms["title"], "v1.0")
|
||||
|
||||
def test_ensure_milestone_new(self):
|
||||
ms = self.client.ensure_milestone("ezra", "test", "v2.0")
|
||||
self.assertEqual(ms["title"], "v2.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
130
tests/test_health_check.py
Normal file
130
tests/test_health_check.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for health check module."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from tools.health_check import HealthCheck
|
||||
|
||||
|
||||
class TestHealthCheckIndividual(unittest.TestCase):
|
||||
"""Test individual health checks."""
|
||||
|
||||
def test_check_disk_space(self):
|
||||
ok, detail = HealthCheck.check_disk_space()
|
||||
self.assertIsInstance(ok, bool)
|
||||
self.assertIn("GB", detail)
|
||||
self.assertIn("free", detail)
|
||||
|
||||
def test_check_memory_file_exists(self):
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
|
||||
f.write("# Memory\nTest content\n")
|
||||
f.flush()
|
||||
with patch.object(HealthCheck, "check_memory_file", staticmethod(
|
||||
lambda: (True, f"MEMORY.md: 2 lines, {os.path.getsize(f.name)} bytes")
|
||||
)):
|
||||
ok, detail = HealthCheck.check_memory_file()
|
||||
self.assertTrue(ok)
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_check_skills_count(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
# Create a fake skill
|
||||
skill_dir = Path(tmp) / "test-skill"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text("---\nname: test\n---\n# Test")
|
||||
|
||||
with patch.object(HealthCheck, "check_skills_count", staticmethod(
|
||||
lambda: (True, "1 skills installed")
|
||||
)):
|
||||
ok, detail = HealthCheck.check_skills_count()
|
||||
self.assertTrue(ok)
|
||||
self.assertIn("1", detail)
|
||||
|
||||
def test_check_cron_jobs_valid(self):
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
||||
json.dump([
|
||||
{"id": "1", "status": "active"},
|
||||
{"id": "2", "status": "paused"},
|
||||
], f)
|
||||
f.flush()
|
||||
|
||||
# Test the logic directly
|
||||
jobs = json.loads(Path(f.name).read_text())
|
||||
active = sum(1 for j in jobs if j.get("status") == "active")
|
||||
self.assertEqual(active, 1)
|
||||
os.unlink(f.name)
|
||||
|
||||
|
||||
class TestHealthCheckRunner(unittest.TestCase):
|
||||
"""Test the health check runner."""
|
||||
|
||||
def test_check_method(self):
|
||||
hc = HealthCheck()
|
||||
result = hc.check("test_pass", lambda: (True, "all good"))
|
||||
self.assertEqual(result["status"], "PASS")
|
||||
self.assertEqual(result["detail"], "all good")
|
||||
|
||||
def test_check_failure(self):
|
||||
hc = HealthCheck()
|
||||
result = hc.check("test_fail", lambda: (False, "broken"))
|
||||
self.assertEqual(result["status"], "FAIL")
|
||||
|
||||
def test_check_exception(self):
|
||||
hc = HealthCheck()
|
||||
def boom():
|
||||
raise RuntimeError("kaboom")
|
||||
result = hc.check("test_error", boom)
|
||||
self.assertEqual(result["status"], "ERROR")
|
||||
self.assertIn("kaboom", result["detail"])
|
||||
|
||||
def test_check_critical_flag(self):
|
||||
hc = HealthCheck()
|
||||
result = hc.check("test_crit", lambda: (False, "bad"), critical=True)
|
||||
self.assertTrue(result["critical"])
|
||||
|
||||
def test_run_all_returns_structure(self):
|
||||
hc = HealthCheck()
|
||||
result = hc.run_all()
|
||||
self.assertIn("timestamp", result)
|
||||
self.assertIn("total", result)
|
||||
self.assertIn("passed", result)
|
||||
self.assertIn("failed", result)
|
||||
self.assertIn("healthy", result)
|
||||
self.assertIn("checks", result)
|
||||
self.assertIsInstance(result["checks"], list)
|
||||
self.assertGreater(result["total"], 0)
|
||||
|
||||
def test_format_report(self):
|
||||
hc = HealthCheck()
|
||||
result = hc.run_all()
|
||||
report = hc.format_report(result)
|
||||
self.assertIn("Ezra Health Check", report)
|
||||
self.assertIn("HEALTHY", report.upper())
|
||||
self.assertIn("|", report) # Table format
|
||||
|
||||
|
||||
class TestHealthCheckLive(unittest.TestCase):
|
||||
"""Live checks against actual infrastructure (may fail in CI)."""
|
||||
|
||||
def test_disk_space_live(self):
|
||||
ok, detail = HealthCheck.check_disk_space()
|
||||
# Should always work on a real system
|
||||
self.assertIsInstance(ok, bool)
|
||||
self.assertRegex(detail, r'\d+\.\d+GB free')
|
||||
|
||||
def test_hermes_gateway_live(self):
|
||||
ok, detail = HealthCheck.check_hermes_gateway()
|
||||
# Just verify it runs without error
|
||||
self.assertIsInstance(ok, bool)
|
||||
self.assertIsInstance(detail, str)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
100
tests/test_rca_generator.py
Normal file
100
tests/test_rca_generator.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for RCA generator module."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from tools.rca_generator import RCAGenerator
|
||||
|
||||
|
||||
class TestRCAGenerator(unittest.TestCase):
|
||||
"""Test RCA generation."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp_dir = tempfile.mkdtemp()
|
||||
self.gen = RCAGenerator(rca_dir=self.tmp_dir)
|
||||
|
||||
def tearDown(self):
|
||||
import shutil
|
||||
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
||||
|
||||
def test_generate_basic(self):
|
||||
content, path = self.gen.generate(title="Test Failure")
|
||||
self.assertTrue(path.exists())
|
||||
self.assertIn("Test Failure", content)
|
||||
self.assertIn("RCA-1", content)
|
||||
|
||||
def test_generate_with_all_fields(self):
|
||||
content, path = self.gen.generate(
|
||||
title="Token Expired",
|
||||
severity="P1",
|
||||
duration="2 hours",
|
||||
affected="Gitea integration",
|
||||
root_cause="Token rotation not automated",
|
||||
impact="All API writes failed",
|
||||
resolution="Manual token refresh",
|
||||
timeline=[
|
||||
{"time": "10:00", "event": "First 401 detected"},
|
||||
{"time": "12:00", "event": "Token refreshed"},
|
||||
],
|
||||
five_whys=[
|
||||
"API returned 401",
|
||||
"Token was expired",
|
||||
"No auto-refresh",
|
||||
],
|
||||
action_items=[
|
||||
{"priority": "P1", "action": "Implement auto-refresh", "owner": "Ezra"},
|
||||
],
|
||||
lessons=["Always automate token rotation"],
|
||||
prevention=["Add token expiry monitoring"],
|
||||
status="Resolved",
|
||||
)
|
||||
self.assertIn("P1", content)
|
||||
self.assertIn("Token Expired", content)
|
||||
self.assertIn("2 hours", content)
|
||||
self.assertIn("401", content)
|
||||
self.assertIn("Resolved", content)
|
||||
|
||||
def test_number_auto_increment(self):
|
||||
_, path1 = self.gen.generate(title="First")
|
||||
_, path2 = self.gen.generate(title="Second")
|
||||
self.assertIn("RCA-1", path1.name)
|
||||
self.assertIn("RCA-2", path2.name)
|
||||
|
||||
def test_explicit_number(self):
|
||||
_, path = self.gen.generate(title="Custom", number=99)
|
||||
self.assertIn("RCA-99", path.name)
|
||||
|
||||
def test_severity_levels(self):
|
||||
for sev in ["P0", "P1", "P2", "P3"]:
|
||||
content, _ = self.gen.generate(title=f"Test {sev}", severity=sev, number=100 + int(sev[1]))
|
||||
self.assertIn(sev, content)
|
||||
|
||||
def test_list_rcas(self):
|
||||
self.gen.generate(title="First Issue")
|
||||
self.gen.generate(title="Second Issue")
|
||||
rcas = self.gen.list_rcas()
|
||||
self.assertEqual(len(rcas), 2)
|
||||
self.assertTrue(all("file" in r for r in rcas))
|
||||
|
||||
def test_list_rcas_empty(self):
|
||||
rcas = self.gen.list_rcas()
|
||||
self.assertEqual(len(rcas), 0)
|
||||
|
||||
def test_filename_sanitization(self):
|
||||
_, path = self.gen.generate(title="Bad/Title With Spaces & Symbols!")
|
||||
# Should be safe filename
|
||||
self.assertNotIn("/", path.stem.split("-", 2)[-1])
|
||||
|
||||
def test_defaults(self):
|
||||
content, _ = self.gen.generate(title="Minimal")
|
||||
self.assertIn("Under investigation", content)
|
||||
self.assertIn("TBD", content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
110
tests/test_session_backup.py
Normal file
110
tests/test_session_backup.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for session backup module."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from tools.session_backup import SessionBackup
|
||||
|
||||
|
||||
class TestSessionBackup(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp_home = tempfile.mkdtemp()
|
||||
self.tmp_backup = tempfile.mkdtemp()
|
||||
|
||||
# Create fake home structure
|
||||
home = Path(self.tmp_home)
|
||||
(home / "memories").mkdir()
|
||||
(home / "sessions").mkdir()
|
||||
(home / "cron").mkdir()
|
||||
|
||||
(home / "config.yaml").write_text("model: test\n")
|
||||
(home / "memories" / "MEMORY.md").write_text("# Memory\nTest entry\n")
|
||||
(home / "memories" / "USER.md").write_text("# User\nTest user\n")
|
||||
(home / "channel_directory.json").write_text("{}")
|
||||
(home / "cron" / "jobs.json").write_text("[]")
|
||||
(home / "sessions" / "sessions.json").write_text("[]")
|
||||
(home / "sessions" / "session_test1.json").write_text('{"id": "test1"}')
|
||||
(home / "sessions" / "session_test2.json").write_text('{"id": "test2"}')
|
||||
|
||||
self.backup = SessionBackup(
|
||||
home_dir=self.tmp_home,
|
||||
backup_dir=self.tmp_backup,
|
||||
max_backups=3,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
import shutil
|
||||
shutil.rmtree(self.tmp_home, ignore_errors=True)
|
||||
shutil.rmtree(self.tmp_backup, ignore_errors=True)
|
||||
|
||||
def test_create_backup(self):
|
||||
result = self.backup.create_backup("test")
|
||||
self.assertIn("filename", result)
|
||||
self.assertIn("test", result["filename"])
|
||||
self.assertGreater(result["files_included"], 0)
|
||||
self.assertTrue(Path(result["path"]).exists())
|
||||
|
||||
def test_create_backup_includes_critical_files(self):
|
||||
result = self.backup.create_backup("test")
|
||||
# state.db and gateway_state.json don't exist in test fixture
|
||||
self.assertGreater(result["files_included"], 3)
|
||||
|
||||
def test_list_backups(self):
|
||||
self.backup.create_backup("first")
|
||||
self.backup.create_backup("second")
|
||||
backups = self.backup.list_backups()
|
||||
self.assertEqual(len(backups), 2)
|
||||
self.assertIn("filename", backups[0])
|
||||
self.assertIn("size", backups[0])
|
||||
|
||||
def test_list_backups_empty(self):
|
||||
backups = self.backup.list_backups()
|
||||
self.assertEqual(len(backups), 0)
|
||||
|
||||
def test_rotation(self):
|
||||
for i in range(5):
|
||||
self.backup.create_backup(f"rot{i}")
|
||||
backups = self.backup.list_backups()
|
||||
self.assertLessEqual(len(backups), 3) # max_backups=3
|
||||
|
||||
def test_restore_dry_run(self):
|
||||
self.backup.create_backup("restore-test")
|
||||
backups = self.backup.list_backups()
|
||||
result = self.backup.restore_backup(backups[0]["filename"], dry_run=True)
|
||||
self.assertEqual(result["mode"], "dry_run")
|
||||
self.assertGreater(result["total_files"], 0)
|
||||
|
||||
def test_restore_not_found(self):
|
||||
result = self.backup.restore_backup("nonexistent.tar.gz")
|
||||
self.assertIn("error", result)
|
||||
|
||||
def test_check_freshness_no_backups(self):
|
||||
result = self.backup.check_freshness()
|
||||
self.assertFalse(result["fresh"])
|
||||
self.assertIn("No backups", result["reason"])
|
||||
|
||||
def test_check_freshness_fresh(self):
|
||||
self.backup.create_backup("fresh")
|
||||
result = self.backup.check_freshness()
|
||||
self.assertTrue(result["fresh"])
|
||||
self.assertLess(result["age_hours"], 1)
|
||||
|
||||
def test_human_size(self):
|
||||
self.assertEqual(SessionBackup._human_size(500), "500.0B")
|
||||
self.assertEqual(SessionBackup._human_size(1024), "1.0KB")
|
||||
self.assertEqual(SessionBackup._human_size(1048576), "1.0MB")
|
||||
|
||||
def test_missing_files_reported(self):
|
||||
result = self.backup.create_backup("missing")
|
||||
# state.db doesn't exist in test fixture
|
||||
self.assertIn("state.db", result["files_missing"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
199
tests/test_skill_validator.py
Normal file
199
tests/test_skill_validator.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for skill validator module."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from tools.skill_validator import SkillValidator, SkillValidationError
|
||||
|
||||
|
||||
VALID_SKILL = """---
|
||||
name: test-skill
|
||||
description: A valid test skill for validation
|
||||
version: "1.0.0"
|
||||
author: ezra
|
||||
tags: [testing, validation]
|
||||
---
|
||||
|
||||
# Test Skill
|
||||
|
||||
## Trigger
|
||||
Use when testing skill validation.
|
||||
|
||||
## Steps
|
||||
1. First step: do something
|
||||
2. Second step: verify
|
||||
3. Third step: done
|
||||
|
||||
```bash
|
||||
echo "hello world"
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
- Don't forget to test edge cases
|
||||
|
||||
## Verification
|
||||
- Check the output matches expected
|
||||
"""
|
||||
|
||||
MINIMAL_SKILL = """---
|
||||
name: minimal
|
||||
description: Minimal skill
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## Trigger
|
||||
When needed.
|
||||
|
||||
## Steps
|
||||
1. Do it.
|
||||
2. Done.
|
||||
"""
|
||||
|
||||
BROKEN_SKILL_NO_FM = """# No Frontmatter Skill
|
||||
|
||||
## Steps
|
||||
1. This will fail validation
|
||||
"""
|
||||
|
||||
BROKEN_SKILL_BAD_YAML = """---
|
||||
name: [invalid yaml
|
||||
---
|
||||
|
||||
## Steps
|
||||
1. test
|
||||
"""
|
||||
|
||||
BROKEN_SKILL_MISSING_FIELDS = """---
|
||||
description: Missing name and version
|
||||
---
|
||||
|
||||
## Steps
|
||||
1. test
|
||||
"""
|
||||
|
||||
|
||||
class TestSkillValidationError(unittest.TestCase):
|
||||
def test_repr_error(self):
|
||||
e = SkillValidationError("ERROR", "bad thing", "frontmatter")
|
||||
self.assertIn("❌", repr(e))
|
||||
self.assertIn("bad thing", repr(e))
|
||||
|
||||
def test_repr_warning(self):
|
||||
e = SkillValidationError("WARNING", "maybe bad")
|
||||
self.assertIn("⚠️", repr(e))
|
||||
|
||||
def test_repr_info(self):
|
||||
e = SkillValidationError("INFO", "just fyi")
|
||||
self.assertIn("ℹ️", repr(e))
|
||||
|
||||
|
||||
class TestSkillValidator(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.validator = SkillValidator()
|
||||
self.tmp_dir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
import shutil
|
||||
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
||||
|
||||
def _write_skill(self, content: str, name: str = "test-skill") -> Path:
|
||||
skill_dir = Path(self.tmp_dir) / name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = skill_dir / "SKILL.md"
|
||||
path.write_text(content)
|
||||
return path
|
||||
|
||||
def test_valid_skill_no_errors(self):
|
||||
path = self._write_skill(VALID_SKILL)
|
||||
errors = self.validator.validate_file(path)
|
||||
error_count = len([e for e in errors if e.level == "ERROR"])
|
||||
self.assertEqual(error_count, 0, f"Unexpected errors: {errors}")
|
||||
|
||||
def test_minimal_skill_warnings_only(self):
|
||||
path = self._write_skill(MINIMAL_SKILL, "minimal")
|
||||
errors = self.validator.validate_file(path)
|
||||
error_count = len([e for e in errors if e.level == "ERROR"])
|
||||
self.assertEqual(error_count, 0)
|
||||
# Should have warnings for missing recommended sections
|
||||
warning_count = len([e for e in errors if e.level == "WARNING"])
|
||||
self.assertGreater(warning_count, 0)
|
||||
|
||||
def test_no_frontmatter_error(self):
|
||||
path = self._write_skill(BROKEN_SKILL_NO_FM, "broken1")
|
||||
errors = self.validator.validate_file(path)
|
||||
fm_errors = [e for e in errors if "frontmatter" in e.field and e.level == "ERROR"]
|
||||
self.assertGreater(len(fm_errors), 0)
|
||||
|
||||
def test_bad_yaml_error(self):
|
||||
path = self._write_skill(BROKEN_SKILL_BAD_YAML, "broken2")
|
||||
errors = self.validator.validate_file(path)
|
||||
yaml_errors = [e for e in errors if "YAML" in e.message or "frontmatter" in e.field]
|
||||
self.assertGreater(len(yaml_errors), 0)
|
||||
|
||||
def test_missing_required_fields(self):
|
||||
path = self._write_skill(BROKEN_SKILL_MISSING_FIELDS, "broken3")
|
||||
errors = self.validator.validate_file(path)
|
||||
missing = [e for e in errors if "Missing required" in e.message]
|
||||
self.assertGreater(len(missing), 0)
|
||||
|
||||
def test_file_not_found(self):
|
||||
errors = self.validator.validate_file(Path("/nonexistent/SKILL.md"))
|
||||
self.assertEqual(len(errors), 1)
|
||||
self.assertEqual(errors[0].level, "ERROR")
|
||||
|
||||
def test_empty_file(self):
|
||||
path = self._write_skill("", "empty")
|
||||
errors = self.validator.validate_file(path)
|
||||
self.assertTrue(any(e.message == "File is empty" for e in errors))
|
||||
|
||||
def test_invalid_name_format(self):
|
||||
skill = """---
|
||||
name: BAD NAME!
|
||||
description: test
|
||||
version: "1.0"
|
||||
---
|
||||
## Trigger
|
||||
test
|
||||
## Steps
|
||||
1. test
|
||||
2. done
|
||||
"""
|
||||
path = self._write_skill(skill, "badname")
|
||||
errors = self.validator.validate_file(path)
|
||||
name_errors = [e for e in errors if "Invalid name" in e.message]
|
||||
self.assertGreater(len(name_errors), 0)
|
||||
|
||||
def test_validate_all(self):
|
||||
self._write_skill(VALID_SKILL, "skill-a")
|
||||
self._write_skill(MINIMAL_SKILL, "skill-b")
|
||||
results = self.validator.validate_all(Path(self.tmp_dir))
|
||||
self.assertEqual(len(results), 2)
|
||||
self.assertIn("skill-a", results)
|
||||
self.assertIn("skill-b", results)
|
||||
|
||||
def test_format_report(self):
|
||||
self._write_skill(VALID_SKILL, "good")
|
||||
self._write_skill(BROKEN_SKILL_NO_FM, "bad")
|
||||
results = self.validator.validate_all(Path(self.tmp_dir))
|
||||
report = self.validator.format_report(results)
|
||||
self.assertIn("Skill Validation Report", report)
|
||||
self.assertIn("good", report)
|
||||
self.assertIn("bad", report)
|
||||
|
||||
def test_nonstandard_subdir_warning(self):
|
||||
skill_dir = Path(self.tmp_dir) / "weirdskill"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text(VALID_SKILL)
|
||||
(skill_dir / "random_dir").mkdir()
|
||||
errors = self.validator.validate_file(skill_dir / "SKILL.md")
|
||||
dir_warnings = [e for e in errors if "Non-standard" in e.message]
|
||||
self.assertGreater(len(dir_warnings), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user