refactor(heartbeat): smart calendar with daily summary and dedup reminders
Calendar no longer bypasses quiet hours. First run after quiet hours sends full daily summary, subsequent runs only remind for next event within 45 min with deduplication. Calendar cooldown set to 30 min. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
"cooldowns": {
|
"cooldowns": {
|
||||||
"email": 1800,
|
"email": 1800,
|
||||||
"calendar": 0,
|
"calendar": 1800,
|
||||||
"kb_index": 14400,
|
"kb_index": 14400,
|
||||||
"git": 14400
|
"git": 14400
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/heartbeat.py
114
src/heartbeat.py
@@ -8,7 +8,7 @@ a Claude CLI session is triggered to handle them.
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, date, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -63,11 +63,11 @@ def run_heartbeat(config: dict | None = None) -> str:
|
|||||||
results.append(email_result)
|
results.append(email_result)
|
||||||
checks["email"] = now.isoformat()
|
checks["email"] = now.isoformat()
|
||||||
|
|
||||||
# Check 2: Calendar (critical — pierces quiet hours)
|
# Check 2: Calendar — daily summary + next-event reminder (no quiet hours bypass)
|
||||||
if check_flags.get("calendar"):
|
if check_flags.get("calendar") and not is_quiet and _should_run("calendar", checks, now, cooldowns):
|
||||||
cal_result = _check_calendar(state)
|
cal_result = _check_calendar_smart(state, quiet_hours)
|
||||||
if cal_result:
|
if cal_result:
|
||||||
critical.append(cal_result)
|
results.append(cal_result)
|
||||||
checks["calendar"] = now.isoformat()
|
checks["calendar"] = now.isoformat()
|
||||||
|
|
||||||
# Check 3: KB index freshness + auto-reindex
|
# Check 3: KB index freshness + auto-reindex
|
||||||
@@ -171,14 +171,77 @@ def _check_email(state: dict) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _check_calendar(state: dict) -> str | None:
|
def _check_calendar_smart(state: dict, quiet_hours: tuple) -> str | None:
|
||||||
"""Check upcoming calendar events via tools/calendar_check.py. Parses JSON."""
|
"""Smart calendar check: daily summary once, then only next-event reminders.
|
||||||
|
|
||||||
|
- First run after quiet hours: full day summary (all events today)
|
||||||
|
- Subsequent runs: only the nearest event within 45 min, deduplicated
|
||||||
|
"""
|
||||||
script = TOOLS_DIR / "calendar_check.py"
|
script = TOOLS_DIR / "calendar_check.py"
|
||||||
if not script.exists():
|
if not script.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
today_str = date.today().isoformat()
|
||||||
|
cal_state = state.setdefault("calendar", {})
|
||||||
|
sent_summary_date = cal_state.get("daily_summary_date")
|
||||||
|
reminded_events = cal_state.get("reminded_events", {})
|
||||||
|
|
||||||
|
# Clean old reminded_events (from previous days)
|
||||||
|
reminded_events = {
|
||||||
|
k: v for k, v in reminded_events.items() if v == today_str
|
||||||
|
}
|
||||||
|
cal_state["reminded_events"] = reminded_events
|
||||||
|
|
||||||
|
is_first_run_today = sent_summary_date != today_str
|
||||||
|
|
||||||
|
if is_first_run_today:
|
||||||
|
# Daily summary: all events for today
|
||||||
|
return _calendar_daily_summary(cal_state, today_str)
|
||||||
|
else:
|
||||||
|
# Next-event reminder (within 45 min, deduplicated)
|
||||||
|
return _calendar_next_reminder(cal_state, today_str, reminded_events)
|
||||||
|
|
||||||
|
|
||||||
|
def _calendar_daily_summary(cal_state: dict, today_str: str) -> str | None:
|
||||||
|
"""Fetch all today's events and return a daily summary."""
|
||||||
|
script = TOOLS_DIR / "calendar_check.py"
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["python3", str(script), "soon"],
|
["python3", str(script), "today"],
|
||||||
|
capture_output=True, text=True, timeout=30,
|
||||||
|
cwd=str(PROJECT_ROOT)
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
output = result.stdout.strip()
|
||||||
|
if not output:
|
||||||
|
return None
|
||||||
|
data = json.loads(output)
|
||||||
|
today_events = data.get("today", [])
|
||||||
|
if not today_events:
|
||||||
|
cal_state["daily_summary_date"] = today_str
|
||||||
|
return None
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
for event in today_events:
|
||||||
|
time = event.get("time", "")
|
||||||
|
name = event.get("summary", "?")
|
||||||
|
parts.append(f" {time} — {name}")
|
||||||
|
|
||||||
|
cal_state["daily_summary_date"] = today_str
|
||||||
|
return "📅 Program azi:\n" + "\n".join(parts)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Calendar daily summary failed: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _calendar_next_reminder(cal_state: dict, today_str: str,
|
||||||
|
reminded_events: dict) -> str | None:
|
||||||
|
"""Remind only for the next upcoming event within 45 min, if not already reminded."""
|
||||||
|
script = TOOLS_DIR / "calendar_check.py"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["python3", str(script), "soon", "1"],
|
||||||
capture_output=True, text=True, timeout=30,
|
capture_output=True, text=True, timeout=30,
|
||||||
cwd=str(PROJECT_ROOT)
|
cwd=str(PROJECT_ROOT)
|
||||||
)
|
)
|
||||||
@@ -191,21 +254,28 @@ def _check_calendar(state: dict) -> str | None:
|
|||||||
upcoming = data.get("upcoming", [])
|
upcoming = data.get("upcoming", [])
|
||||||
if not upcoming:
|
if not upcoming:
|
||||||
return None
|
return None
|
||||||
parts = []
|
|
||||||
for event in upcoming:
|
event = upcoming[0]
|
||||||
mins = event.get("minutes_until", "?")
|
mins = event.get("minutes_until", 999)
|
||||||
name = event.get("summary", "?")
|
name = event.get("summary", "?")
|
||||||
time = event.get("time", "")
|
time = event.get("time", "")
|
||||||
parts.append(f"in {mins} min — {name} ({time})")
|
|
||||||
return "Calendar: " + "; ".join(parts)
|
# Only remind if within 45 minutes
|
||||||
except json.JSONDecodeError:
|
if isinstance(mins, (int, float)) and mins > 45:
|
||||||
# Fallback: treat as plain text
|
return None
|
||||||
output = result.stdout.strip()
|
|
||||||
if output:
|
# Dedup key: event name + time
|
||||||
return f"Calendar: {output}"
|
dedup_key = f"{name}@{time}"
|
||||||
return None
|
if dedup_key in reminded_events:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Mark as reminded
|
||||||
|
reminded_events[dedup_key] = today_str
|
||||||
|
cal_state["reminded_events"] = reminded_events
|
||||||
|
|
||||||
|
return f"⏰ in {mins} min — {name} ({time})"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("Calendar check failed: %s", e)
|
log.warning("Calendar next reminder failed: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.heartbeat import (
|
from src.heartbeat import (
|
||||||
_check_calendar,
|
_calendar_daily_summary,
|
||||||
|
_calendar_next_reminder,
|
||||||
|
_check_calendar_smart,
|
||||||
_check_email,
|
_check_email,
|
||||||
_check_git,
|
_check_git,
|
||||||
_check_kb_index,
|
_check_kb_index,
|
||||||
@@ -207,52 +209,113 @@ class TestCheckEmail:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCheckCalendar:
|
class TestCheckCalendarSmart:
|
||||||
"""Test calendar check via tools/calendar_check.py with JSON parsing."""
|
"""Test smart calendar: daily summary + next-event reminders with dedup."""
|
||||||
|
|
||||||
def test_no_script(self, tmp_env):
|
def test_no_script(self, tmp_env):
|
||||||
assert _check_calendar({}) is None
|
assert _check_calendar_smart({}, (23, 8)) is None
|
||||||
|
|
||||||
def test_json_with_upcoming(self, tmp_env):
|
def test_first_run_sends_daily_summary(self, tmp_env):
|
||||||
|
"""First run after quiet hours should return full day summary."""
|
||||||
|
script = tmp_env["tools"] / "calendar_check.py"
|
||||||
|
script.write_text("pass")
|
||||||
|
output = json.dumps({
|
||||||
|
"today": [
|
||||||
|
{"summary": "Standup", "time": "09:00"},
|
||||||
|
{"summary": "Lunch", "time": "12:00"},
|
||||||
|
],
|
||||||
|
"tomorrow": []
|
||||||
|
})
|
||||||
|
mock_result = MagicMock(returncode=0, stdout=output)
|
||||||
|
state = {}
|
||||||
|
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
||||||
|
result = _check_calendar_smart(state, (23, 8))
|
||||||
|
assert "Program azi" in result
|
||||||
|
assert "09:00 — Standup" in result
|
||||||
|
assert "12:00 — Lunch" in result
|
||||||
|
assert state["calendar"]["daily_summary_date"] == date.today().isoformat()
|
||||||
|
|
||||||
|
def test_second_run_sends_next_event_only(self, tmp_env):
|
||||||
|
"""After daily summary, only remind for next event within 45 min."""
|
||||||
script = tmp_env["tools"] / "calendar_check.py"
|
script = tmp_env["tools"] / "calendar_check.py"
|
||||||
script.write_text("pass")
|
script.write_text("pass")
|
||||||
output = json.dumps({
|
output = json.dumps({
|
||||||
"upcoming": [
|
"upcoming": [
|
||||||
{"summary": "NLP Session", "minutes_until": 45, "time": "15:00"},
|
{"summary": "Meeting", "minutes_until": 30, "time": "10:00"},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
mock_result = MagicMock(returncode=0, stdout=output)
|
mock_result = MagicMock(returncode=0, stdout=output)
|
||||||
|
state = {"calendar": {"daily_summary_date": date.today().isoformat(), "reminded_events": {}}}
|
||||||
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
||||||
result = _check_calendar({})
|
result = _check_calendar_smart(state, (23, 8))
|
||||||
assert result == "Calendar: in 45 min — NLP Session (15:00)"
|
assert "in 30 min — Meeting (10:00)" in result
|
||||||
|
assert "Meeting@10:00" in state["calendar"]["reminded_events"]
|
||||||
|
|
||||||
def test_json_no_upcoming(self, tmp_env):
|
def test_dedup_prevents_repeat_reminder(self, tmp_env):
|
||||||
|
"""Same event should not be reminded twice."""
|
||||||
script = tmp_env["tools"] / "calendar_check.py"
|
script = tmp_env["tools"] / "calendar_check.py"
|
||||||
script.write_text("pass")
|
script.write_text("pass")
|
||||||
output = json.dumps({"upcoming": []})
|
output = json.dumps({
|
||||||
|
"upcoming": [
|
||||||
|
{"summary": "Meeting", "minutes_until": 25, "time": "10:00"},
|
||||||
|
]
|
||||||
|
})
|
||||||
mock_result = MagicMock(returncode=0, stdout=output)
|
mock_result = MagicMock(returncode=0, stdout=output)
|
||||||
|
today_str = date.today().isoformat()
|
||||||
|
state = {"calendar": {
|
||||||
|
"daily_summary_date": today_str,
|
||||||
|
"reminded_events": {"Meeting@10:00": today_str}
|
||||||
|
}}
|
||||||
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
||||||
assert _check_calendar({}) is None
|
result = _check_calendar_smart(state, (23, 8))
|
||||||
|
assert result is None
|
||||||
|
|
||||||
def test_plaintext_fallback(self, tmp_env):
|
def test_event_beyond_45_min_not_reminded(self, tmp_env):
|
||||||
|
"""Events more than 45 min away should not trigger a reminder."""
|
||||||
script = tmp_env["tools"] / "calendar_check.py"
|
script = tmp_env["tools"] / "calendar_check.py"
|
||||||
script.write_text("pass")
|
script.write_text("pass")
|
||||||
mock_result = MagicMock(returncode=0, stdout="Meeting at 3pm\n")
|
output = json.dumps({
|
||||||
|
"upcoming": [
|
||||||
|
{"summary": "Later", "minutes_until": 90, "time": "14:00"},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
mock_result = MagicMock(returncode=0, stdout=output)
|
||||||
|
state = {"calendar": {"daily_summary_date": date.today().isoformat(), "reminded_events": {}}}
|
||||||
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
||||||
assert _check_calendar({}) == "Calendar: Meeting at 3pm"
|
result = _check_calendar_smart(state, (23, 8))
|
||||||
|
assert result is None
|
||||||
|
|
||||||
def test_empty_output(self, tmp_env):
|
def test_no_events_today(self, tmp_env):
|
||||||
|
"""No events returns None and still marks summary as sent."""
|
||||||
script = tmp_env["tools"] / "calendar_check.py"
|
script = tmp_env["tools"] / "calendar_check.py"
|
||||||
script.write_text("pass")
|
script.write_text("pass")
|
||||||
mock_result = MagicMock(returncode=0, stdout="\n")
|
output = json.dumps({"today": [], "tomorrow": []})
|
||||||
|
mock_result = MagicMock(returncode=0, stdout=output)
|
||||||
|
state = {}
|
||||||
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
||||||
assert _check_calendar({}) is None
|
result = _check_calendar_smart(state, (23, 8))
|
||||||
|
assert result is None
|
||||||
|
assert state["calendar"]["daily_summary_date"] == date.today().isoformat()
|
||||||
|
|
||||||
|
def test_old_reminded_events_cleaned(self, tmp_env):
|
||||||
|
"""Reminded events from previous days are cleaned up."""
|
||||||
|
script = tmp_env["tools"] / "calendar_check.py"
|
||||||
|
script.write_text("pass")
|
||||||
|
output = json.dumps({"today": [{"summary": "X", "time": "09:00"}], "tomorrow": []})
|
||||||
|
mock_result = MagicMock(returncode=0, stdout=output)
|
||||||
|
state = {"calendar": {
|
||||||
|
"daily_summary_date": "2020-01-01",
|
||||||
|
"reminded_events": {"OldEvent@09:00": "2020-01-01"}
|
||||||
|
}}
|
||||||
|
with patch("src.heartbeat.subprocess.run", return_value=mock_result):
|
||||||
|
_check_calendar_smart(state, (23, 8))
|
||||||
|
assert "OldEvent@09:00" not in state["calendar"]["reminded_events"]
|
||||||
|
|
||||||
def test_subprocess_exception(self, tmp_env):
|
def test_subprocess_exception(self, tmp_env):
|
||||||
script = tmp_env["tools"] / "calendar_check.py"
|
script = tmp_env["tools"] / "calendar_check.py"
|
||||||
script.write_text("pass")
|
script.write_text("pass")
|
||||||
with patch("src.heartbeat.subprocess.run", side_effect=OSError("fail")):
|
with patch("src.heartbeat.subprocess.run", side_effect=OSError("fail")):
|
||||||
assert _check_calendar({}) is None
|
assert _check_calendar_smart({}, (23, 8)) is None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -401,7 +464,7 @@ class TestRunHeartbeat:
|
|||||||
|
|
||||||
def test_all_ok(self, tmp_env):
|
def test_all_ok(self, tmp_env):
|
||||||
with patch("src.heartbeat._check_email", return_value=None), \
|
with patch("src.heartbeat._check_email", return_value=None), \
|
||||||
patch("src.heartbeat._check_calendar", return_value=None), \
|
patch("src.heartbeat._check_calendar_smart", return_value=None), \
|
||||||
patch("src.heartbeat._check_kb_index", return_value=None), \
|
patch("src.heartbeat._check_kb_index", return_value=None), \
|
||||||
patch("src.heartbeat._check_git", return_value=None), \
|
patch("src.heartbeat._check_git", return_value=None), \
|
||||||
patch("src.heartbeat._run_claude_extra", return_value=None):
|
patch("src.heartbeat._run_claude_extra", return_value=None):
|
||||||
@@ -410,7 +473,7 @@ class TestRunHeartbeat:
|
|||||||
|
|
||||||
def test_with_results(self, tmp_env):
|
def test_with_results(self, tmp_env):
|
||||||
with patch("src.heartbeat._check_email", return_value="Email: 2 necitite (X, Y)"), \
|
with patch("src.heartbeat._check_email", return_value="Email: 2 necitite (X, Y)"), \
|
||||||
patch("src.heartbeat._check_calendar", return_value=None), \
|
patch("src.heartbeat._check_calendar_smart", return_value=None), \
|
||||||
patch("src.heartbeat._check_kb_index", return_value="KB: 1 fișiere reindexate"), \
|
patch("src.heartbeat._check_kb_index", return_value="KB: 1 fișiere reindexate"), \
|
||||||
patch("src.heartbeat._check_git", return_value=None), \
|
patch("src.heartbeat._check_git", return_value=None), \
|
||||||
patch("src.heartbeat._is_quiet_hour", return_value=False), \
|
patch("src.heartbeat._is_quiet_hour", return_value=False), \
|
||||||
@@ -418,29 +481,22 @@ class TestRunHeartbeat:
|
|||||||
result = run_heartbeat()
|
result = run_heartbeat()
|
||||||
assert result == "Email: 2 necitite (X, Y) | KB: 1 fișiere reindexate"
|
assert result == "Email: 2 necitite (X, Y) | KB: 1 fișiere reindexate"
|
||||||
|
|
||||||
def test_quiet_hours_suppresses_normal(self, tmp_env):
|
def test_quiet_hours_suppresses_all(self, tmp_env):
|
||||||
|
"""During quiet hours, calendar is also suppressed (no longer critical)."""
|
||||||
with patch("src.heartbeat._check_email", return_value="Email: 5 necitite (A)"), \
|
with patch("src.heartbeat._check_email", return_value="Email: 5 necitite (A)"), \
|
||||||
patch("src.heartbeat._check_calendar", return_value=None), \
|
patch("src.heartbeat._check_calendar_smart", return_value="Calendar stuff") as mock_cal, \
|
||||||
patch("src.heartbeat._check_kb_index", return_value=None), \
|
patch("src.heartbeat._check_kb_index", return_value=None), \
|
||||||
patch("src.heartbeat._check_git", return_value="Git: 2 uncommitted"), \
|
patch("src.heartbeat._check_git", return_value="Git: 2 uncommitted"), \
|
||||||
patch("src.heartbeat._is_quiet_hour", return_value=True):
|
patch("src.heartbeat._is_quiet_hour", return_value=True):
|
||||||
result = run_heartbeat()
|
result = run_heartbeat()
|
||||||
assert result == "HEARTBEAT_OK"
|
assert result == "HEARTBEAT_OK"
|
||||||
|
mock_cal.assert_not_called()
|
||||||
def test_quiet_hours_allows_critical_calendar(self, tmp_env):
|
|
||||||
with patch("src.heartbeat._check_email", return_value="Email: 5 necitite (A)"), \
|
|
||||||
patch("src.heartbeat._check_calendar", return_value="Calendar: in 30 min — Meeting (15:00)"), \
|
|
||||||
patch("src.heartbeat._check_kb_index", return_value=None), \
|
|
||||||
patch("src.heartbeat._check_git", return_value="Git: 2 uncommitted"), \
|
|
||||||
patch("src.heartbeat._is_quiet_hour", return_value=True):
|
|
||||||
result = run_heartbeat()
|
|
||||||
assert result == "Calendar: in 30 min — Meeting (15:00)"
|
|
||||||
|
|
||||||
def test_config_disables_check(self, tmp_env):
|
def test_config_disables_check(self, tmp_env):
|
||||||
"""Checks can be disabled via config."""
|
"""Checks can be disabled via config."""
|
||||||
config = {"heartbeat": {"checks": {"git": False}}}
|
config = {"heartbeat": {"checks": {"git": False}}}
|
||||||
with patch("src.heartbeat._check_email", return_value=None), \
|
with patch("src.heartbeat._check_email", return_value=None), \
|
||||||
patch("src.heartbeat._check_calendar", return_value=None), \
|
patch("src.heartbeat._check_calendar_smart", return_value=None), \
|
||||||
patch("src.heartbeat._check_kb_index", return_value=None), \
|
patch("src.heartbeat._check_kb_index", return_value=None), \
|
||||||
patch("src.heartbeat._check_git", return_value="Git: 5 uncommitted") as mock_git, \
|
patch("src.heartbeat._check_git", return_value="Git: 5 uncommitted") as mock_git, \
|
||||||
patch("src.heartbeat._run_claude_extra", return_value=None):
|
patch("src.heartbeat._run_claude_extra", return_value=None):
|
||||||
@@ -452,7 +508,7 @@ class TestRunHeartbeat:
|
|||||||
"""Quiet hours can be overridden via config."""
|
"""Quiet hours can be overridden via config."""
|
||||||
config = {"heartbeat": {"quiet_hours": [0, 1]}} # only 0-1 is quiet
|
config = {"heartbeat": {"quiet_hours": [0, 1]}} # only 0-1 is quiet
|
||||||
with patch("src.heartbeat._check_email", return_value=None), \
|
with patch("src.heartbeat._check_email", return_value=None), \
|
||||||
patch("src.heartbeat._check_calendar", return_value=None), \
|
patch("src.heartbeat._check_calendar_smart", return_value=None), \
|
||||||
patch("src.heartbeat._check_kb_index", return_value=None), \
|
patch("src.heartbeat._check_kb_index", return_value=None), \
|
||||||
patch("src.heartbeat._check_git", return_value="Git: 3 uncommitted"), \
|
patch("src.heartbeat._check_git", return_value="Git: 3 uncommitted"), \
|
||||||
patch("src.heartbeat._is_quiet_hour", return_value=False), \
|
patch("src.heartbeat._is_quiet_hour", return_value=False), \
|
||||||
@@ -462,7 +518,7 @@ class TestRunHeartbeat:
|
|||||||
|
|
||||||
def test_saves_state_after_run(self, tmp_env):
|
def test_saves_state_after_run(self, tmp_env):
|
||||||
with patch("src.heartbeat._check_email", return_value=None), \
|
with patch("src.heartbeat._check_email", return_value=None), \
|
||||||
patch("src.heartbeat._check_calendar", return_value=None), \
|
patch("src.heartbeat._check_calendar_smart", return_value=None), \
|
||||||
patch("src.heartbeat._check_kb_index", return_value=None), \
|
patch("src.heartbeat._check_kb_index", return_value=None), \
|
||||||
patch("src.heartbeat._check_git", return_value=None), \
|
patch("src.heartbeat._check_git", return_value=None), \
|
||||||
patch("src.heartbeat._run_claude_extra", return_value=None):
|
patch("src.heartbeat._run_claude_extra", return_value=None):
|
||||||
@@ -473,7 +529,7 @@ class TestRunHeartbeat:
|
|||||||
|
|
||||||
def test_saves_check_timestamps(self, tmp_env):
|
def test_saves_check_timestamps(self, tmp_env):
|
||||||
with patch("src.heartbeat._check_email", return_value=None), \
|
with patch("src.heartbeat._check_email", return_value=None), \
|
||||||
patch("src.heartbeat._check_calendar", return_value=None), \
|
patch("src.heartbeat._check_calendar_smart", return_value=None), \
|
||||||
patch("src.heartbeat._check_kb_index", return_value=None), \
|
patch("src.heartbeat._check_kb_index", return_value=None), \
|
||||||
patch("src.heartbeat._check_git", return_value=None), \
|
patch("src.heartbeat._check_git", return_value=None), \
|
||||||
patch("src.heartbeat._run_claude_extra", return_value=None):
|
patch("src.heartbeat._run_claude_extra", return_value=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user