mirror of
https://github.com/agno-agi/agno.git
synced 2026-10-11 22:09:58 +00:00
[fix] Keep Google Calendar slots within working hours (#10878)
## Summary Fixes #10880. `find_available_slots` can offer 16:30–17:15 for a 45-minute appointment when working hours end at 17:00. Comparing only the end hour also admits appointments that cross midnight. Compare the complete slot end with the closing time on its start date, preserving appointments that end exactly at closing time and custom working hours. ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [x] Code complies with style guidelines - [x] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [x] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [x] Tested in clean environment - [x] Tests added/updated (if applicable) ### Duplicate and AI-Generated PR Check - [x] I have searched existing open pull requests and confirmed that no other PR already addresses this issue - [ ] If a similar PR exists, I have explained below why this PR is a better approach - [x] Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.) --- ## Additional Notes Validation in a fresh Python 3.12 environment: - Before the fix, six new regression cases failed and the 30-minute control passed. - After the fix, all 70 tests in `libs/agno/tests/unit/tools/test_google_calendar.py` passed. - Changed files pass Ruff lint, Ruff formatting, and `git diff --check`. - `scripts/format.sh` passed. - `scripts/validate.sh` passed: all Ruff checks, mypy for 1,091 Agno and 21 agnoctl source files, and the cookbook pattern check. The tests cover minute overflow, exact closing time, a full working day, oversized and next-day appointments, and custom closing hours. No Google credentials or network calls are used. AI tool: Codex. Co-authored-by: Sannya Singal <[email protected]>
This commit is contained in:
co-authored by
Sannya Singal
parent
1a07cbcb4f
commit
718ae26fb9
@@ -610,7 +610,8 @@ class GoogleCalendarTools(GoogleToolkit):
|
||||
is_available = False
|
||||
break
|
||||
|
||||
if is_available and slot_end.hour <= working_hours_end:
|
||||
working_day_end = current.replace(hour=working_hours_end, minute=0, second=0, microsecond=0)
|
||||
if is_available and slot_end <= working_day_end:
|
||||
available_slots.append({"start": current.isoformat(), "end": slot_end.isoformat()})
|
||||
|
||||
current += datetime.timedelta(minutes=30)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
@@ -464,6 +465,48 @@ class TestFetchAllEvents:
|
||||
|
||||
|
||||
class TestFindAvailableSlots:
|
||||
@pytest.mark.parametrize(
|
||||
("duration_minutes", "end_hour", "expected_last_end", "expected_count"),
|
||||
[
|
||||
(30, 17, "17:00:00", 16),
|
||||
(45, 17, "16:45:00", 15),
|
||||
(60, 17, "17:00:00", 15),
|
||||
(480, 17, "17:00:00", 1),
|
||||
(481, 17, None, 0),
|
||||
(1500, 17, None, 0),
|
||||
(45, 18, "17:45:00", 17),
|
||||
],
|
||||
)
|
||||
@patch.object(GoogleCalendarTools, "fetch_all_events")
|
||||
@patch.object(GoogleCalendarTools, "_get_working_hours")
|
||||
def test_available_slots_end_within_working_hours(
|
||||
self,
|
||||
mock_working_hours,
|
||||
mock_fetch,
|
||||
calendar_tools,
|
||||
duration_minutes,
|
||||
end_hour,
|
||||
expected_last_end,
|
||||
expected_count,
|
||||
):
|
||||
mock_working_hours.return_value = json.dumps(
|
||||
{"start_hour": 9, "end_hour": end_hour, "timezone": "UTC", "locale": "en"}
|
||||
)
|
||||
mock_fetch.return_value = json.dumps([])
|
||||
|
||||
result = json.loads(
|
||||
calendar_tools.find_available_slots("2026-10-02", "2026-10-02", duration_minutes=duration_minutes)
|
||||
)
|
||||
|
||||
slots = result["available_slots"]
|
||||
assert len(slots) == expected_count
|
||||
if expected_last_end is not None:
|
||||
assert slots[-1]["end"] == f"2026-10-02T{expected_last_end}+00:00"
|
||||
for slot in slots:
|
||||
start = datetime.fromisoformat(slot["start"])
|
||||
end = datetime.fromisoformat(slot["end"])
|
||||
assert end <= start.replace(hour=end_hour, minute=0, second=0, microsecond=0)
|
||||
|
||||
@patch.object(GoogleCalendarTools, "fetch_all_events")
|
||||
@patch.object(GoogleCalendarTools, "_get_working_hours")
|
||||
def test_find_available_slots_success(self, mock_working_hours, mock_fetch, calendar_tools):
|
||||
|
||||
Reference in New Issue
Block a user