fix(ai_mail): retire dashboard_sync writer — stop writing the ai_mail dashboard section (prax self-sources)
This commit is contained in:
@@ -47,6 +47,11 @@ PyPI version — not the changelog header.
|
||||
`.ai_mail.local/inbox.json` directly (`_read_mail_counts`) for `new_mail`/
|
||||
`opened_mail`, so the `ai_mail` section is no longer a data dependency and can
|
||||
be retired. 116 prax tests, seedgo 100%.
|
||||
- **Retired `@ai_mail`'s dashboard section writer (completes the dashboard
|
||||
slim).** ai_mail no longer writes to the dashboard — removed
|
||||
`push_dashboard_update` from 5 call sites and archived `dashboard_sync.py`.
|
||||
With prax self-sourcing mail counts, the `ai_mail` section now stays gone (a
|
||||
mail op no longer re-adds it — verified). 737 ai_mail tests.
|
||||
- **`.backupignore` is now a true `.gitignore` for the backup system — a single
|
||||
source of truth (FPLAN-0269).** Replaced the hand-rolled `fnmatch`+part-loop
|
||||
matcher (which broke leading-slash anchoring, `*`-crossing-`/`, dir-only `foo/`,
|
||||
|
||||
@@ -60,11 +60,6 @@
|
||||
"standard": "deep_nesting",
|
||||
"reason": "2 functions: get_user_by_email() depth 4, get_all_users() depth 4 — registry lookup with path normalization and validation"
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/email/dashboard_sync.py",
|
||||
"standard": "handlers",
|
||||
"reason": "Imports prax.apps.modules.dashboard.write_section — cross-branch module import required for dashboard integration. No ai_mail module wraps this."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/email/delivery.py",
|
||||
"standard": "handlers",
|
||||
@@ -120,11 +115,6 @@
|
||||
"standard": "naming",
|
||||
"reason": "False positive — _append_footer is a function reference stored in a local variable, not a module-level constant."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/email/dashboard_sync.py",
|
||||
"standard": "naming",
|
||||
"reason": "False positive — _write_section is a lazy-import function reference, not a module-level constant."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/email/delivery.py",
|
||||
"standard": "naming",
|
||||
@@ -200,11 +190,6 @@
|
||||
"standard": "deep_nesting",
|
||||
"reason": "_send_direct() depth 5 (arg parsing with branch resolution, --from flag, --dispatch flag), handle_close() depth 4 (close with archive + dashboard update)"
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/email/dashboard_sync.py",
|
||||
"standard": "deep_nesting",
|
||||
"reason": "_human_readable_age() depth 5, _calculate_section_data() depth 5 — timestamp parsing with multiple fallback formats"
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/dispatch/dispatch_monitor.py",
|
||||
"standard": "deep_nesting",
|
||||
|
||||
@@ -56,24 +56,17 @@ def batch_close(
|
||||
|
||||
def batch_close_post_ops(
|
||||
branch_path: Path,
|
||||
push_dashboard_fn: Optional[Callable] = None,
|
||||
update_central_fn: Optional[Callable] = None,
|
||||
purge_deleted_fn: Optional[Callable] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Run post-operations after a batch close (dashboard update + purge).
|
||||
Run post-operations after a batch close (central update + purge).
|
||||
|
||||
Args:
|
||||
branch_path: Path to branch directory
|
||||
push_dashboard_fn: Optional push_dashboard_update callable
|
||||
update_central_fn: Optional update_central callable
|
||||
purge_deleted_fn: Optional purge_deleted_folder callable
|
||||
"""
|
||||
if push_dashboard_fn:
|
||||
try:
|
||||
push_dashboard_fn(branch_path)
|
||||
except Exception as e:
|
||||
logger.warning("[close] push_dashboard_fn failed for %s: %s", branch_path, e)
|
||||
if update_central_fn:
|
||||
try:
|
||||
update_central_fn()
|
||||
|
||||
@@ -93,25 +93,18 @@ def on_email_delivered(
|
||||
new_count: int,
|
||||
opened_count: int,
|
||||
total: int,
|
||||
push_dashboard_fn: Optional[Callable] = None,
|
||||
update_central_fn: Optional[Callable] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Post-delivery callback: update dashboard and central.
|
||||
Post-delivery callback: update central.
|
||||
|
||||
Args:
|
||||
branch_path: Path to the branch that received email
|
||||
new_count: Number of new (unread) messages
|
||||
opened_count: Number of opened messages
|
||||
total: Total message count
|
||||
push_dashboard_fn: Callable for push_dashboard_update
|
||||
update_central_fn: Callable for update_central
|
||||
"""
|
||||
if push_dashboard_fn:
|
||||
try:
|
||||
push_dashboard_fn(branch_path)
|
||||
except Exception as e:
|
||||
logger.warning("[error_dispatch] dashboard update failed for %s: %s", branch_path, e)
|
||||
if update_central_fn:
|
||||
try:
|
||||
update_central_fn()
|
||||
|
||||
@@ -38,13 +38,6 @@ def _get_inbox_lock():
|
||||
return _inbox_lock
|
||||
|
||||
|
||||
def _get_push_dashboard_update() -> Any:
|
||||
"""Lazy import push_dashboard_update from dashboard_sync."""
|
||||
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
|
||||
|
||||
return push_dashboard_update
|
||||
|
||||
|
||||
def _get_update_central() -> Any:
|
||||
"""Lazy import update_central."""
|
||||
from aipass.ai_mail.apps.handlers.central_writer import update_central
|
||||
@@ -191,13 +184,7 @@ def mark_all_read_and_archive(branch_path: Path) -> Tuple[bool, str, int]:
|
||||
|
||||
|
||||
def _update_dashboard(branch_path: Path, new: int, opened: int, total: int) -> None:
|
||||
"""Update dashboard ai_mail section with enriched data via write-through API."""
|
||||
try:
|
||||
_get_push_dashboard_update()(branch_path)
|
||||
except Exception as e:
|
||||
logger.warning("[cleanup] dashboard update failed for %s: %s", branch_path, e)
|
||||
|
||||
# Update central after any inbox changes
|
||||
"""Update central stats after inbox changes."""
|
||||
try:
|
||||
_get_update_central()()
|
||||
except Exception as e:
|
||||
|
||||
@@ -267,7 +267,6 @@ def _orchestrate_dispatch_send(args: List[str]) -> bool:
|
||||
from aipass.ai_mail.apps.handlers.email.delivery import deliver_email_to_branch
|
||||
from aipass.ai_mail.apps.handlers.email.header import prepend_dispatch_header
|
||||
from aipass.ai_mail.apps.handlers.email.error_dispatch import dispatch_send_error, on_email_delivered
|
||||
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
|
||||
from aipass.ai_mail.apps.handlers.users.user import get_current_user
|
||||
from aipass.ai_mail.apps.handlers.registry.read import get_branch_by_email
|
||||
|
||||
@@ -286,7 +285,6 @@ def _orchestrate_dispatch_send(args: List[str]) -> bool:
|
||||
new_count,
|
||||
opened_count,
|
||||
total,
|
||||
push_dashboard_fn=push_dashboard_update,
|
||||
update_central_fn=update_central,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,15 +23,8 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
# Infrastructure
|
||||
_AI_MAIL_DIR = Path(__file__).resolve().parents[2]
|
||||
_REPO_ROOT = _AI_MAIL_DIR.parents[2]
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.cli.apps.modules import console, error
|
||||
|
||||
# Handlers - business logic providers
|
||||
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
|
||||
from aipass.ai_mail.apps.handlers.email.create import load_email_file
|
||||
from aipass.ai_mail.apps.handlers.email.format import format_email_list_item, format_email_header
|
||||
from aipass.ai_mail.apps.handlers.email.inbox_ops import load_inbox
|
||||
@@ -48,6 +41,9 @@ from aipass.ai_mail.apps.handlers.email.close_ops import batch_close, batch_clos
|
||||
from aipass.ai_mail.apps.handlers.email.inbox_resolve import resolve_inbox_target
|
||||
from aipass.ai_mail.apps.modules.email_send import handle_send
|
||||
|
||||
_AI_MAIL_DIR = Path(__file__).resolve().parents[2]
|
||||
_REPO_ROOT = _AI_MAIL_DIR.parents[2]
|
||||
|
||||
try:
|
||||
from aipass.ai_mail.apps.handlers.central_writer import update_central
|
||||
except ImportError as e:
|
||||
@@ -255,7 +251,7 @@ def handle_close(args: List[str]) -> bool:
|
||||
except ImportError as e:
|
||||
logger.warning("[email] purge import unavailable: %s", e)
|
||||
run_purge = None
|
||||
batch_close_post_ops(branch_path, push_dashboard_update, update_central, run_purge)
|
||||
batch_close_post_ops(branch_path, update_central, run_purge)
|
||||
console.print(f"\nClosed {closed}, failed {failed}")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -387,7 +383,6 @@ def print_introspection():
|
||||
console.print(" - reply.py (get_email_by_id — retrieve email by message ID)")
|
||||
console.print(" - reply.py (send_reply — send reply to an email)")
|
||||
console.print(" - header.py (prepend_dispatch_header — prepend dispatch header to message)")
|
||||
console.print(" - dashboard_sync.py (push_dashboard_update — push email stats to dashboard)")
|
||||
console.print(" - error_dispatch.py (dispatch_send_error — handle and report send errors)")
|
||||
console.print(" - error_dispatch.py (on_email_delivered — post-delivery callback handler)")
|
||||
console.print(" handlers/users/")
|
||||
|
||||
@@ -17,14 +17,10 @@ under the size threshold.
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
_AI_MAIL_DIR = Path(__file__).resolve().parents[2]
|
||||
_REPO_ROOT = _AI_MAIL_DIR.parents[2]
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.cli.apps.modules import console, error
|
||||
from aipass.trigger.apps.modules.core import trigger
|
||||
|
||||
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
|
||||
from aipass.ai_mail.apps.handlers.email.delivery import deliver_email_to_branch
|
||||
from aipass.ai_mail.apps.handlers.email.create import create_email_file, load_email_file
|
||||
from aipass.ai_mail.apps.handlers.email.header import prepend_dispatch_header
|
||||
@@ -40,6 +36,9 @@ from aipass.ai_mail.apps.handlers.email.send import (
|
||||
from aipass.ai_mail.apps.handlers.email.error_dispatch import dispatch_send_error, on_email_delivered
|
||||
from aipass.ai_mail.apps.handlers.email.send_args import parse_send_args, resolve_dispatch_target
|
||||
|
||||
_AI_MAIL_DIR = Path(__file__).resolve().parents[2]
|
||||
_REPO_ROOT = _AI_MAIL_DIR.parents[2]
|
||||
|
||||
try:
|
||||
from aipass.ai_mail.apps.handlers.central_writer import update_central
|
||||
except ImportError as e:
|
||||
@@ -54,7 +53,6 @@ def _delivery_callback(branch_path, new_count, opened_count, total):
|
||||
new_count,
|
||||
opened_count,
|
||||
total,
|
||||
push_dashboard_fn=push_dashboard_update,
|
||||
update_central_fn=update_central,
|
||||
)
|
||||
|
||||
|
||||
@@ -120,13 +120,11 @@ def test_batch_close_post_ops_all_fns_called(tmp_path: Path):
|
||||
branch_path = tmp_path / "branch"
|
||||
branch_path.mkdir()
|
||||
|
||||
push_fn = MagicMock()
|
||||
central_fn = MagicMock()
|
||||
purge_fn = MagicMock()
|
||||
|
||||
mod.batch_close_post_ops(branch_path, push_fn, central_fn, purge_fn)
|
||||
mod.batch_close_post_ops(branch_path, central_fn, purge_fn)
|
||||
|
||||
push_fn.assert_called_once_with(branch_path)
|
||||
central_fn.assert_called_once_with()
|
||||
purge_fn.assert_called_once_with(branch_path / ".ai_mail.local")
|
||||
|
||||
@@ -137,22 +135,7 @@ def test_batch_close_post_ops_none_fns(tmp_path: Path):
|
||||
branch_path.mkdir()
|
||||
|
||||
# Should not raise
|
||||
mod.batch_close_post_ops(branch_path, None, None, None)
|
||||
|
||||
|
||||
def test_batch_close_post_ops_push_exception_suppressed(tmp_path: Path):
|
||||
"""Exception in push_dashboard_fn is caught; other fns still called."""
|
||||
branch_path = tmp_path / "branch"
|
||||
branch_path.mkdir()
|
||||
|
||||
push_fn = MagicMock(side_effect=RuntimeError("push failed"))
|
||||
central_fn = MagicMock()
|
||||
purge_fn = MagicMock()
|
||||
|
||||
mod.batch_close_post_ops(branch_path, push_fn, central_fn, purge_fn)
|
||||
|
||||
central_fn.assert_called_once()
|
||||
purge_fn.assert_called_once()
|
||||
mod.batch_close_post_ops(branch_path, None, None)
|
||||
|
||||
|
||||
def test_batch_close_post_ops_central_exception_suppressed(tmp_path: Path):
|
||||
@@ -160,13 +143,11 @@ def test_batch_close_post_ops_central_exception_suppressed(tmp_path: Path):
|
||||
branch_path = tmp_path / "branch"
|
||||
branch_path.mkdir()
|
||||
|
||||
push_fn = MagicMock()
|
||||
central_fn = MagicMock(side_effect=RuntimeError("central failed"))
|
||||
purge_fn = MagicMock()
|
||||
|
||||
mod.batch_close_post_ops(branch_path, push_fn, central_fn, purge_fn)
|
||||
mod.batch_close_post_ops(branch_path, central_fn, purge_fn)
|
||||
|
||||
push_fn.assert_called_once()
|
||||
purge_fn.assert_called_once()
|
||||
|
||||
|
||||
@@ -175,13 +156,11 @@ def test_batch_close_post_ops_purge_exception_suppressed(tmp_path: Path):
|
||||
branch_path = tmp_path / "branch"
|
||||
branch_path.mkdir()
|
||||
|
||||
push_fn = MagicMock()
|
||||
central_fn = MagicMock()
|
||||
purge_fn = MagicMock(side_effect=RuntimeError("purge failed"))
|
||||
|
||||
mod.batch_close_post_ops(branch_path, push_fn, central_fn, purge_fn)
|
||||
mod.batch_close_post_ops(branch_path, central_fn, purge_fn)
|
||||
|
||||
push_fn.assert_called_once()
|
||||
central_fn.assert_called_once()
|
||||
|
||||
|
||||
@@ -192,6 +171,6 @@ def test_batch_close_post_ops_partial_fns(tmp_path: Path):
|
||||
|
||||
central_fn = MagicMock()
|
||||
|
||||
mod.batch_close_post_ops(branch_path, None, central_fn, None)
|
||||
mod.batch_close_post_ops(branch_path, central_fn, None)
|
||||
|
||||
central_fn.assert_called_once_with()
|
||||
|
||||
@@ -47,7 +47,6 @@ _H_CREATE = "aipass.ai_mail.apps.handlers.email.create"
|
||||
_H_DELIVERY = "aipass.ai_mail.apps.handlers.email.delivery"
|
||||
_H_HEADER = "aipass.ai_mail.apps.handlers.email.header"
|
||||
_H_ERR = "aipass.ai_mail.apps.handlers.email.error_dispatch"
|
||||
_H_DASH = "aipass.ai_mail.apps.handlers.email.dashboard_sync"
|
||||
_H_USERS = "aipass.ai_mail.apps.handlers.users.user"
|
||||
_H_REG = "aipass.ai_mail.apps.handlers.registry.read"
|
||||
_H_CENTRAL = "aipass.ai_mail.apps.handlers.central_writer"
|
||||
@@ -658,7 +657,6 @@ def _send_patches(overrides: dict | None = None) -> ExitStack:
|
||||
f"{_H_HEADER}.prepend_dispatch_header": MagicMock(return_value="[DISPATCH] Body"),
|
||||
f"{_H_SEND}.send_to_single": MagicMock(return_value=(True, None)),
|
||||
f"{_H_ERR}.on_email_delivered": MagicMock(),
|
||||
f"{_H_DASH}.push_dashboard_update": MagicMock(),
|
||||
f"{_H_USERS}.get_current_user": MagicMock(return_value={"name": "test"}),
|
||||
f"{_H_REG}.get_branch_by_email": MagicMock(return_value={"email": "@target"}),
|
||||
f"{_H_CENTRAL}.update_central": MagicMock(),
|
||||
|
||||
@@ -385,7 +385,7 @@ class TestHandleClose:
|
||||
post_ops_called = []
|
||||
monkeypatch.setattr(
|
||||
"aipass.ai_mail.apps.modules.email.batch_close_post_ops",
|
||||
lambda bp, push_fn, central_fn, purge_fn: post_ops_called.append(True),
|
||||
lambda bp, central_fn, purge_fn: post_ops_called.append(True),
|
||||
)
|
||||
mock_console = MagicMock()
|
||||
mock_console.print = lambda msg, **kw: None
|
||||
@@ -1286,7 +1286,7 @@ class TestHandleCloseExtended:
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"aipass.ai_mail.apps.modules.email.batch_close_post_ops",
|
||||
lambda bp, push_fn, central_fn, purge_fn: None,
|
||||
lambda bp, central_fn, purge_fn: None,
|
||||
)
|
||||
printed: list[str] = []
|
||||
errors: list[str] = []
|
||||
@@ -1338,7 +1338,7 @@ class TestHandleCloseExtended:
|
||||
post_ops_called: list[bool] = []
|
||||
monkeypatch.setattr(
|
||||
"aipass.ai_mail.apps.modules.email.batch_close_post_ops",
|
||||
lambda bp, push_fn, central_fn, purge_fn: post_ops_called.append(True),
|
||||
lambda bp, central_fn, purge_fn: post_ops_called.append(True),
|
||||
)
|
||||
printed: list[str] = []
|
||||
mock_console = MagicMock()
|
||||
@@ -1450,7 +1450,6 @@ class TestDeliveryCallback:
|
||||
new_count,
|
||||
opened_count,
|
||||
total,
|
||||
push_dashboard_fn=None,
|
||||
update_central_fn=None,
|
||||
):
|
||||
"""Capture on_email_delivered arguments."""
|
||||
@@ -1460,7 +1459,6 @@ class TestDeliveryCallback:
|
||||
"new_count": new_count,
|
||||
"opened_count": opened_count,
|
||||
"total": total,
|
||||
"push_dashboard_fn": push_dashboard_fn,
|
||||
"update_central_fn": update_central_fn,
|
||||
}
|
||||
)
|
||||
@@ -1478,7 +1476,6 @@ class TestDeliveryCallback:
|
||||
assert delivered_args[0]["new_count"] == 3
|
||||
assert delivered_args[0]["opened_count"] == 2
|
||||
assert delivered_args[0]["total"] == 5
|
||||
assert delivered_args[0]["push_dashboard_fn"] is not None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
|
||||
@@ -155,51 +155,34 @@ def test_dispatch_send_error_passes_correct_email_data(monkeypatch):
|
||||
# ---- on_email_delivered tests --------------------------------
|
||||
|
||||
|
||||
def test_on_email_delivered_with_both_callbacks():
|
||||
"""Both callbacks are invoked when provided."""
|
||||
push_fn = MagicMock()
|
||||
def test_on_email_delivered_with_central_callback():
|
||||
"""Central callback is invoked when provided."""
|
||||
update_fn = MagicMock()
|
||||
branch_path = "/some/path"
|
||||
|
||||
on_email_delivered(branch_path, 3, 1, 10, push_fn, update_fn)
|
||||
on_email_delivered(branch_path, 3, 1, 10, update_central_fn=update_fn)
|
||||
|
||||
push_fn.assert_called_once_with(branch_path)
|
||||
update_fn.assert_called_once_with()
|
||||
|
||||
|
||||
def test_on_email_delivered_with_none_callbacks():
|
||||
"""No error when both callbacks are None."""
|
||||
on_email_delivered("/some/path", 3, 1, 10, None, None)
|
||||
|
||||
|
||||
def test_on_email_delivered_dashboard_failure_does_not_block_central():
|
||||
"""Dashboard failure does not prevent central update from running."""
|
||||
push_fn = MagicMock(side_effect=RuntimeError("dashboard broken"))
|
||||
update_fn = MagicMock()
|
||||
|
||||
on_email_delivered("/some/path", 3, 1, 10, push_fn, update_fn)
|
||||
|
||||
push_fn.assert_called_once()
|
||||
update_fn.assert_called_once()
|
||||
"""No error when callback is None."""
|
||||
on_email_delivered("/some/path", 3, 1, 10, None)
|
||||
|
||||
|
||||
def test_on_email_delivered_central_failure_does_not_raise():
|
||||
"""Central update failure is caught silently."""
|
||||
push_fn = MagicMock()
|
||||
update_fn = MagicMock(side_effect=RuntimeError("central broken"))
|
||||
|
||||
on_email_delivered("/some/path", 3, 1, 10, push_fn, update_fn)
|
||||
on_email_delivered("/some/path", 3, 1, 10, update_central_fn=update_fn)
|
||||
|
||||
push_fn.assert_called_once()
|
||||
update_fn.assert_called_once()
|
||||
|
||||
|
||||
def test_on_email_delivered_both_fail_no_exception():
|
||||
"""Both callbacks failing does not raise any exception."""
|
||||
push_fn = MagicMock(side_effect=RuntimeError("push fail"))
|
||||
def test_on_email_delivered_central_fail_no_exception():
|
||||
"""Central callback failing does not raise any exception."""
|
||||
update_fn = MagicMock(side_effect=RuntimeError("update fail"))
|
||||
|
||||
on_email_delivered("/some/path", 3, 1, 10, push_fn, update_fn)
|
||||
on_email_delivered("/some/path", 3, 1, 10, update_central_fn=update_fn)
|
||||
|
||||
push_fn.assert_called_once()
|
||||
update_fn.assert_called_once()
|
||||
|
||||
@@ -42,12 +42,6 @@ def _mock_inbox_lock(monkeypatch):
|
||||
monkeypatch.setattr(mod, "_get_inbox_lock", lambda: _noop_lock)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_dashboard(monkeypatch):
|
||||
"""Replace _get_push_dashboard_update with a no-op."""
|
||||
monkeypatch.setattr(mod, "_get_push_dashboard_update", lambda: lambda _bp: None)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_central(monkeypatch):
|
||||
"""Replace _get_update_central with a no-op."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for miscellaneous handlers -- central_writer.update_central, dispatch status.check_pid_status,
|
||||
daemon.run_daemon, json_handler.increment_counter/update_data_metrics, delivery.deliver_to_inbox_file,
|
||||
dashboard_sync.push_dashboard_update, inbox_resolve.resolve_inbox_target."""
|
||||
inbox_resolve.resolve_inbox_target."""
|
||||
|
||||
import json
|
||||
import os
|
||||
@@ -14,7 +14,6 @@ import aipass.ai_mail.apps.handlers.central_writer as central_mod
|
||||
import aipass.ai_mail.apps.handlers.dispatch.daemon as daemon_mod
|
||||
import aipass.ai_mail.apps.handlers.json_utils.json_handler as json_handler_mod
|
||||
import aipass.ai_mail.apps.handlers.email.delivery as delivery_mod
|
||||
import aipass.ai_mail.apps.handlers.email.dashboard_sync as dashboard_mod
|
||||
from aipass.ai_mail.apps.handlers.central_writer import update_central
|
||||
from aipass.ai_mail.apps.handlers.dispatch.status import check_pid_status
|
||||
from aipass.ai_mail.apps.handlers.json_utils.json_handler import (
|
||||
@@ -22,7 +21,6 @@ from aipass.ai_mail.apps.handlers.json_utils.json_handler import (
|
||||
update_data_metrics,
|
||||
)
|
||||
from aipass.ai_mail.apps.handlers.email.delivery import deliver_to_inbox_file
|
||||
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
|
||||
from aipass.ai_mail.apps.handlers.email.inbox_resolve import resolve_inbox_target
|
||||
|
||||
|
||||
@@ -61,14 +59,6 @@ def _silence_json_handler_delivery():
|
||||
yield mock_jh
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _silence_json_handler_dashboard():
|
||||
"""Prevent log_operation in dashboard_sync from writing real JSON files."""
|
||||
with patch("aipass.ai_mail.apps.handlers.email.dashboard_sync.json_handler") as mock_jh:
|
||||
mock_jh.log_operation.return_value = True
|
||||
yield mock_jh
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _silence_json_handler_inbox_resolve():
|
||||
"""Prevent log_operation in inbox_resolve from writing real JSON files."""
|
||||
@@ -399,64 +389,6 @@ def test_deliver_to_inbox_file_preserves_existing_messages(tmp_path, _noop_inbox
|
||||
assert result["messages"][1]["subject"] == "Old email"
|
||||
|
||||
|
||||
# ==============================================================
|
||||
# push_dashboard_update tests
|
||||
# ==============================================================
|
||||
|
||||
|
||||
def test_push_dashboard_update_happy_path(tmp_path):
|
||||
"""Successful dashboard push returns True."""
|
||||
branch_path = tmp_path / "trigger"
|
||||
inbox_dir = branch_path / ".ai_mail.local"
|
||||
inbox_dir.mkdir(parents=True)
|
||||
inbox_file = inbox_dir / "inbox.json"
|
||||
inbox_data = {
|
||||
"messages": [
|
||||
{"id": "m1", "status": "new", "timestamp": "2026-04-01 10:00:00"},
|
||||
{"id": "m2", "status": "opened", "timestamp": "2026-04-01 09:00:00"},
|
||||
]
|
||||
}
|
||||
inbox_file.write_text(json.dumps(inbox_data), encoding="utf-8")
|
||||
|
||||
mock_write = MagicMock(return_value=True)
|
||||
|
||||
with patch.object(dashboard_mod, "_get_write_section", return_value=mock_write):
|
||||
result = push_dashboard_update(branch_path)
|
||||
|
||||
assert result is True
|
||||
mock_write.assert_called_once()
|
||||
section_data = mock_write.call_args[0][1]
|
||||
assert section_data == "ai_mail"
|
||||
|
||||
|
||||
def test_push_dashboard_update_no_inbox(tmp_path):
|
||||
"""Returns True with zero stats when no inbox exists."""
|
||||
branch_path = tmp_path / "empty_branch"
|
||||
branch_path.mkdir()
|
||||
|
||||
mock_write = MagicMock(return_value=True)
|
||||
|
||||
with patch.object(dashboard_mod, "_get_write_section", return_value=mock_write):
|
||||
result = push_dashboard_update(branch_path)
|
||||
|
||||
assert result is True
|
||||
mock_write.assert_called_once()
|
||||
section_data = mock_write.call_args[0][2]
|
||||
assert section_data["new"] == 0
|
||||
assert section_data["total"] == 0
|
||||
|
||||
|
||||
def test_push_dashboard_update_catches_exceptions(tmp_path):
|
||||
"""Returns False on any exception (never raises)."""
|
||||
branch_path = tmp_path / "broken"
|
||||
branch_path.mkdir()
|
||||
|
||||
with patch.object(dashboard_mod, "_get_write_section", side_effect=RuntimeError("broken")):
|
||||
result = push_dashboard_update(branch_path)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
# ==============================================================
|
||||
# resolve_inbox_target tests
|
||||
# ==============================================================
|
||||
|
||||
Reference in New Issue
Block a user