Download tests/plugins/test_google_meet_plugin.py from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/tests/plugins/test_google_meet_plugin.py
- Command line
-
hf download hf://SaylorTwift/hermes-agent/tests/plugins/test_google_meet_plugin.py
-
curl -L -o test_google_meet_plugin.py https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/tests/plugins/test_google_meet_plugin.py
10.9 kB
| """Tests for the google_meet plugin. | |
| Covers the safety-gated pieces that don't require Playwright: | |
| * URL regex — only ``https://meet.google.com/`` URLs pass | |
| * Meeting-id extraction from Meet URLs | |
| * Status / transcript writes round-trip through the file-backed state | |
| * Tool handlers return well-formed JSON under all branches | |
| * Process manager refuses unsafe URLs and clears stale state cleanly | |
| * ``_on_session_end`` hook is defensive (no-ops when no bot active) | |
| Does NOT spawn a real Chromium — we mock ``subprocess.Popen`` where needed. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import signal | |
| from pathlib import Path | |
| from unittest.mock import patch | |
| import pytest | |
| def _isolate_home(tmp_path, monkeypatch): | |
| hermes_home = tmp_path / ".hermes" | |
| hermes_home.mkdir() | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| yield hermes_home | |
| # --------------------------------------------------------------------------- | |
| # URL safety gate | |
| # --------------------------------------------------------------------------- | |
| def test_is_safe_meet_url_accepts_standard_meet_codes(): | |
| from plugins.google_meet.meet_bot import _is_safe_meet_url | |
| assert _is_safe_meet_url("https://meet.google.com/abc-defg-hij") | |
| assert _is_safe_meet_url("https://meet.google.com/abc-defg-hij?pli=1") | |
| assert _is_safe_meet_url("https://meet.google.com/new") | |
| assert _is_safe_meet_url("https://meet.google.com/lookup/ABC123") | |
| def test_meeting_id_extraction(): | |
| from plugins.google_meet.meet_bot import _meeting_id_from_url | |
| assert _meeting_id_from_url("https://meet.google.com/abc-defg-hij") == "abc-defg-hij" | |
| assert _meeting_id_from_url("https://meet.google.com/abc-defg-hij?pli=1") == "abc-defg-hij" | |
| # fallback for codes we can't parse (e.g. /new before redirect) | |
| fallback = _meeting_id_from_url("https://meet.google.com/new") | |
| assert fallback.startswith("meet-") | |
| # --------------------------------------------------------------------------- | |
| # _BotState — transcript + status file round-trip | |
| # --------------------------------------------------------------------------- | |
| def test_bot_state_dedupes_captions_and_flushes_status(tmp_path): | |
| from plugins.google_meet.meet_bot import _BotState | |
| out = tmp_path / "session" | |
| state = _BotState(out_dir=out, meeting_id="abc-defg-hij", | |
| url="https://meet.google.com/abc-defg-hij") | |
| state.record_caption("Alice", "Hey everyone") | |
| state.record_caption("Alice", "Hey everyone") # dup — ignored | |
| state.record_caption("Bob", "Let's start") | |
| transcript = (out / "transcript.txt").read_text() | |
| assert "Alice: Hey everyone" in transcript | |
| assert "Bob: Let's start" in transcript | |
| # dedup — Alice line appears exactly once | |
| assert transcript.count("Alice: Hey everyone") == 1 | |
| status = json.loads((out / "status.json").read_text()) | |
| assert status["meetingId"] == "abc-defg-hij" | |
| assert status["transcriptLines"] == 2 | |
| assert status["transcriptPath"].endswith("transcript.txt") | |
| def test_parse_duration(): | |
| from plugins.google_meet.meet_bot import _parse_duration | |
| assert _parse_duration("30m") == 30 * 60 | |
| assert _parse_duration("2h") == 2 * 3600 | |
| assert _parse_duration("90s") == 90 | |
| assert _parse_duration("90") == 90 | |
| assert _parse_duration("") is None | |
| assert _parse_duration("bogus") is None | |
| # --------------------------------------------------------------------------- | |
| # process_manager — refuses unsafe URLs, manages active pointer | |
| # --------------------------------------------------------------------------- | |
| def test_start_refuses_unsafe_url(): | |
| from plugins.google_meet import process_manager as pm | |
| res = pm.start("https://evil.example.com/abc-defg-hij") | |
| assert res["ok"] is False | |
| assert "refusing" in res["error"] | |
| def test_status_reports_no_active_meeting(): | |
| from plugins.google_meet import process_manager as pm | |
| assert pm.status() == {"ok": False, "reason": "no active meeting"} | |
| assert pm.transcript() == {"ok": False, "reason": "no active meeting"} | |
| assert pm.stop() == {"ok": False, "reason": "no active meeting"} | |
| def test_transcript_reads_last_n_lines(tmp_path): | |
| from plugins.google_meet import process_manager as pm | |
| meeting_dir = Path(os.environ["HERMES_HOME"]) / "workspace" / "meetings" / "abc-defg-hij" | |
| meeting_dir.mkdir(parents=True) | |
| (meeting_dir / "transcript.txt").write_text( | |
| "[10:00:00] Alice: one\n" | |
| "[10:00:01] Bob: two\n" | |
| "[10:00:02] Alice: three\n" | |
| ) | |
| pm._write_active({ | |
| "pid": 0, "meeting_id": "abc-defg-hij", | |
| "out_dir": str(meeting_dir), | |
| "url": "https://meet.google.com/abc-defg-hij", | |
| "started_at": 0, | |
| }) | |
| res = pm.transcript(last=2) | |
| assert res["ok"] is True | |
| assert res["total"] == 3 | |
| assert len(res["lines"]) == 2 | |
| assert res["lines"][-1].endswith("Alice: three") | |
| def test_stop_signals_process_and_clears_pointer(tmp_path): | |
| from plugins.google_meet import process_manager as pm | |
| pm._write_active({ | |
| "pid": 11111, "meeting_id": "x-y-z", | |
| "out_dir": str(tmp_path / "x-y-z"), | |
| "url": "https://meet.google.com/x-y-z", | |
| "started_at": 0, | |
| }) | |
| alive_seq = iter([True, True, False]) # alive at first, gone after SIGTERM | |
| def _alive(pid): | |
| try: | |
| return next(alive_seq) | |
| except StopIteration: | |
| return False | |
| sent = [] | |
| def _kill(pid, sig): | |
| sent.append((pid, sig)) | |
| with patch.object(pm, "_pid_alive", side_effect=_alive), \ | |
| patch.object(pm.os, "kill", side_effect=_kill), \ | |
| patch.object(pm.time, "sleep", lambda _s: None): | |
| res = pm.stop() | |
| assert res["ok"] is True | |
| assert (11111, signal.SIGTERM) in sent | |
| # .active.json cleared | |
| assert pm._read_active() is None | |
| # --------------------------------------------------------------------------- | |
| # Tool handlers — JSON shape + safety gates | |
| # --------------------------------------------------------------------------- | |
| def test_meet_join_handler_missing_url_returns_error(): | |
| from plugins.google_meet.tools import handle_meet_join | |
| out = json.loads(handle_meet_join({})) | |
| assert out["success"] is False | |
| assert "url is required" in out["error"] | |
| # --------------------------------------------------------------------------- | |
| # _on_session_end — defensive cleanup | |
| # --------------------------------------------------------------------------- | |
| def test_on_session_end_noop_when_nothing_active(): | |
| from plugins.google_meet import _on_session_end | |
| # Should not raise and should not call stop(). | |
| with patch("plugins.google_meet.pm.stop") as stop_mock: | |
| _on_session_end() | |
| stop_mock.assert_not_called() | |
| # --------------------------------------------------------------------------- | |
| # Plugin register() — platform gating + tool registration | |
| # --------------------------------------------------------------------------- | |
| def test_register_refuses_on_windows(): | |
| import plugins.google_meet as plugin | |
| calls = {"tools": [], "cli": [], "hooks": []} | |
| class _Ctx: | |
| def register_tool(self, **kw): calls["tools"].append(kw["name"]) | |
| def register_cli_command(self, **kw): calls["cli"].append(kw["name"]) | |
| def register_hook(self, name, fn): calls["hooks"].append(name) | |
| with patch.object(plugin.platform, "system", return_value="Windows"): | |
| plugin.register(_Ctx()) | |
| assert calls == {"tools": [], "cli": [], "hooks": []} | |
| # --------------------------------------------------------------------------- | |
| # v2: process_manager.enqueue_say + realtime-mode passthrough | |
| # --------------------------------------------------------------------------- | |
| def test_enqueue_say_requires_text(): | |
| from plugins.google_meet import process_manager as pm | |
| assert pm.enqueue_say("")["ok"] is False | |
| assert pm.enqueue_say(" ")["ok"] is False | |
| # --------------------------------------------------------------------------- | |
| # v3: NodeClient routing from tool handlers | |
| # --------------------------------------------------------------------------- | |
| def test_cli_register_includes_node_subcommand(): | |
| """`hermes meet` argparse tree includes the node subtree.""" | |
| import argparse | |
| from plugins.google_meet.cli import register_cli | |
| parser = argparse.ArgumentParser(prog="hermes meet") | |
| register_cli(parser) | |
| # Parse a known-good node invocation to prove the subtree is wired. | |
| ns = parser.parse_args(["node", "list"]) | |
| assert ns.meet_command == "node" | |
| assert ns.node_cmd == "list" | |
| # --------------------------------------------------------------------------- | |
| # v2.1: new _BotState fields + status dict shape | |
| # --------------------------------------------------------------------------- | |
| # --------------------------------------------------------------------------- | |
| # Admission detection + barge-in helper | |
| # --------------------------------------------------------------------------- | |
| def test_looks_like_human_speaker(): | |
| from plugins.google_meet.meet_bot import _looks_like_human_speaker | |
| # Blank, "unknown", "you", and the bot's own name → not human (no barge-in) | |
| for s in ("", " ", "Unknown", "unknown", "You", "you", "Hermes Agent", "hermes agent"): | |
| assert not _looks_like_human_speaker(s, "Hermes Agent"), f"{s!r} should NOT be human" | |
| # Real names → human (barge-in) | |
| for s in ("Alice", "Bob Lee", "@teknium"): | |
| assert _looks_like_human_speaker(s, "Hermes Agent"), f"{s!r} SHOULD be human" | |
| def test_detect_admission_returns_false_on_error(): | |
| from plugins.google_meet.meet_bot import _ADMISSION_PROBE_JS, _probe | |
| class _FakePage: | |
| def evaluate(self, _js): raise RuntimeError("boom") | |
| assert _probe(_FakePage(), _ADMISSION_PROBE_JS) is False | |
| # --------------------------------------------------------------------------- | |
| # Realtime session counters + cancel_response (barge-in) | |
| # --------------------------------------------------------------------------- | |
| def test_realtime_session_cancel_response_when_disconnected(): | |
| from plugins.google_meet.realtime.openai_client import RealtimeSession | |
| sess = RealtimeSession(api_key="sk-test", audio_sink_path=None) | |
| # No _ws yet — cancel should no-op and return False. | |
| assert sess.cancel_response() is False | |
| # --------------------------------------------------------------------------- | |
| # hermes meet install CLI | |
| # --------------------------------------------------------------------------- | |
| def test_cmd_install_refuses_windows(capsys): | |
| from plugins.google_meet.cli import _cmd_install | |
| with patch("plugins.google_meet.cli.platform" if False else "platform.system", | |
| return_value="Windows"): | |
| rc = _cmd_install(realtime=False, assume_yes=True) | |
| assert rc == 1 | |
| out = capsys.readouterr().out | |
| assert "Windows" in out | |