J.A.R.V.I.S-py/tests/test_outlook_handlers.py
Bossiara13 5265cb0fbe
Some checks are pending
Python CI / pytest (ubuntu-latest) (push) Waiting to run
Python CI / pytest (windows-latest) (push) Waiting to run
Python CI / ruff (push) Waiting to run
feat: Wave 5 Python parity — outlook COM + time tracker
Outlook COM (`outlook_handlers.py`, agent):
- 4 handlers mirroring Rust pack: unread_count, latest, send_clipboard,
  summarize_inbox. Uses `win32com.client` with lazy import so the module
  loads even when pywin32 is missing — handlers degrade gracefully and
  speak a "Outlook недоступен" reply.
- `requirements.txt`: pywin32>=305 (optional).
- 7 unit tests mock `_outlook_session` via unittest.mock.

Time tracker (`time_tracker_handlers.py`, agent):
- 5 handlers: start, stop, today, week, reset. JSON store at
  `<here>/time_tracker.json`, atomic write-then-rename.
- Same shape as the Rust pack's `jarvis.state` blob: current_session_start
  + sessions[{start,end}]. Local-calendar today boundary.
- Full Russian pluralisation (час/часа/часов, минута/минуты/минут).
- 14 unit tests using the existing `isolated_state` fixture in conftest.py.

`extensions.py` + `main.py`: imports + 9 new proxy do_* functions + 9
new dispatch elifs. `commands.yaml`: 9 new entries.

Tests: 60 → 81 (+14 tracker + 7 outlook).
2026-05-16 14:34:44 +03:00

103 lines
3 KiB
Python

"""Tests for outlook_handlers — COM bridge mocked via unittest.mock.
We patch the module-level `_outlook_session` to return a fake (app, ns) pair
made of MagicMocks so we never need a live Outlook install.
"""
import importlib
import sys
from unittest.mock import MagicMock, patch
def _fresh_module():
if 'outlook_handlers' in sys.modules:
del sys.modules['outlook_handlers']
return importlib.import_module('outlook_handlers')
def _make_inbox_with_unread(count):
"""Build a fake Inbox object whose UnReadItemCount is `count`."""
inbox = MagicMock()
inbox.UnReadItemCount = count
return inbox
def test_unread_count_speaks_singular_for_one():
mod = _fresh_module()
spoken = []
mod.set_speak(lambda t: spoken.append(t))
app = MagicMock()
ns = MagicMock()
ns.GetDefaultFolder.return_value = _make_inbox_with_unread(1)
with patch.object(mod, '_outlook_session', return_value=(app, ns)):
mod.do_outlook_unread_count({}, '')
assert spoken, "expected the handler to speak"
assert "Одно" in spoken[0]
def test_unread_count_zero_says_no_unread():
mod = _fresh_module()
spoken = []
mod.set_speak(lambda t: spoken.append(t))
app = MagicMock()
ns = MagicMock()
ns.GetDefaultFolder.return_value = _make_inbox_with_unread(0)
with patch.object(mod, '_outlook_session', return_value=(app, ns)):
mod.do_outlook_unread_count({}, '')
assert spoken
assert "нет" in spoken[0].lower()
def test_unread_count_plural_count():
mod = _fresh_module()
spoken = []
mod.set_speak(lambda t: spoken.append(t))
ns = MagicMock()
ns.GetDefaultFolder.return_value = _make_inbox_with_unread(7)
with patch.object(mod, '_outlook_session', return_value=(MagicMock(), ns)):
mod.do_outlook_unread_count({}, '')
assert "7" in spoken[0]
def test_unavailable_outlook_says_so():
mod = _fresh_module()
spoken = []
mod.set_speak(lambda t: spoken.append(t))
with patch.object(mod, '_outlook_session', return_value=(None, None)):
mod.do_outlook_unread_count({}, '')
assert spoken
assert "Outlook" in spoken[0]
def test_extract_recipient_strips_trigger():
mod = _fresh_module()
assert mod._extract_recipient("отправь письмо Дмитрию") == "Дмитрию"
assert mod._extract_recipient("email this to Bob") == "Bob"
assert mod._extract_recipient("Bob") == "Bob"
assert mod._extract_recipient("") == ""
def test_send_clipboard_no_recipient_prompts_for_name():
mod = _fresh_module()
spoken = []
mod.set_speak(lambda t: spoken.append(t))
# No trigger and no recipient in the voice -> _extract_recipient returns ""
mod.do_outlook_send_clipboard({}, "")
assert spoken
assert "имя" in spoken[0].lower() or "name" in spoken[0].lower()
def test_strip_html_basic():
mod = _fresh_module()
assert mod._strip_html("<b>Hi</b>&nbsp;there") == "Hi there"
assert mod._strip_html("") == ""