chore(trigger): retire dead bulletin_created dashboard writer
The bulletin_created event handler propagated a 'bulletin_board' section into every branch dashboard, but it was fully dead: nothing fired the event, its BULLETINS.central.json store no longer exists, and prax already prunes 'bulletin_board' via DEPRECATED_SECTIONS. Archived the handler to events/.archive/, removed its import + trigger.on() registration, dropped the 5 covering tests (558 pass). seedgo audit 100%. prax pruning left intact. Closes td-102. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QEQZXCtgnF3NQtcttTErpq
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
feecd263eb
commit
c771a22771
@@ -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).
|
||||
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user