diff --git a/CHANGELOG.md b/CHANGELOG.md index f29023ad..8ba30e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,14 @@ PyPI version — not the changelog header. 10 existing line-scoped entries across drone/memory/skills and dropped 3 dead entries already pointing past EOF (td-009). +### Removed + +- **Dead `bulletin_created` trigger handler** — the event handler that wrote a + `bulletin_board` section into every branch dashboard is retired: nothing fired + the event, its `BULLETINS.central.json` store no longer exists, and prax + already prunes `bulletin_board` as a deprecated section. Archived + unwired + from the event registry; prax's pruning stays (td-102). + ## [2026-06-23] The **2.6.0** release — a large `dev → main` merge spanning several weeks (68 commits). diff --git a/src/aipass/trigger/apps/handlers/events/bulletin_created.py b/src/aipass/trigger/apps/handlers/events/bulletin_created.py deleted file mode 100644 index e700fbb8..00000000 --- a/src/aipass/trigger/apps/handlers/events/bulletin_created.py +++ /dev/null @@ -1,246 +0,0 @@ -# =================== AIPass ==================== -# Name: bulletin_created.py -# Description: Bulletin created event handler for dashboard propagation -# Version: 1.0.0 -# Created: 2026-01-31 -# Modified: 2026-01-31 -# ============================================= - -""" -Bulletin Created Event Handler - -Handles bulletin_created events fired when a new bulletin is created. -Propagates the bulletin to all branch dashboards. - -Event data expected: - - bulletin_id: ID of the created bulletin - - title: Bulletin title - - message: Bulletin content - - priority: Bulletin priority level - - created_by: Who created it - - timestamp: When created -""" - -import json -from datetime import datetime, timezone -from pathlib import Path -from typing import Any, Dict, List -from aipass.trigger.apps.config import TRIGGER_ROOT, atomic_write_json -from aipass.trigger.apps.handlers.json import json_handler - - -def _find_repo_root() -> Path: - """Walk up from this file to find the repo root (contains AIPASS_REGISTRY.json).""" - current = Path(__file__).resolve().parent - for parent in [current] + list(current.parents): - if (parent / "AIPASS_REGISTRY.json").exists(): - return parent - return Path.cwd() - - -_REPO_ROOT = _find_repo_root() - -# Paths -BRANCH_REGISTRY = _REPO_ROOT / "AIPASS_REGISTRY.json" -BULLETINS_PATH = _REPO_ROOT / "BULLETINS.central.json" - -_HANDLER_LOG = TRIGGER_ROOT / "logs" / "bulletin_handler.log" - - -def _log_warning(message: str) -> None: - """Log warning to file (event handlers cannot import Prax logger - causes recursion).""" - try: - _HANDLER_LOG.parent.mkdir(parents=True, exist_ok=True) - ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - with open(_HANDLER_LOG, "a", encoding="utf-8") as f: - f.write(f"{ts} | WARNING | {message}\n") - except Exception: - pass - - -def _load_branch_registry() -> List[Dict]: - """ - Load branch registry silently. - - Returns: - List of branch dicts with name, path, status. - Empty list on any error. - """ - try: - if not BRANCH_REGISTRY.exists(): - return [] - data = json.loads(BRANCH_REGISTRY.read_text(encoding="utf-8")) - return data.get("branches", []) - except Exception as exc: - _log_warning(f"load branch registry failed: {exc}") - return [] - - -def _load_bulletins() -> List[Dict]: - """ - Load bulletins from central storage. - - Returns: - List of bulletin dicts. Empty list on error. - """ - try: - if not BULLETINS_PATH.exists(): - return [] - data = json.loads(BULLETINS_PATH.read_text()) - return data.get("bulletins", []) - except Exception as exc: - _log_warning(f"load bulletins failed: {exc}") - return [] - - -def _filter_active_bulletins(bulletins: List[Dict]) -> List[Dict]: - """ - Filter bulletins to only active ones. - - Args: - bulletins: List of all bulletins - - Returns: - List of active bulletins only - """ - return [b for b in bulletins if b.get("active", False)] - - -def _load_dashboard(branch_path: Path) -> Dict: - """ - Load existing dashboard or create minimal structure. - - Args: - branch_path: Path to branch root - - Returns: - Dashboard data dict (existing or minimal structure) - """ - dashboard_path = branch_path / "DASHBOARD.local.json" - - if dashboard_path.exists(): - try: - data = json.loads(dashboard_path.read_text()) - if "sections" not in data: - data["sections"] = {} - if "bulletin_board" not in data["sections"]: - data["sections"]["bulletin_board"] = {"managed_by": "aipass", "active_bulletins": [], "pending_ack": []} - return data - except Exception as exc: - _log_warning(f"parse dashboard JSON for {branch_path}: {exc}") - - # Create minimal dashboard structure - return { - "last_refreshed": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "sections": {"bulletin_board": {"managed_by": "aipass", "active_bulletins": [], "pending_ack": []}}, - } - - -def _save_dashboard(branch_path: Path, dashboard: Dict) -> bool: - """ - Save dashboard to branch. - - Args: - branch_path: Path to branch root - dashboard: Dashboard data to save - - Returns: - True if saved, False on error - """ - try: - dashboard_path = branch_path / "DASHBOARD.local.json" - dashboard["last_refreshed"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - atomic_write_json(dashboard_path, dashboard) - return True - except Exception as exc: - _log_warning(f"save dashboard to {branch_path}: {exc}") - return False - - -def _propagate_bulletins_to_branches() -> None: - """ - Propagate active bulletins to all branch dashboards. - - Loads active bulletins and updates each branch's dashboard - with the bulletin_board section. - - Silent failure - catches all exceptions. - """ - try: - # Load branch registry - branches = _load_branch_registry() - if not branches: - return - - # Load and filter active bulletins - all_bulletins = _load_bulletins() - active_bulletins = _filter_active_bulletins(all_bulletins) - - # Update each branch dashboard - for branch in branches: - branch_path_str = branch.get("path") - if not branch_path_str: - continue - - branch_path = Path(branch_path_str) - if not branch_path.exists(): - continue - - try: - # Load dashboard - dashboard = _load_dashboard(branch_path) - - # Update bulletin_board section ONLY - if "sections" not in dashboard: - dashboard["sections"] = {} - - dashboard["sections"]["bulletin_board"] = { - "managed_by": "aipass", - "active_bulletins": active_bulletins, - "pending_ack": [], - } - - # Save dashboard - _save_dashboard(branch_path, dashboard) - except Exception as exc: - _log_warning(f"propagate to branch {branch_path_str}: {exc}") - continue - - except Exception as exc: - _log_warning(f"bulletin propagation failed: {exc}") - - -def handle_bulletin_created( - _bulletin_id: str | None = None, - _title: str | None = None, - _message: str | None = None, - _priority: str | None = None, - _created_by: str | None = None, - _timestamp: str | None = None, - **_kwargs: Any, -) -> None: - """ - Handle bulletin_created event - propagate bulletin to all branch dashboards. - - Event parameters are received but not used directly - we reload from - central storage to ensure consistency with any concurrent updates. - - Args: - _bulletin_id: ID of the created bulletin (unused - reload from storage) - _title: Bulletin title (unused - reload from storage) - _message: Bulletin content (unused - reload from storage) - _priority: Bulletin priority level (unused - reload from storage) - _created_by: Who created it (unused - reload from storage) - _timestamp: When created (unused - reload from storage) - **_kwargs: Additional event data (ignored) - """ - try: - # Propagate bulletins to all branches - # We reload from central storage to ensure consistency - # (the newly created bulletin should already be saved there) - _propagate_bulletins_to_branches() - - json_handler.log_operation("bulletin_event", {"success": True}) - - except Exception as exc: - _log_warning(f"handle_bulletin_created failed: {exc}") diff --git a/src/aipass/trigger/apps/handlers/events/registry.py b/src/aipass/trigger/apps/handlers/events/registry.py index d035b06b..81aa4373 100644 --- a/src/aipass/trigger/apps/handlers/events/registry.py +++ b/src/aipass/trigger/apps/handlers/events/registry.py @@ -61,7 +61,6 @@ def setup_handlers(): except ImportError: _log_warning("ai_mail not available — error notifications won't send") from .warning_logged import handle_warning_logged - from .bulletin_created import handle_bulletin_created from .memory_template_updated import handle_memory_template_updated # from .pr_status_sync import handle_pr_created, handle_pr_merged # TDPLAN-0007: status-sync decommissioned @@ -74,7 +73,6 @@ def setup_handlers(): trigger.on("plan_file_moved", handle_plan_file_moved) trigger.on("error_detected", handle_error_detected) trigger.on("warning_logged", handle_warning_logged) - trigger.on("bulletin_created", handle_bulletin_created) trigger.on("memory_template_updated", handle_memory_template_updated) # trigger.on("pr_created", handle_pr_created) # TDPLAN-0007: status-sync decommissioned # trigger.on("pr_merged", handle_pr_merged) # TDPLAN-0007: status-sync decommissioned diff --git a/src/aipass/trigger/tests/test_event_handlers.py b/src/aipass/trigger/tests/test_event_handlers.py index 40e92bfd..e9f28c01 100644 --- a/src/aipass/trigger/tests/test_event_handlers.py +++ b/src/aipass/trigger/tests/test_event_handlers.py @@ -6,7 +6,7 @@ # Modified: 2026-04-25 # ============================================= -"""Tests for cli, memory_template_updated, warning_logged, and bulletin_created event handlers.""" +"""Tests for cli, memory_template_updated, and warning_logged event handlers.""" import sys from pathlib import Path @@ -41,7 +41,6 @@ def _mock_infrastructure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Non "aipass.trigger.apps.handlers.events.cli", "aipass.trigger.apps.handlers.events.memory_template_updated", "aipass.trigger.apps.handlers.events.warning_logged", - "aipass.trigger.apps.handlers.events.bulletin_created", ): monkeypatch.delitem(sys.modules, mod_name, raising=False) @@ -67,13 +66,6 @@ def _import_warning_logged(): return m -def _import_bulletin(): - """Import bulletin_created handler module fresh after mocking.""" - import aipass.trigger.apps.handlers.events.bulletin_created as m - - return m - - # --------------------------------------------------------------------------- # cli.py -- handle_cli_header_displayed # --------------------------------------------------------------------------- @@ -197,65 +189,3 @@ class TestHandleWarningLogged: mod = _import_warning_logged() result = mod.handle_warning_logged() assert result is None - - -# --------------------------------------------------------------------------- -# bulletin_created.py -- handle_bulletin_created -# --------------------------------------------------------------------------- - - -class TestHandleBulletinCreated: - """Tests for handle_bulletin_created from bulletin_created.py.""" - - def test_does_not_raise_when_files_missing(self) -> None: - """Silently handles missing registry and bulletins files.""" - mod = _import_bulletin() - mod.handle_bulletin_created() - - def test_logs_operation_on_success(self) -> None: - """Logs bulletin_event after successful propagation.""" - mod = _import_bulletin() - mod._propagate_bulletins_to_branches = MagicMock() - from aipass.trigger.apps.handlers.json import json_handler - - json_handler.log_operation.reset_mock() # type: ignore[union-attr] - - mod.handle_bulletin_created() - - mod._propagate_bulletins_to_branches.assert_called_once() - json_handler.log_operation.assert_called_once_with( # type: ignore[union-attr] - "bulletin_event", {"success": True} - ) - - def test_catches_propagation_exception(self) -> None: - """Does not log operation when propagation raises.""" - mod = _import_bulletin() - mod._propagate_bulletins_to_branches = MagicMock(side_effect=RuntimeError("propagation failed")) - from aipass.trigger.apps.handlers.json import json_handler - - json_handler.log_operation.reset_mock() # type: ignore[union-attr] - - mod.handle_bulletin_created() - - json_handler.log_operation.assert_not_called() # type: ignore[union-attr] - - def test_accepts_all_params(self) -> None: - """Accepts all documented event parameters without error.""" - mod = _import_bulletin() - mod._propagate_bulletins_to_branches = MagicMock() - - mod.handle_bulletin_created( - _bulletin_id="b1", - _title="System update", - _message="Scheduled maintenance", - _priority="high", - _created_by="devpulse", - _timestamp="2026-04-25T10:00:00", - ) - - def test_returns_none(self) -> None: - """Handler returns None.""" - mod = _import_bulletin() - mod._propagate_bulletins_to_branches = MagicMock() - result = mod.handle_bulletin_created() - assert result is None