From a99547740dab830c8b121574e4ef50db8fc500f8 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 8 May 2026 10:49:23 -0700 Subject: [PATCH] fix(teams-pipeline): drop-scheduler fallback + test wiring for enablement gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- plugins/teams_pipeline/runtime.py | 12 +++++++++++- .../test_teams_pipeline_runtime_wiring.py | 16 ++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/plugins/teams_pipeline/runtime.py b/plugins/teams_pipeline/runtime.py index 0163ccaaae..e8d3ada710 100644 --- a/plugins/teams_pipeline/runtime.py +++ b/plugins/teams_pipeline/runtime.py @@ -111,9 +111,19 @@ def bind_gateway_runtime(gateway: Any) -> bool: error_message = str(exc) gateway._teams_pipeline_runtime_error = error_message logger.warning( - "Teams pipeline runtime unavailable; leaving webhook scheduler unchanged: %s", + "Teams pipeline runtime unavailable: %s. Installing a drop-scheduler " + "so Graph notifications ack cleanly without piling up unbound.", error_message, ) + + async def _drop(notification: dict[str, Any], event: Any) -> None: + logger.debug( + "Dropping Graph notification because runtime is unavailable: id=%s resource=%s", + notification.get("id"), + notification.get("resource"), + ) + + adapter.set_notification_scheduler(_drop) return False async def _schedule(notification: dict[str, Any], event: Any) -> None: diff --git a/tests/gateway/test_teams_pipeline_runtime_wiring.py b/tests/gateway/test_teams_pipeline_runtime_wiring.py index d1f95b51ba..5a62033d00 100644 --- a/tests/gateway/test_teams_pipeline_runtime_wiring.py +++ b/tests/gateway/test_teams_pipeline_runtime_wiring.py @@ -28,6 +28,10 @@ def test_gateway_runner_wires_teams_pipeline_runtime(monkeypatch): 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) @@ -47,6 +51,10 @@ def test_gateway_runner_skips_wiring_without_msgraph_adapter(monkeypatch): 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) @@ -154,7 +162,11 @@ def test_build_pipeline_runtime_skips_sender_when_adapter_layer_is_unavailable(m assert runtime.teams_sender is None -def test_bind_gateway_runtime_leaves_scheduler_unchanged_on_failure(monkeypatch): +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 @@ -181,5 +193,5 @@ def test_bind_gateway_runtime_leaves_scheduler_unchanged_on_failure(monkeypatch) bound = bind_gateway_runtime(gateway) assert bound is False - assert gateway.adapters[Platform.MSGRAPH_WEBHOOK].scheduler is None + assert callable(gateway.adapters[Platform.MSGRAPH_WEBHOOK].scheduler) assert gateway._teams_pipeline_runtime_error == "boom"