From 8c92e14caf54d646dd325a0a929a03af7c3903d7 Mon Sep 17 00:00:00 2001 From: Felipe Domingues Date: Sat, 30 May 2026 13:53:20 -0300 Subject: [PATCH] fix: route managed cronjobs and skills --- patches/skills_api.py | 46 ++++++++++++++--- tests/test_skills_api.py | 103 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 tests/test_skills_api.py diff --git a/patches/skills_api.py b/patches/skills_api.py index 845e7af..302d7ef 100644 --- a/patches/skills_api.py +++ b/patches/skills_api.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + """ skills_api.py — Reverse proxy + Runtime Sync API para Hermes Agent. @@ -91,10 +93,32 @@ def _write_text(path: str, content: str, mode: int = 0o644) -> None: def _managed_skill_dirname(skill: dict) -> str: - raw_name = str(skill.get("name") or "skill").strip().lower() - slug = re.sub(r"[^a-z0-9._-]+", "-", raw_name).strip("-") or "skill" - skill_id = str(skill.get("skill_id") or "manual").strip()[:8] or "manual" - return f"{slug}--{skill_id}" + raw_name = str(skill.get("name") or "skill").strip() + # Hermes' skill_view resolves bare names by directory name/path, not by + # frontmatter. Keep Mika-managed skill dirs aligned with the user-facing + # skill name so `skill_view(name="Minha skill")` can load them immediately. + safe_name = re.sub(r"[\x00/\\]+", "-", raw_name) + safe_name = re.sub(r"\s+", " ", safe_name).strip(" .") + if not safe_name or safe_name in {".", ".."}: + return "skill" + return safe_name[:120] + + +def _telegram_home_origin() -> dict | None: + chat_id = os.environ.get("TELEGRAM_HOME_CHANNEL", "").strip() + if not chat_id: + return None + thread_id = ( + os.environ.get("TELEGRAM_CRON_THREAD_ID", "").strip() + or os.environ.get("TELEGRAM_HOME_CHANNEL_THREAD_ID", "").strip() + ) + origin = { + "platform": "telegram", + "chat_id": chat_id, + } + if thread_id: + origin["thread_id"] = thread_id + return origin def _managed_entry_name(slug_source: str, entry_id: str) -> str: @@ -215,8 +239,8 @@ def _sanitize_cron_runtime_job(raw_job: dict, existing_job: dict | None, synced_ "last_status": existing_job.get("last_status") if existing_job else None, "last_error": existing_job.get("last_error") if existing_job else None, "last_delivery_error": existing_job.get("last_delivery_error") if existing_job else None, - "deliver": "local", - "origin": None, + "deliver": "origin", + "origin": existing_job.get("origin") if existing_job and existing_job.get("origin") else _telegram_home_origin(), "enabled_toolsets": None, "workdir": None, "managed_by": "mika", @@ -329,6 +353,14 @@ async def skills_sync(request: web.Request) -> web.Response: } _write_json(SKILLS_MANIFEST_PATH, manifest, mode=0o644) + reload_result = None + reload_error = None + try: + from agent.skill_commands import reload_skills + reload_result = reload_skills() + except Exception as exc: + reload_error = str(exc) + return web.json_response({ "ok": True, "received": len(incoming_skills), @@ -336,6 +368,8 @@ async def skills_sync(request: web.Request) -> web.Response: "skipped": skipped, "removed": removed, "managed_skills_root": MANAGED_SKILLS_ROOT, + "reload": reload_result, + "reload_error": reload_error, }) diff --git a/tests/test_skills_api.py b/tests/test_skills_api.py new file mode 100644 index 0000000..03450a6 --- /dev/null +++ b/tests/test_skills_api.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import importlib +import os +import sys +import types +import unittest +from unittest import mock + + +class SkillsApiSyncTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + fake_web = types.SimpleNamespace( + Request=object, + Response=object, + Application=object, + json_response=lambda payload, status=200: {"payload": payload, "status": status}, + run_app=lambda *_args, **_kwargs: None, + ) + fake_aiohttp = types.ModuleType("aiohttp") + fake_aiohttp.web = fake_web + fake_aiohttp.ClientSession = object + fake_aiohttp.ClientTimeout = object + sys.modules.setdefault("aiohttp", fake_aiohttp) + cls.skills_api = importlib.import_module("patches.skills_api") + + def test_managed_skill_dirname_matches_user_facing_skill_name(self) -> None: + dirname = self.skills_api._managed_skill_dirname({ + "skill_id": "ffdcdb9f-a52b-447b-ad47-57b390ce6c5d", + "name": "teste fechamento codex", + }) + + self.assertEqual(dirname, "teste fechamento codex") + + def test_managed_skill_dirname_removes_path_separators(self) -> None: + dirname = self.skills_api._managed_skill_dirname({ + "name": "../minha/skill\\nova", + }) + + self.assertEqual(dirname, "-minha-skill-nova") + + def test_managed_cronjobs_deliver_to_origin_with_telegram_home(self) -> None: + def fake_next_run(_schedule, _last_run_at): + return "2026-05-30T16:33:00+00:00" + + with mock.patch.dict( + os.environ, + { + "TELEGRAM_HOME_CHANNEL": "12345", + "TELEGRAM_CRON_THREAD_ID": "topic-1", + }, + clear=False, + ): + job = self.skills_api._sanitize_cron_runtime_job( + { + "job_id": "job-1", + "name": "validar fechamento final", + "action_prompt": "validar fechamento final", + "cron_expression": "*/3 * * * *", + "status": "active", + "human_readable": "A cada 3 minutos", + }, + None, + "2026-05-30T16:30:00Z", + fake_next_run, + ) + + self.assertIsNotNone(job) + self.assertEqual(job["deliver"], "origin") + self.assertEqual( + job["origin"], + {"platform": "telegram", "chat_id": "12345", "thread_id": "topic-1"}, + ) + + def test_existing_origin_is_preserved_on_resync(self) -> None: + def fake_next_run(_schedule, _last_run_at): + return "2026-05-30T16:33:00+00:00" + + job = self.skills_api._sanitize_cron_runtime_job( + { + "job_id": "job-1", + "name": "validar fechamento final", + "action_prompt": "validar fechamento final", + "cron_expression": "*/3 * * * *", + "status": "active", + "human_readable": "A cada 3 minutos", + }, + { + "origin": {"platform": "telegram", "chat_id": "existing"}, + "schedule": {"expr": "*/3 * * * *"}, + "next_run_at": "2026-05-30T16:36:00+00:00", + }, + "2026-05-30T16:30:00Z", + fake_next_run, + ) + + self.assertIsNotNone(job) + self.assertEqual(job["origin"], {"platform": "telegram", "chat_id": "existing"}) + + +if __name__ == "__main__": + unittest.main()