diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f1d3af..e6a5966c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/`, diff --git a/src/aipass/ai_mail/.seedgo/bypass.json b/src/aipass/ai_mail/.seedgo/bypass.json index 23abf4ee..a2ec3006 100644 --- a/src/aipass/ai_mail/.seedgo/bypass.json +++ b/src/aipass/ai_mail/.seedgo/bypass.json @@ -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", diff --git a/src/aipass/ai_mail/apps/handlers/email/close_ops.py b/src/aipass/ai_mail/apps/handlers/email/close_ops.py index 5d9f1e63..9cb2d5f2 100644 --- a/src/aipass/ai_mail/apps/handlers/email/close_ops.py +++ b/src/aipass/ai_mail/apps/handlers/email/close_ops.py @@ -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() diff --git a/src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py b/src/aipass/ai_mail/apps/handlers/email/dashboard_sync(disabled).py similarity index 100% rename from src/aipass/ai_mail/apps/handlers/email/dashboard_sync.py rename to src/aipass/ai_mail/apps/handlers/email/dashboard_sync(disabled).py diff --git a/src/aipass/ai_mail/apps/handlers/email/error_dispatch.py b/src/aipass/ai_mail/apps/handlers/email/error_dispatch.py index 4e684697..c48ee6be 100644 --- a/src/aipass/ai_mail/apps/handlers/email/error_dispatch.py +++ b/src/aipass/ai_mail/apps/handlers/email/error_dispatch.py @@ -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() diff --git a/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py b/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py index 4a3c8b49..6d331efc 100644 --- a/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py +++ b/src/aipass/ai_mail/apps/handlers/email/inbox_cleanup.py @@ -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: diff --git a/src/aipass/ai_mail/apps/modules/dispatch.py b/src/aipass/ai_mail/apps/modules/dispatch.py index 5b4e0302..986bccbc 100644 --- a/src/aipass/ai_mail/apps/modules/dispatch.py +++ b/src/aipass/ai_mail/apps/modules/dispatch.py @@ -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, ) diff --git a/src/aipass/ai_mail/apps/modules/email.py b/src/aipass/ai_mail/apps/modules/email.py index fd3c5372..32fc0dcd 100644 --- a/src/aipass/ai_mail/apps/modules/email.py +++ b/src/aipass/ai_mail/apps/modules/email.py @@ -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/") diff --git a/src/aipass/ai_mail/apps/modules/email_send.py b/src/aipass/ai_mail/apps/modules/email_send.py index 7e30a1af..ceddc702 100644 --- a/src/aipass/ai_mail/apps/modules/email_send.py +++ b/src/aipass/ai_mail/apps/modules/email_send.py @@ -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, ) diff --git a/src/aipass/ai_mail/tests/test_close_ops.py b/src/aipass/ai_mail/tests/test_close_ops.py index a4cdb485..c453bfbe 100644 --- a/src/aipass/ai_mail/tests/test_close_ops.py +++ b/src/aipass/ai_mail/tests/test_close_ops.py @@ -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() diff --git a/src/aipass/ai_mail/tests/test_dispatch_module.py b/src/aipass/ai_mail/tests/test_dispatch_module.py index 55ad96a0..a8e4859b 100644 --- a/src/aipass/ai_mail/tests/test_dispatch_module.py +++ b/src/aipass/ai_mail/tests/test_dispatch_module.py @@ -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(), diff --git a/src/aipass/ai_mail/tests/test_email_module.py b/src/aipass/ai_mail/tests/test_email_module.py index 2f683ecf..be09c753 100644 --- a/src/aipass/ai_mail/tests/test_email_module.py +++ b/src/aipass/ai_mail/tests/test_email_module.py @@ -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 # =========================================================================== diff --git a/src/aipass/ai_mail/tests/test_error_dispatch.py b/src/aipass/ai_mail/tests/test_error_dispatch.py index 61be67b9..bcfdafb5 100644 --- a/src/aipass/ai_mail/tests/test_error_dispatch.py +++ b/src/aipass/ai_mail/tests/test_error_dispatch.py @@ -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() diff --git a/src/aipass/ai_mail/tests/test_inbox_cleanup.py b/src/aipass/ai_mail/tests/test_inbox_cleanup.py index 28c94536..1adab444 100644 --- a/src/aipass/ai_mail/tests/test_inbox_cleanup.py +++ b/src/aipass/ai_mail/tests/test_inbox_cleanup.py @@ -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.""" diff --git a/src/aipass/ai_mail/tests/test_misc_handlers.py b/src/aipass/ai_mail/tests/test_misc_handlers.py index af8934a0..a1afa1d7 100644 --- a/src/aipass/ai_mail/tests/test_misc_handlers.py +++ b/src/aipass/ai_mail/tests/test_misc_handlers.py @@ -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 # ==============================================================