feat: devpulse dashboard plugin + startup protocol wiring (DPLAN-0180 Phases 2-6)
This commit is contained in:
@@ -6,11 +6,11 @@ User: user
|
||||
|
||||
# Startup protocol
|
||||
|
||||
On any greeting, silently read these files from CWD and run the commands — no narration, no announcing steps. Just do it and respond with the status.
|
||||
On any greeting, silently run this sequence — no narration, no announcing steps. Just do it and respond with the status.
|
||||
|
||||
- Read: `.trinity/passport.json`, `.trinity/local.json`, `.trinity/observations.json`, `README.md`, `STATUS.local.md`
|
||||
- Check: `drone @ai_mail inbox` — process any mail, don't ask.
|
||||
- Run: `drone @git status`
|
||||
- Refresh: `drone @prax dashboard refresh @<self>` — where `<self>` is your branch name (CWD directory name)
|
||||
- Dashboard: Read `DASHBOARD.local.json` — act on what needs attention (new mail → check inbox, active plans → note them). This is your single status glance.
|
||||
- Refresh: If `STATUS.local.md` is stale (last updated date older than latest session in local.json), update it from your memories. Keep Current Work accurate.
|
||||
|
||||
Use drone commands for all operations. Never raw git, gh, file access, or python -m when drone provides it.
|
||||
|
||||
@@ -497,6 +497,11 @@
|
||||
"file": "apps/__init__.py",
|
||||
"reason": "Package marker \u2014 not a CLI entry point. --version is in apps/prax.py."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_devpulse_dashboard_plugin.py",
|
||||
"standard": "architecture",
|
||||
"reason": "Test file lives in tests/ by convention, not in the 3-layer apps/ structure. This is standard for all prax test files."
|
||||
},
|
||||
{
|
||||
"file": "tests/test_logging.py",
|
||||
"standard": "architecture",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: __init__.py
|
||||
# Description: Prax plugins directory
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-05-16
|
||||
# Modified: 2026-05-16
|
||||
# =============================================
|
||||
|
||||
"""Prax plugins — extension point for cross-branch dashboard contributions."""
|
||||
@@ -0,0 +1,20 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: __init__.py
|
||||
# Description: Devpulse dashboard plugin for prax
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-05-16
|
||||
# Modified: 2026-05-16
|
||||
# =============================================
|
||||
|
||||
"""Devpulse dashboard plugin.
|
||||
|
||||
Populates devpulse's DASHBOARD.local.json with custom sections:
|
||||
git, session, dispatch (and optionally system_health).
|
||||
|
||||
Each section builder gathers data and calls write_section() to persist.
|
||||
refresh() orchestrates all section builders in sequence.
|
||||
"""
|
||||
|
||||
from .refresh import refresh
|
||||
|
||||
__all__ = ["refresh"]
|
||||
@@ -0,0 +1,85 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: dispatch_section.py
|
||||
# Description: Dispatch status section builder for devpulse dashboard
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-05-16
|
||||
# Modified: 2026-05-16
|
||||
# =============================================
|
||||
|
||||
"""Dispatch section builder for devpulse dashboard plugin.
|
||||
|
||||
Scans all 12 AIPass branches for .dispatch.lock files to show
|
||||
which agents are currently active. Writes to dashboard via write_section().
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from aipass.prax.apps.modules.dashboard import write_section
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
BRANCH_NAMES: List[str] = [
|
||||
"ai_mail",
|
||||
"aipass",
|
||||
"api",
|
||||
"cli",
|
||||
"devpulse",
|
||||
"drone",
|
||||
"flow",
|
||||
"memory",
|
||||
"prax",
|
||||
"seedgo",
|
||||
"spawn",
|
||||
"trigger",
|
||||
]
|
||||
|
||||
|
||||
def _get_aipass_src(branch_path: Path) -> Path:
|
||||
"""Resolve the src/aipass/ root from any branch path."""
|
||||
return branch_path.resolve().parent
|
||||
|
||||
|
||||
def _read_lock(lock_path: Path) -> Dict:
|
||||
"""Read dispatch lock file, return parsed data or empty dict."""
|
||||
try:
|
||||
data = json.loads(lock_path.read_text())
|
||||
return {
|
||||
"subject": data.get("subject", ""),
|
||||
"started": data.get("started", ""),
|
||||
}
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
logger.warning("Failed to read dispatch lock %s: %s", lock_path, e)
|
||||
return {"subject": "unknown", "started": ""}
|
||||
|
||||
|
||||
def build_dispatch_section(branch_path: Path) -> bool:
|
||||
"""Build dispatch section data and write to dashboard.
|
||||
|
||||
Scans all 12 branches for active dispatch locks.
|
||||
|
||||
Args:
|
||||
branch_path: Path to devpulse branch root.
|
||||
|
||||
Returns:
|
||||
True if write_section succeeded, False otherwise.
|
||||
"""
|
||||
aipass_src = _get_aipass_src(branch_path)
|
||||
|
||||
agents_active: List[str] = []
|
||||
details: Dict[str, Dict] = {}
|
||||
|
||||
for name in BRANCH_NAMES:
|
||||
lock_path = aipass_src / name / ".ai_mail.local" / ".dispatch.lock"
|
||||
if lock_path.exists():
|
||||
agents_active.append(name)
|
||||
details[name] = _read_lock(lock_path)
|
||||
|
||||
section_data: Dict = {
|
||||
"managed_by": "devpulse",
|
||||
"agents_active": agents_active,
|
||||
"agents_active_count": len(agents_active),
|
||||
"details": details,
|
||||
}
|
||||
|
||||
return write_section(branch_path, "dispatch", section_data)
|
||||
@@ -0,0 +1,90 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: git_section.py
|
||||
# Description: Git status section builder for devpulse dashboard
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-05-16
|
||||
# Modified: 2026-05-16
|
||||
# =============================================
|
||||
|
||||
"""Git section builder for devpulse dashboard plugin.
|
||||
|
||||
Gathers current git state (branch, changes, commits ahead of main)
|
||||
and writes to devpulse's DASHBOARD.local.json via write_section().
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
import subprocess
|
||||
|
||||
from aipass.prax.apps.modules.dashboard import write_section
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
def _find_repo_root(start: Path) -> Path:
|
||||
"""Walk up from start to find .git directory."""
|
||||
current = start.resolve()
|
||||
while current != current.parent:
|
||||
if (current / ".git").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
raise FileNotFoundError("No .git found above " + str(start))
|
||||
|
||||
|
||||
def _run_git(repo_root: Path, *args: str) -> str:
|
||||
"""Run a git command and return stdout. Returns empty string on failure."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git"] + list(args),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
return ""
|
||||
except (subprocess.SubprocessError, OSError) as e:
|
||||
logger.info(f"git command failed: {args} — {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def build_git_section(branch_path: Path) -> bool:
|
||||
"""Build git section data and write to dashboard.
|
||||
|
||||
Args:
|
||||
branch_path: Path to devpulse branch root.
|
||||
|
||||
Returns:
|
||||
True if write_section succeeded, False otherwise.
|
||||
"""
|
||||
repo_root = _find_repo_root(branch_path)
|
||||
|
||||
# Current branch
|
||||
branch = _run_git(repo_root, "rev-parse", "--abbrev-ref", "HEAD") or "unknown"
|
||||
|
||||
# Files changed (porcelain counts all modified/untracked)
|
||||
status_output = _run_git(repo_root, "status", "--porcelain")
|
||||
files_changed = len(status_output.splitlines()) if status_output else 0
|
||||
|
||||
# Commits ahead of origin/main
|
||||
ahead_str = _run_git(repo_root, "rev-list", "--count", "origin/main..HEAD")
|
||||
try:
|
||||
ahead_of_main = int(ahead_str)
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.info(f"Could not parse ahead count '{ahead_str}': {e}")
|
||||
ahead_of_main = 0
|
||||
|
||||
# Last commit message and date
|
||||
last_commit_msg = _run_git(repo_root, "log", "-1", "--format=%s") or ""
|
||||
last_commit_date = _run_git(repo_root, "log", "-1", "--format=%cs") or ""
|
||||
|
||||
section_data: Dict = {
|
||||
"managed_by": "devpulse",
|
||||
"branch": branch,
|
||||
"files_changed": files_changed,
|
||||
"ahead_of_main": ahead_of_main,
|
||||
"last_commit_msg": last_commit_msg,
|
||||
"last_commit_date": last_commit_date,
|
||||
}
|
||||
|
||||
return write_section(branch_path, "git", section_data)
|
||||
@@ -0,0 +1,72 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: refresh.py
|
||||
# Description: Orchestrates all devpulse custom dashboard sections
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-05-16
|
||||
# Modified: 2026-05-16
|
||||
# =============================================
|
||||
|
||||
"""Refresh orchestrator for devpulse dashboard plugin.
|
||||
|
||||
Calls each section builder in sequence. Each builder gathers its own data
|
||||
and writes to DASHBOARD.local.json via prax's write_section() API.
|
||||
Failures in one section don't block others.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
# Devpulse branch path — resolved relative to this file's location
|
||||
_AIPASS_SRC = Path(__file__).resolve().parents[4] # .../src/aipass/
|
||||
DEVPULSE_PATH = _AIPASS_SRC / "devpulse"
|
||||
|
||||
|
||||
def refresh(branch_path: Path = None) -> Dict:
|
||||
"""Refresh all devpulse custom dashboard sections.
|
||||
|
||||
Args:
|
||||
branch_path: Override path to devpulse branch root.
|
||||
Defaults to auto-resolved path.
|
||||
|
||||
Returns:
|
||||
Dict with results per section: {section_name: {"success": bool, "error": str|None}}
|
||||
"""
|
||||
target = branch_path or DEVPULSE_PATH
|
||||
results: Dict[str, Dict] = {}
|
||||
|
||||
builders: List[tuple] = [
|
||||
("git", _refresh_git),
|
||||
("session", _refresh_session),
|
||||
("dispatch", _refresh_dispatch),
|
||||
]
|
||||
|
||||
for name, builder_fn in builders:
|
||||
try:
|
||||
builder_fn(target)
|
||||
results[name] = {"success": True, "error": None}
|
||||
except Exception as e:
|
||||
logger.error("Failed to refresh section '%s': %s", name, e)
|
||||
results[name] = {"success": False, "error": str(e)}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _refresh_git(branch_path: Path) -> None:
|
||||
from .git_section import build_git_section
|
||||
|
||||
build_git_section(branch_path)
|
||||
|
||||
|
||||
def _refresh_session(branch_path: Path) -> None:
|
||||
from .session_section import build_session_section
|
||||
|
||||
build_session_section(branch_path)
|
||||
|
||||
|
||||
def _refresh_dispatch(branch_path: Path) -> None:
|
||||
from .dispatch_section import build_dispatch_section
|
||||
|
||||
build_dispatch_section(branch_path)
|
||||
@@ -0,0 +1,86 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: session_section.py
|
||||
# Description: Session info section builder for devpulse dashboard
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-05-16
|
||||
# Modified: 2026-05-16
|
||||
# =============================================
|
||||
|
||||
"""Session section builder for devpulse dashboard plugin.
|
||||
|
||||
Reads .trinity/local.json to extract current session ID, date,
|
||||
active tasks, and last session summary. Writes to dashboard
|
||||
via write_section().
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from aipass.prax.apps.modules.dashboard import write_section
|
||||
from aipass.prax.apps.modules.logger import system_logger as logger
|
||||
|
||||
|
||||
def build_session_section(branch_path: Path) -> bool:
|
||||
"""Build session section data and write to dashboard.
|
||||
|
||||
Args:
|
||||
branch_path: Path to devpulse branch root.
|
||||
|
||||
Returns:
|
||||
True if write_section succeeded, False otherwise.
|
||||
"""
|
||||
local_json_path = branch_path / ".trinity" / "local.json"
|
||||
|
||||
if not local_json_path.exists():
|
||||
section_data: Dict = {
|
||||
"managed_by": "devpulse",
|
||||
"current_session": "unknown",
|
||||
"session_date": "",
|
||||
"today_focus": "",
|
||||
"active_tasks": [],
|
||||
"last_session_summary": "",
|
||||
}
|
||||
return write_section(branch_path, "session", section_data)
|
||||
|
||||
try:
|
||||
data = json.loads(local_json_path.read_text())
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
logger.warning("Failed to read local.json at %s: %s", local_json_path, exc)
|
||||
section_data = {
|
||||
"managed_by": "devpulse",
|
||||
"current_session": "error",
|
||||
"session_date": "",
|
||||
"today_focus": "",
|
||||
"active_tasks": [],
|
||||
"last_session_summary": "Failed to read local.json",
|
||||
}
|
||||
return write_section(branch_path, "session", section_data)
|
||||
|
||||
# Extract latest session (first in list = newest)
|
||||
sessions = data.get("sessions", [])
|
||||
current_session = ""
|
||||
session_date = ""
|
||||
last_session_summary = ""
|
||||
|
||||
if sessions:
|
||||
latest = sessions[0]
|
||||
current_session = latest.get("id", "")
|
||||
session_date = latest.get("d", "")
|
||||
last_session_summary = latest.get("sum", "")
|
||||
|
||||
# Extract active tasks
|
||||
active_tasks_data = data.get("active_tasks", {})
|
||||
today_focus = active_tasks_data.get("today_focus", "")
|
||||
pending = active_tasks_data.get("pending", [])
|
||||
|
||||
section_data = {
|
||||
"managed_by": "devpulse",
|
||||
"current_session": current_session,
|
||||
"session_date": session_date,
|
||||
"today_focus": today_focus,
|
||||
"active_tasks": pending,
|
||||
"last_session_summary": last_session_summary,
|
||||
}
|
||||
|
||||
return write_section(branch_path, "session", section_data)
|
||||
@@ -0,0 +1,259 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_devpulse_dashboard_plugin.py
|
||||
# Description: Tests for devpulse dashboard plugin
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-05-16
|
||||
# Modified: 2026-05-16
|
||||
# =============================================
|
||||
|
||||
"""Tests for devpulse dashboard plugin (git, session, dispatch sections + refresh)."""
|
||||
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _git_subprocess_responses(cmd, **kwargs):
|
||||
"""Mock subprocess.run responses for git commands."""
|
||||
result = MagicMock()
|
||||
result.returncode = 0
|
||||
args = cmd[1:] # strip 'git'
|
||||
responses = {
|
||||
("rev-parse", "--abbrev-ref", "HEAD"): "dev\n",
|
||||
("status", "--porcelain"): " M file1.py\n M file2.py\n",
|
||||
("rev-list", "--count", "origin/main..HEAD"): "5\n",
|
||||
("log", "-1", "--format=%s"): "feat: add dashboard plugin\n",
|
||||
("log", "-1", "--format=%cs"): "2026-05-16\n",
|
||||
}
|
||||
result.stdout = responses.get(tuple(args), "")
|
||||
return result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def branch_path(tmp_path):
|
||||
"""Create a minimal branch directory structure."""
|
||||
(tmp_path / "DASHBOARD.local.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"_warning": "AUTO-GENERATED",
|
||||
"branch": "DEVPULSE",
|
||||
"last_updated": "",
|
||||
"quick_status": {"action_required": False},
|
||||
"sections": {},
|
||||
}
|
||||
)
|
||||
)
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def branch_with_trinity(branch_path):
|
||||
"""Branch path with .trinity/local.json."""
|
||||
trinity = branch_path / ".trinity"
|
||||
trinity.mkdir()
|
||||
local_data = {
|
||||
"sessions": [{"id": "S162", "d": "2026-05-16", "sum": "Test session summary", "st": "+"}],
|
||||
"active_tasks": {
|
||||
"today_focus": "Testing the plugin",
|
||||
"pending": ["Task 1", "Task 2"],
|
||||
"recently_completed": [],
|
||||
},
|
||||
}
|
||||
(trinity / "local.json").write_text(json.dumps(local_data))
|
||||
return branch_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def branch_with_git(branch_path):
|
||||
"""Branch path inside a fake git repo."""
|
||||
(branch_path / ".git").mkdir()
|
||||
return branch_path
|
||||
|
||||
|
||||
class TestGitSection:
|
||||
"""Tests for git_section.py."""
|
||||
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.git_section.subprocess.run")
|
||||
def test_build_git_section_success(self, mock_run, branch_with_git):
|
||||
"""Test successful git section build with mocked subprocess."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.git_section import build_git_section
|
||||
|
||||
mock_run.side_effect = _git_subprocess_responses
|
||||
result = build_git_section(branch_with_git)
|
||||
assert result is True
|
||||
|
||||
# Verify dashboard was written
|
||||
dash = json.loads((branch_with_git / "DASHBOARD.local.json").read_text())
|
||||
git = dash["sections"]["git"]
|
||||
assert git["managed_by"] == "devpulse"
|
||||
assert git["branch"] == "dev"
|
||||
assert git["files_changed"] == 2
|
||||
assert git["ahead_of_main"] == 5
|
||||
assert git["last_commit_msg"] == "feat: add dashboard plugin"
|
||||
assert git["last_commit_date"] == "2026-05-16"
|
||||
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.git_section.subprocess.run")
|
||||
def test_build_git_section_subprocess_failure(self, mock_run, branch_with_git):
|
||||
"""Test git section handles subprocess failures gracefully."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.git_section import build_git_section
|
||||
|
||||
mock_run.side_effect = OSError("git not found")
|
||||
# Should not raise — handles errors internally
|
||||
result = build_git_section(branch_with_git)
|
||||
# Still writes section with empty/default values
|
||||
assert result is True
|
||||
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.git_section._find_repo_root")
|
||||
def test_build_git_section_no_git_dir(self, mock_find_root, branch_path):
|
||||
"""Test git section when no .git directory exists."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.git_section import build_git_section
|
||||
|
||||
mock_find_root.side_effect = FileNotFoundError("No .git found above " + str(branch_path))
|
||||
with pytest.raises(FileNotFoundError):
|
||||
build_git_section(branch_path)
|
||||
|
||||
|
||||
class TestSessionSection:
|
||||
"""Tests for session_section.py."""
|
||||
|
||||
def test_build_session_section_success(self, branch_with_trinity):
|
||||
"""Test session section reads local.json correctly."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.session_section import build_session_section
|
||||
|
||||
result = build_session_section(branch_with_trinity)
|
||||
assert result is True
|
||||
|
||||
dash = json.loads((branch_with_trinity / "DASHBOARD.local.json").read_text())
|
||||
session = dash["sections"]["session"]
|
||||
assert session["managed_by"] == "devpulse"
|
||||
assert session["current_session"] == "S162"
|
||||
assert session["session_date"] == "2026-05-16"
|
||||
assert session["today_focus"] == "Testing the plugin"
|
||||
assert session["active_tasks"] == ["Task 1", "Task 2"]
|
||||
assert session["last_session_summary"] == "Test session summary"
|
||||
|
||||
def test_build_session_section_no_local_json(self, branch_path):
|
||||
"""Test session section when local.json doesn't exist."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.session_section import build_session_section
|
||||
|
||||
result = build_session_section(branch_path)
|
||||
assert result is True
|
||||
|
||||
dash = json.loads((branch_path / "DASHBOARD.local.json").read_text())
|
||||
session = dash["sections"]["session"]
|
||||
assert session["current_session"] == "unknown"
|
||||
|
||||
def test_build_session_section_corrupt_json(self, branch_path):
|
||||
"""Test session section handles corrupt local.json."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.session_section import build_session_section
|
||||
|
||||
trinity = branch_path / ".trinity"
|
||||
trinity.mkdir()
|
||||
(trinity / "local.json").write_text("not valid json{{{")
|
||||
|
||||
result = build_session_section(branch_path)
|
||||
assert result is True
|
||||
|
||||
dash = json.loads((branch_path / "DASHBOARD.local.json").read_text())
|
||||
session = dash["sections"]["session"]
|
||||
assert session["current_session"] == "error"
|
||||
|
||||
|
||||
class TestDispatchSection:
|
||||
"""Tests for dispatch_section.py."""
|
||||
|
||||
def test_build_dispatch_section_no_locks(self, branch_path):
|
||||
"""Test dispatch section when no agents are active."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.dispatch_section import build_dispatch_section
|
||||
|
||||
# Create sibling branch dirs (dispatch scans parent for all branches)
|
||||
parent = branch_path.parent
|
||||
for name in ["ai_mail", "prax", "seedgo"]:
|
||||
(parent / name / ".ai_mail.local").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
result = build_dispatch_section(branch_path)
|
||||
assert result is True
|
||||
|
||||
dash = json.loads((branch_path / "DASHBOARD.local.json").read_text())
|
||||
dispatch = dash["sections"]["dispatch"]
|
||||
assert dispatch["managed_by"] == "devpulse"
|
||||
assert dispatch["agents_active"] == []
|
||||
assert dispatch["agents_active_count"] == 0
|
||||
|
||||
def test_build_dispatch_section_with_locks(self, branch_path):
|
||||
"""Test dispatch section detects active dispatch locks."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.dispatch_section import build_dispatch_section
|
||||
|
||||
parent = branch_path.parent
|
||||
# Create lock for "prax"
|
||||
prax_mail = parent / "prax" / ".ai_mail.local"
|
||||
prax_mail.mkdir(parents=True, exist_ok=True)
|
||||
lock_data = {"pid": 12345, "subject": "Build dashboard view", "started": "2026-05-16T14:00:00"}
|
||||
(prax_mail / ".dispatch.lock").write_text(json.dumps(lock_data))
|
||||
|
||||
result = build_dispatch_section(branch_path)
|
||||
assert result is True
|
||||
|
||||
dash = json.loads((branch_path / "DASHBOARD.local.json").read_text())
|
||||
dispatch = dash["sections"]["dispatch"]
|
||||
assert "prax" in dispatch["agents_active"]
|
||||
assert dispatch["agents_active_count"] == 1
|
||||
assert dispatch["details"]["prax"]["subject"] == "Build dashboard view"
|
||||
|
||||
def test_build_dispatch_section_corrupt_lock(self, branch_path):
|
||||
"""Test dispatch section handles corrupt lock files."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.dispatch_section import build_dispatch_section
|
||||
|
||||
parent = branch_path.parent
|
||||
seedgo_mail = parent / "seedgo" / ".ai_mail.local"
|
||||
seedgo_mail.mkdir(parents=True, exist_ok=True)
|
||||
(seedgo_mail / ".dispatch.lock").write_text("broken json{{{")
|
||||
|
||||
result = build_dispatch_section(branch_path)
|
||||
assert result is True
|
||||
|
||||
dash = json.loads((branch_path / "DASHBOARD.local.json").read_text())
|
||||
dispatch = dash["sections"]["dispatch"]
|
||||
assert "seedgo" in dispatch["agents_active"]
|
||||
assert dispatch["details"]["seedgo"]["subject"] == "unknown"
|
||||
|
||||
|
||||
class TestRefresh:
|
||||
"""Tests for refresh.py orchestrator."""
|
||||
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.refresh._refresh_git")
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.refresh._refresh_session")
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.refresh._refresh_dispatch")
|
||||
def test_refresh_all_success(self, mock_dispatch, mock_session, mock_git, branch_path):
|
||||
"""Test refresh orchestrator calls all builders."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.refresh import refresh
|
||||
|
||||
results = refresh(branch_path)
|
||||
assert results["git"]["success"] is True
|
||||
assert results["session"]["success"] is True
|
||||
assert results["dispatch"]["success"] is True
|
||||
mock_git.assert_called_once_with(branch_path)
|
||||
mock_session.assert_called_once_with(branch_path)
|
||||
mock_dispatch.assert_called_once_with(branch_path)
|
||||
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.refresh._refresh_git")
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.refresh._refresh_session")
|
||||
@patch("aipass.prax.apps.plugins.devpulse_dashboard.refresh._refresh_dispatch")
|
||||
def test_refresh_partial_failure(self, mock_dispatch, mock_session, mock_git, branch_path):
|
||||
"""Test refresh continues when one section fails."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.refresh import refresh
|
||||
|
||||
mock_git.side_effect = FileNotFoundError("No .git")
|
||||
results = refresh(branch_path)
|
||||
assert results["git"]["success"] is False
|
||||
assert "No .git" in results["git"]["error"]
|
||||
assert results["session"]["success"] is True
|
||||
assert results["dispatch"]["success"] is True
|
||||
|
||||
def test_refresh_default_path(self):
|
||||
"""Test refresh uses DEVPULSE_PATH by default."""
|
||||
from aipass.prax.apps.plugins.devpulse_dashboard.refresh import DEVPULSE_PATH
|
||||
|
||||
assert DEVPULSE_PATH.name == "devpulse"
|
||||
assert "src/aipass/devpulse" in str(DEVPULSE_PATH)
|
||||
Reference in New Issue
Block a user