Files
hermes-agent/tests/skills/test_google_workspace_api.py

187 lines
6.1 KiB
Python
Raw Normal View History

"""Tests for Google Workspace gws bridge and CLI wrapper."""
import importlib.util
import json
import os
import subprocess
import sys
import types
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
BRIDGE_PATH = (
Path(__file__).resolve().parents[2]
/ "skills/productivity/google-workspace/scripts/gws_bridge.py"
)
API_PATH = (
Path(__file__).resolve().parents[2]
/ "skills/productivity/google-workspace/scripts/google_api.py"
)
@pytest.fixture
def bridge_module(monkeypatch, tmp_path):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
spec = importlib.util.spec_from_file_location("gws_bridge_test", BRIDGE_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
@pytest.fixture
def api_module(monkeypatch, tmp_path):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
spec = importlib.util.spec_from_file_location("gws_api_test", API_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
fix(tests): resolve 53 CI test failures across 8 root causes 1. Telegram xdist mock pollution (37 tests): Add tests/gateway/conftest.py with a shared _ensure_telegram_mock() that runs at collection time. Under pytest-xdist, test_telegram_caption_merge.py (bare top-level import, no mock) would trigger the ImportError fallback in gateway/platforms/telegram.py, caching ChatType=None and Update=Any for the entire worker — cascading into 37 downstream failures. 2. VIRTUAL_ENV env var leak (4 tests): TestDetectVenvDir tests monkeypatched sys.prefix but didn't clear VIRTUAL_ENV. After commit 50c35dca added a VIRTUAL_ENV check to _detect_venv_dir(), CI's real venv leaked through. 3. Copilot base_url missing (1 test): _resolve_runtime_from_pool_entry() set api_mode for copilot but didn't add the base_url fallback — unlike openrouter, anthropic, and codex which all have one. Production bug. 4. Stale vision model assertion (1 test): _PROVIDER_VISION_MODELS added zai -> glm-5v-turbo but the test still expected the main model glm-5.1. 5. Reasoning item id intentionally stripped (1 test): Production code at run_agent.py:3738 deliberately excludes 'id' from reasoning items (store=False causes API 404). Test was asserting the old behavior. 6. context_length warning not reaching custom_providers (1 test): The test didn't pass base_url to AIAgent, so self.base_url was empty and the custom_providers URL comparison at line 1302 never matched. 7. Matrix room ID URL-encoding (1 test): Production code now URL-encodes room IDs (!room:example.com -> %21room%3Aexample.com) but the test assertion wasn't updated. 8. Google Workspace calendar tests (2 tests): Tests assert on +agenda CLI args that don't exist in the production calendar_list() function. They only 'passed' before because _gws_binary() returned None, the Python SDK fallback ran, googleapiclient import failed, SystemExit was raised, and post-exit assertions were never reached. Skip when gws not installed. Remaining 4 failures (test_run_progress_topics.py) are pre-existing flaky tests that fail inconsistently under xdist — confirmed on clean main.
2026-04-16 07:24:16 +05:30
# Ensure the gws CLI code path is taken even when the binary isn't
# installed (CI). Without this, calendar_list() falls through to the
# Python SDK path which imports ``googleapiclient`` — not in deps.
module._gws_binary = lambda: "/usr/bin/gws"
# Bypass authentication check — no real token file in CI.
module._ensure_authenticated = lambda: None
return module
fix(tests): resolve 53 CI test failures across 8 root causes 1. Telegram xdist mock pollution (37 tests): Add tests/gateway/conftest.py with a shared _ensure_telegram_mock() that runs at collection time. Under pytest-xdist, test_telegram_caption_merge.py (bare top-level import, no mock) would trigger the ImportError fallback in gateway/platforms/telegram.py, caching ChatType=None and Update=Any for the entire worker — cascading into 37 downstream failures. 2. VIRTUAL_ENV env var leak (4 tests): TestDetectVenvDir tests monkeypatched sys.prefix but didn't clear VIRTUAL_ENV. After commit 50c35dca added a VIRTUAL_ENV check to _detect_venv_dir(), CI's real venv leaked through. 3. Copilot base_url missing (1 test): _resolve_runtime_from_pool_entry() set api_mode for copilot but didn't add the base_url fallback — unlike openrouter, anthropic, and codex which all have one. Production bug. 4. Stale vision model assertion (1 test): _PROVIDER_VISION_MODELS added zai -> glm-5v-turbo but the test still expected the main model glm-5.1. 5. Reasoning item id intentionally stripped (1 test): Production code at run_agent.py:3738 deliberately excludes 'id' from reasoning items (store=False causes API 404). Test was asserting the old behavior. 6. context_length warning not reaching custom_providers (1 test): The test didn't pass base_url to AIAgent, so self.base_url was empty and the custom_providers URL comparison at line 1302 never matched. 7. Matrix room ID URL-encoding (1 test): Production code now URL-encodes room IDs (!room:example.com -> %21room%3Aexample.com) but the test assertion wasn't updated. 8. Google Workspace calendar tests (2 tests): Tests assert on +agenda CLI args that don't exist in the production calendar_list() function. They only 'passed' before because _gws_binary() returned None, the Python SDK fallback ran, googleapiclient import failed, SystemExit was raised, and post-exit assertions were never reached. Skip when gws not installed. Remaining 4 failures (test_run_progress_topics.py) are pre-existing flaky tests that fail inconsistently under xdist — confirmed on clean main.
2026-04-16 07:24:16 +05:30
_gws_installed = importlib.util.find_spec("shutil") and __import__("shutil").which("gws")
def _write_token(path: Path, *, token="ya29.test", expiry=None, **extra):
data = {
"token": token,
"refresh_token": "1//refresh",
"client_id": "123.apps.googleusercontent.com",
"client_secret": "secret",
"token_uri": "https://oauth2.googleapis.com/token",
**extra,
}
if expiry is not None:
data["expiry"] = expiry
path.write_text(json.dumps(data))
def test_bridge_returns_valid_token(bridge_module, tmp_path):
"""Non-expired token is returned without refresh."""
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
token_path = bridge_module.get_token_path()
_write_token(token_path, token="ya29.valid", expiry=future)
result = bridge_module.get_valid_token()
assert result == "ya29.valid"
def test_bridge_refreshes_expired_token(bridge_module, tmp_path):
"""Expired token triggers a refresh via token_uri."""
past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
token_path = bridge_module.get_token_path()
_write_token(token_path, token="ya29.old", expiry=past)
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps({
"access_token": "ya29.refreshed",
"expires_in": 3600,
}).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
with patch("urllib.request.urlopen", return_value=mock_resp):
result = bridge_module.get_valid_token()
assert result == "ya29.refreshed"
# Verify persisted
saved = json.loads(token_path.read_text())
assert saved["token"] == "ya29.refreshed"
def test_bridge_exits_on_missing_token(bridge_module):
"""Missing token file causes exit with code 1."""
with pytest.raises(SystemExit):
bridge_module.get_valid_token()
def test_bridge_main_injects_token_env(bridge_module, tmp_path):
"""main() sets GOOGLE_WORKSPACE_CLI_TOKEN in subprocess env."""
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
token_path = bridge_module.get_token_path()
_write_token(token_path, token="ya29.injected", expiry=future)
captured = {}
def capture_run(cmd, **kwargs):
captured["cmd"] = cmd
captured["env"] = kwargs.get("env", {})
return MagicMock(returncode=0)
with patch.object(sys, "argv", ["gws_bridge.py", "gmail", "+triage"]):
with patch.object(subprocess, "run", side_effect=capture_run):
with pytest.raises(SystemExit):
bridge_module.main()
assert captured["env"]["GOOGLE_WORKSPACE_CLI_TOKEN"] == "ya29.injected"
assert captured["cmd"] == ["gws", "gmail", "+triage"]
fix(tests): resolve 53 CI test failures across 8 root causes 1. Telegram xdist mock pollution (37 tests): Add tests/gateway/conftest.py with a shared _ensure_telegram_mock() that runs at collection time. Under pytest-xdist, test_telegram_caption_merge.py (bare top-level import, no mock) would trigger the ImportError fallback in gateway/platforms/telegram.py, caching ChatType=None and Update=Any for the entire worker — cascading into 37 downstream failures. 2. VIRTUAL_ENV env var leak (4 tests): TestDetectVenvDir tests monkeypatched sys.prefix but didn't clear VIRTUAL_ENV. After commit 50c35dca added a VIRTUAL_ENV check to _detect_venv_dir(), CI's real venv leaked through. 3. Copilot base_url missing (1 test): _resolve_runtime_from_pool_entry() set api_mode for copilot but didn't add the base_url fallback — unlike openrouter, anthropic, and codex which all have one. Production bug. 4. Stale vision model assertion (1 test): _PROVIDER_VISION_MODELS added zai -> glm-5v-turbo but the test still expected the main model glm-5.1. 5. Reasoning item id intentionally stripped (1 test): Production code at run_agent.py:3738 deliberately excludes 'id' from reasoning items (store=False causes API 404). Test was asserting the old behavior. 6. context_length warning not reaching custom_providers (1 test): The test didn't pass base_url to AIAgent, so self.base_url was empty and the custom_providers URL comparison at line 1302 never matched. 7. Matrix room ID URL-encoding (1 test): Production code now URL-encodes room IDs (!room:example.com -> %21room%3Aexample.com) but the test assertion wasn't updated. 8. Google Workspace calendar tests (2 tests): Tests assert on +agenda CLI args that don't exist in the production calendar_list() function. They only 'passed' before because _gws_binary() returned None, the Python SDK fallback ran, googleapiclient import failed, SystemExit was raised, and post-exit assertions were never reached. Skip when gws not installed. Remaining 4 failures (test_run_progress_topics.py) are pre-existing flaky tests that fail inconsistently under xdist — confirmed on clean main.
2026-04-16 07:24:16 +05:30
@pytest.mark.skipif(not _gws_installed, reason="gws CLI not installed")
def test_api_calendar_list_uses_agenda_by_default(api_module):
"""calendar list without dates uses +agenda helper."""
captured = {}
def capture_run(cmd, **kwargs):
captured["cmd"] = cmd
fix(tests): resolve 53 CI test failures across 8 root causes 1. Telegram xdist mock pollution (37 tests): Add tests/gateway/conftest.py with a shared _ensure_telegram_mock() that runs at collection time. Under pytest-xdist, test_telegram_caption_merge.py (bare top-level import, no mock) would trigger the ImportError fallback in gateway/platforms/telegram.py, caching ChatType=None and Update=Any for the entire worker — cascading into 37 downstream failures. 2. VIRTUAL_ENV env var leak (4 tests): TestDetectVenvDir tests monkeypatched sys.prefix but didn't clear VIRTUAL_ENV. After commit 50c35dca added a VIRTUAL_ENV check to _detect_venv_dir(), CI's real venv leaked through. 3. Copilot base_url missing (1 test): _resolve_runtime_from_pool_entry() set api_mode for copilot but didn't add the base_url fallback — unlike openrouter, anthropic, and codex which all have one. Production bug. 4. Stale vision model assertion (1 test): _PROVIDER_VISION_MODELS added zai -> glm-5v-turbo but the test still expected the main model glm-5.1. 5. Reasoning item id intentionally stripped (1 test): Production code at run_agent.py:3738 deliberately excludes 'id' from reasoning items (store=False causes API 404). Test was asserting the old behavior. 6. context_length warning not reaching custom_providers (1 test): The test didn't pass base_url to AIAgent, so self.base_url was empty and the custom_providers URL comparison at line 1302 never matched. 7. Matrix room ID URL-encoding (1 test): Production code now URL-encodes room IDs (!room:example.com -> %21room%3Aexample.com) but the test assertion wasn't updated. 8. Google Workspace calendar tests (2 tests): Tests assert on +agenda CLI args that don't exist in the production calendar_list() function. They only 'passed' before because _gws_binary() returned None, the Python SDK fallback ran, googleapiclient import failed, SystemExit was raised, and post-exit assertions were never reached. Skip when gws not installed. Remaining 4 failures (test_run_progress_topics.py) are pre-existing flaky tests that fail inconsistently under xdist — confirmed on clean main.
2026-04-16 07:24:16 +05:30
return MagicMock(returncode=0, stdout="{}", stderr="")
args = api_module.argparse.Namespace(
start="", end="", max=25, calendar="primary", func=api_module.calendar_list,
)
with patch.object(subprocess, "run", side_effect=capture_run):
with pytest.raises(SystemExit):
api_module.calendar_list(args)
gws_args = captured["cmd"][2:] # skip python + bridge path
assert "calendar" in gws_args
assert "+agenda" in gws_args
assert "--days" in gws_args
fix(tests): resolve 53 CI test failures across 8 root causes 1. Telegram xdist mock pollution (37 tests): Add tests/gateway/conftest.py with a shared _ensure_telegram_mock() that runs at collection time. Under pytest-xdist, test_telegram_caption_merge.py (bare top-level import, no mock) would trigger the ImportError fallback in gateway/platforms/telegram.py, caching ChatType=None and Update=Any for the entire worker — cascading into 37 downstream failures. 2. VIRTUAL_ENV env var leak (4 tests): TestDetectVenvDir tests monkeypatched sys.prefix but didn't clear VIRTUAL_ENV. After commit 50c35dca added a VIRTUAL_ENV check to _detect_venv_dir(), CI's real venv leaked through. 3. Copilot base_url missing (1 test): _resolve_runtime_from_pool_entry() set api_mode for copilot but didn't add the base_url fallback — unlike openrouter, anthropic, and codex which all have one. Production bug. 4. Stale vision model assertion (1 test): _PROVIDER_VISION_MODELS added zai -> glm-5v-turbo but the test still expected the main model glm-5.1. 5. Reasoning item id intentionally stripped (1 test): Production code at run_agent.py:3738 deliberately excludes 'id' from reasoning items (store=False causes API 404). Test was asserting the old behavior. 6. context_length warning not reaching custom_providers (1 test): The test didn't pass base_url to AIAgent, so self.base_url was empty and the custom_providers URL comparison at line 1302 never matched. 7. Matrix room ID URL-encoding (1 test): Production code now URL-encodes room IDs (!room:example.com -> %21room%3Aexample.com) but the test assertion wasn't updated. 8. Google Workspace calendar tests (2 tests): Tests assert on +agenda CLI args that don't exist in the production calendar_list() function. They only 'passed' before because _gws_binary() returned None, the Python SDK fallback ran, googleapiclient import failed, SystemExit was raised, and post-exit assertions were never reached. Skip when gws not installed. Remaining 4 failures (test_run_progress_topics.py) are pre-existing flaky tests that fail inconsistently under xdist — confirmed on clean main.
2026-04-16 07:24:16 +05:30
@pytest.mark.skipif(not _gws_installed, reason="gws CLI not installed")
def test_api_calendar_list_respects_date_range(api_module):
"""calendar list with --start/--end uses raw events list API."""
captured = {}
def capture_run(cmd, **kwargs):
captured["cmd"] = cmd
return MagicMock(returncode=0)
args = api_module.argparse.Namespace(
start="2026-04-01T00:00:00Z",
end="2026-04-07T23:59:59Z",
max=25,
calendar="primary",
func=api_module.calendar_list,
)
with patch.object(subprocess, "run", side_effect=capture_run):
with pytest.raises(SystemExit):
api_module.calendar_list(args)
gws_args = captured["cmd"][2:]
assert "events" in gws_args
assert "list" in gws_args
params_idx = gws_args.index("--params")
params = json.loads(gws_args[params_idx + 1])
assert params["timeMin"] == "2026-04-01T00:00:00Z"
assert params["timeMax"] == "2026-04-07T23:59:59Z"