Compare commits
2 Commits
9c1f9f94e7
...
8b76a2dbf7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b76a2dbf7 | ||
|
|
f8ff971627 |
12
config.json
12
config.json
@@ -38,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
"cooldowns": {
|
"cooldowns": {
|
||||||
"email": 1800,
|
"email": 1800,
|
||||||
"calendar": 0,
|
"calendar": 1800,
|
||||||
"kb_index": 14400,
|
"kb_index": 14400,
|
||||||
"git": 14400
|
"git": 14400
|
||||||
}
|
}
|
||||||
@@ -48,15 +48,7 @@
|
|||||||
"WebFetch", "WebSearch",
|
"WebFetch", "WebSearch",
|
||||||
"Bash(python3 *)", "Bash(.venv/bin/python3 *)",
|
"Bash(python3 *)", "Bash(.venv/bin/python3 *)",
|
||||||
"Bash(pip *)", "Bash(pytest *)",
|
"Bash(pip *)", "Bash(pytest *)",
|
||||||
"Bash(git add *)", "Bash(git commit *)",
|
"Bash(git *)",
|
||||||
"Bash(git push)", "Bash(git push *)",
|
|
||||||
"Bash(git pull)", "Bash(git pull *)",
|
|
||||||
"Bash(git status)", "Bash(git status *)",
|
|
||||||
"Bash(git diff)", "Bash(git diff *)",
|
|
||||||
"Bash(git log)", "Bash(git log *)",
|
|
||||||
"Bash(git checkout *)",
|
|
||||||
"Bash(git branch)", "Bash(git branch *)",
|
|
||||||
"Bash(git stash)", "Bash(git stash *)",
|
|
||||||
"Bash(npm *)", "Bash(node *)", "Bash(npx *)",
|
"Bash(npm *)", "Bash(node *)", "Bash(npx *)",
|
||||||
"Bash(systemctl --user *)",
|
"Bash(systemctl --user *)",
|
||||||
"Bash(trash *)", "Bash(mkdir *)", "Bash(cp *)",
|
"Bash(trash *)", "Bash(mkdir *)", "Bash(cp *)",
|
||||||
|
|||||||
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