hermes-agent/tests/gateway/test_teams_pipeline_runtime_wiring.py
Teknium a99547740d fix(teams-pipeline): drop-scheduler fallback + test wiring for enablement gate
Two salvage follow-ups on top of @dlkakbs's plugin runtime.

1. Install a drop-scheduler when the runtime fails to build.

   Previously when ``build_pipeline_runtime()`` raised (e.g. missing
   Graph env vars, subscription store path unwritable), ``bind_gateway_runtime``
   logged a warning and returned False, leaving the msgraph_webhook
   adapter with no scheduler at all. Incoming Graph notifications
   would then fall back to the adapter's default ``handle_message``
   path, which produces a raw JSON dump as a user-role message — not
   useful and fires every time Graph retries.

   Now a no-op drop-scheduler is installed instead, so:
   - Graph notifications ack cleanly (202) so Graph stops retrying.
   - The failure is surfaced once in the log with the error.
   - No user-role messages get manufactured from raw change payloads.

   The adapter is still bindable later once the runtime becomes
   available (e.g. after the operator runs ``hermes teams-pipeline
   validate`` and fixes the config), since the gateway's
   ``_teams_pipeline_runtime`` sentinel wasn't set to a non-None value.

2. Test wiring for ``_teams_pipeline_plugin_enabled()`` gate.

   The happy-path runner-wiring tests monkeypatched ``bind_gateway_runtime``
   but not ``_load_gateway_config``. In the hermetic test environment
   the real config read ran, saw no enabled plugins, and short-circuited
   the bind call before the test could observe it — so the test
   expected ``calls == [runner]`` but got ``calls == []``.

   Adds a ``_load_gateway_config`` monkeypatch with
   ``plugins.enabled = ["teams_pipeline"]`` to the happy-path tests.
   The explicit-disabled test ``test_gateway_runner_skips_wiring_when_teams_pipeline_plugin_disabled``
   already patches the config correctly.

   Also renames ``test_bind_gateway_runtime_leaves_scheduler_unchanged_on_failure``
   to ``test_bind_gateway_runtime_installs_drop_scheduler_on_failure``
   and updates the assertion — this test contradicted the drop-scheduler
   test in ``tests/plugins/test_teams_pipeline_plugin.py`` which
   expected the scheduler to be installed. The plugin-test name
   (``test_bind_gateway_runtime_drops_notifications_when_unavailable``)
   clearly describes the intended behavior; fixing the wiring-test
   assertion aligns both tests.

Validation:
- ``scripts/run_tests.sh tests/plugins/test_teams_pipeline_plugin.py
  tests/gateway/test_teams_pipeline_runtime_wiring.py
  tests/hermes_cli/test_teams_pipeline_plugin_cli.py`` — 25/25 passed.
2026-05-08 11:18:14 -07:00

197 lines
5.7 KiB
Python

"""Tests for Teams pipeline runtime wiring into the gateway."""
from __future__ import annotations
import sys
from types import ModuleType
from types import SimpleNamespace
from unittest.mock import MagicMock
from gateway.config import Platform, PlatformConfig
from gateway.run import GatewayRunner
from plugins.teams_pipeline.runtime import (
bind_gateway_runtime,
build_pipeline_runtime,
build_pipeline_runtime_config,
)
def test_gateway_runner_wires_teams_pipeline_runtime(monkeypatch):
runner = GatewayRunner.__new__(GatewayRunner)
runner.adapters = {Platform.MSGRAPH_WEBHOOK: object()}
runner._teams_pipeline_runtime_error = None
calls: list[object] = []
def _bind(gateway_runner):
calls.append(gateway_runner)
return True
monkeypatch.setattr("plugins.teams_pipeline.runtime.bind_gateway_runtime", _bind)
monkeypatch.setattr(
"gateway.run._load_gateway_config",
lambda: {"plugins": {"enabled": ["teams_pipeline"]}},
)
GatewayRunner._wire_teams_pipeline_runtime(runner)
assert calls == [runner]
def test_gateway_runner_skips_wiring_without_msgraph_adapter(monkeypatch):
runner = GatewayRunner.__new__(GatewayRunner)
runner.adapters = {Platform.TELEGRAM: MagicMock()}
runner._teams_pipeline_runtime_error = None
called = False
def _bind(_gateway_runner):
nonlocal called
called = True
return True
monkeypatch.setattr("plugins.teams_pipeline.runtime.bind_gateway_runtime", _bind)
monkeypatch.setattr(
"gateway.run._load_gateway_config",
lambda: {"plugins": {"enabled": ["teams_pipeline"]}},
)
GatewayRunner._wire_teams_pipeline_runtime(runner)
assert called is False
def test_gateway_runner_skips_wiring_when_teams_pipeline_plugin_disabled(monkeypatch):
runner = GatewayRunner.__new__(GatewayRunner)
runner.adapters = {Platform.MSGRAPH_WEBHOOK: object()}
runner._teams_pipeline_runtime_error = None
called = False
def _bind(_gateway_runner):
nonlocal called
called = True
return True
monkeypatch.setattr("plugins.teams_pipeline.runtime.bind_gateway_runtime", _bind)
monkeypatch.setattr(
"gateway.run._load_gateway_config",
lambda: {"plugins": {"enabled": []}},
)
GatewayRunner._wire_teams_pipeline_runtime(runner)
assert called is False
def test_runtime_config_disables_teams_delivery_without_target():
gateway_config = SimpleNamespace(
platforms={
Platform("teams"): PlatformConfig(enabled=True, extra={}),
}
)
config = build_pipeline_runtime_config(gateway_config)
assert "teams_delivery" not in config
def test_build_pipeline_runtime_only_wires_sender_when_delivery_configured(monkeypatch):
gateway = SimpleNamespace(
config=SimpleNamespace(
platforms={
Platform("teams"): PlatformConfig(enabled=True, extra={}),
}
)
)
monkeypatch.setattr(
"plugins.teams_pipeline.runtime.build_graph_client",
lambda: object(),
)
monkeypatch.setattr(
"plugins.teams_pipeline.runtime.resolve_teams_pipeline_store_path",
lambda: "/tmp/teams-pipeline-store.json",
)
monkeypatch.setattr(
"plugins.teams_pipeline.runtime.TeamsPipelineStore",
lambda path: {"path": path},
)
runtime = build_pipeline_runtime(gateway)
assert runtime.teams_sender is None
def test_build_pipeline_runtime_skips_sender_when_adapter_layer_is_unavailable(monkeypatch):
gateway = SimpleNamespace(
config=SimpleNamespace(
platforms={
Platform("teams"): PlatformConfig(
enabled=True,
extra={
"delivery_mode": "graph",
"team_id": "team-1",
"channel_id": "channel-1",
},
),
}
)
)
monkeypatch.setattr(
"plugins.teams_pipeline.runtime.build_graph_client",
lambda: object(),
)
monkeypatch.setattr(
"plugins.teams_pipeline.runtime.resolve_teams_pipeline_store_path",
lambda: "/tmp/teams-pipeline-store.json",
)
monkeypatch.setattr(
"plugins.teams_pipeline.runtime.TeamsPipelineStore",
lambda path: {"path": path},
)
monkeypatch.setitem(
sys.modules,
"plugins.platforms.teams.adapter",
ModuleType("plugins.platforms.teams.adapter"),
)
runtime = build_pipeline_runtime(gateway)
assert runtime.teams_sender is None
def test_bind_gateway_runtime_installs_drop_scheduler_on_failure(monkeypatch):
"""When the runtime can't build, install a drop-scheduler so Graph
notifications still ack cleanly rather than leaving the adapter's
scheduler unbound.
"""
class FakeAdapter:
def __init__(self):
self.scheduler = None
def set_notification_scheduler(self, scheduler):
self.scheduler = scheduler
gateway = SimpleNamespace(
adapters={Platform.MSGRAPH_WEBHOOK: FakeAdapter()},
config=SimpleNamespace(
platforms={
Platform("teams"): PlatformConfig(enabled=True, extra={}),
}
),
_teams_pipeline_runtime=None,
_teams_pipeline_runtime_error=None,
)
monkeypatch.setattr(
"plugins.teams_pipeline.runtime.build_pipeline_runtime",
lambda _gateway: (_ for _ in ()).throw(RuntimeError("boom")),
)
bound = bind_gateway_runtime(gateway)
assert bound is False
assert callable(gateway.adapters[Platform.MSGRAPH_WEBHOOK].scheduler)
assert gateway._teams_pipeline_runtime_error == "boom"