"""Tests for publish_findings node — _build_mr_note and GitLab MR write-back.""" from __future__ import annotations from typing import Any from unittest.mock import MagicMock, patch import pytest from tools.investigation.reporting.gitlab_writeback import _build_mr_note # --------------------------------------------------------------------------- # _build_mr_note # --------------------------------------------------------------------------- def test_build_mr_note_wraps_in_details_block() -> None: result = _build_mr_note("root cause is X") assert "
" in result assert "Investigation summary" in result assert "root cause is X" in result assert "### RCA Finding" in result def test_build_mr_note_truncates_long_messages() -> None: long_message = "x" * 5000 result = _build_mr_note(long_message) assert len(result) < 5000 + 200 # body capped + wrapper overhead assert result.endswith("
") assert "..." in result def test_build_mr_note_does_not_truncate_short_messages() -> None: message = "short message" result = _build_mr_note(message) assert "..." not in result assert message in result # --------------------------------------------------------------------------- # GitLab MR write-back in generate_report # --------------------------------------------------------------------------- def _make_state(**overrides: Any) -> dict[str, Any]: base: dict[str, Any] = { "problem_md": "something broke", "root_cause_category": "infra", "slack_context": {}, "organization_slug": None, "available_sources": { "gitlab": { "project_id": "my-org/my-repo", "merge_request_iid": "5", "gitlab_url": "https://gitlab.example.com/api/v4", "gitlab_token": "gl-token", } }, "resolved_integrations": {}, } base.update(overrides) return base def _patch_generate_report_deps(monkeypatch: pytest.MonkeyPatch) -> None: """Patch all heavy dependencies of generate_report so we can run it in tests.""" from tools.investigation.reporting.formatters.messages import ReportMessages monkeypatch.setattr( "tools.investigation.reporting.node.build_report_context", lambda _state: {}, ) monkeypatch.setattr( "tools.investigation.reporting.node.build_report_messages", lambda _ctx: ReportMessages( slack_text="slack report text", telegram_html="telegram report text", whatsapp_text="whatsapp report text", slack_blocks=[], ), ) monkeypatch.setattr( "tools.investigation.reporting.node.create_investigation_and_attach_url", lambda _state, _msg, _summary: ( "inv-id-123", "https://app.example.com/inv/1", ), ) monkeypatch.setattr( "tools.investigation.reporting.node.render_report", lambda _msg, **_kw: None, ) monkeypatch.setattr( "tools.investigation.reporting.node.open_in_editor", lambda _msg: None, ) def test_gitlab_writeback_calls_post_when_enabled(monkeypatch: pytest.MonkeyPatch) -> None: _patch_generate_report_deps(monkeypatch) monkeypatch.setenv("GITLAB_MR_WRITEBACK", "true") mock_send_slack = MagicMock(return_value=(False, None)) mock_post_note = MagicMock(return_value={"id": 1}) mock_build_action_blocks = MagicMock(return_value=[]) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch( "tools.investigation.reporting.gitlab_writeback.post_gitlab_mr_note", mock_post_note, ), patch( "tools.investigation.reporting.gitlab_writeback.build_gitlab_config", return_value=MagicMock(), ), ): from tools.investigation.reporting.node import generate_report generate_report(_make_state()) # type: ignore[arg-type] mock_post_note.assert_called_once() _, kwargs = mock_post_note.call_args assert kwargs["project_id"] == "my-org/my-repo" assert kwargs["mr_iid"] == "5" def test_gitlab_writeback_skipped_when_env_var_not_set(monkeypatch: pytest.MonkeyPatch) -> None: _patch_generate_report_deps(monkeypatch) monkeypatch.delenv("GITLAB_MR_WRITEBACK", raising=False) mock_post_note = MagicMock() mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch( "tools.investigation.reporting.gitlab_writeback.post_gitlab_mr_note", mock_post_note, ), ): from tools.investigation.reporting.node import generate_report generate_report(_make_state()) # type: ignore[arg-type] mock_post_note.assert_not_called() def test_gitlab_writeback_skipped_when_mr_iid_missing(monkeypatch: pytest.MonkeyPatch) -> None: _patch_generate_report_deps(monkeypatch) monkeypatch.setenv("GITLAB_MR_WRITEBACK", "true") state = _make_state( available_sources={"gitlab": {"project_id": "my-org/my-repo", "merge_request_iid": ""}} ) mock_post_note = MagicMock() mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch( "tools.investigation.reporting.gitlab_writeback.post_gitlab_mr_note", mock_post_note, ), ): from tools.investigation.reporting.node import generate_report generate_report(state) # type: ignore[arg-type] mock_post_note.assert_not_called() def test_gitlab_writeback_failure_does_not_raise(monkeypatch: pytest.MonkeyPatch) -> None: _patch_generate_report_deps(monkeypatch) monkeypatch.setenv("GITLAB_MR_WRITEBACK", "true") mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch( "tools.investigation.reporting.gitlab_writeback.post_gitlab_mr_note", side_effect=RuntimeError("network error"), ), patch( "tools.investigation.reporting.gitlab_writeback.build_gitlab_config", return_value=MagicMock(), ), ): from tools.investigation.reporting.node import generate_report result = generate_report(_make_state()) # type: ignore[arg-type] assert "slack_message" in result # report returned despite write-back failure def test_generate_report_can_skip_terminal_render_and_editor( monkeypatch: pytest.MonkeyPatch, ) -> None: _patch_generate_report_deps(monkeypatch) mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) mock_render_report = MagicMock() mock_open_in_editor = MagicMock() with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch("tools.investigation.reporting.node.render_report", mock_render_report), patch("tools.investigation.reporting.node.open_in_editor", mock_open_in_editor), ): from tools.investigation.reporting.node import generate_report generate_report( _make_state(), # type: ignore[arg-type] render_terminal=False, open_editor=False, ) mock_render_report.assert_not_called() mock_open_in_editor.assert_not_called() def test_openclaw_writeback_calls_delivery_when_configured( monkeypatch: pytest.MonkeyPatch, ) -> None: _patch_generate_report_deps(monkeypatch) mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) mock_openclaw_delivery = MagicMock(return_value=(True, None)) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch("integrations.openclaw.delivery.send_openclaw_report", mock_openclaw_delivery), ): from tools.investigation.reporting.node import generate_report generate_report( _make_state( resolved_integrations={ "openclaw": { "mode": "streamable-http", "url": "https://openclaw.example.com/mcp", "auth_token": "tok", } } ) ) # type: ignore[arg-type] mock_openclaw_delivery.assert_called_once() def test_whatsapp_delivery_uses_twilio_credentials(monkeypatch: pytest.MonkeyPatch) -> None: _patch_generate_report_deps(monkeypatch) mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) mock_whatsapp_delivery = MagicMock(return_value=(True, "")) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch("integrations.whatsapp.delivery.send_whatsapp_report", mock_whatsapp_delivery), ): from tools.investigation.reporting.node import generate_report generate_report( _make_state( resolved_integrations={ "whatsapp": { "account_sid": "AC123", "auth_token": "tok", "from_number": "whatsapp:+14155238886", "default_to": "+1234567890", } } ) ) # type: ignore[arg-type] mock_whatsapp_delivery.assert_called_once_with( "whatsapp report text", { "account_sid": "AC123", "auth_token": "tok", "from_number": "whatsapp:+14155238886", "to": "+1234567890", }, ) def test_twilio_sms_dispatched_when_enabled(monkeypatch: pytest.MonkeyPatch) -> None: _patch_generate_report_deps(monkeypatch) mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) mock_sms = MagicMock(return_value=(True, "", "SM-XYZ")) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch("integrations.twilio.delivery.send_twilio_sms_report", mock_sms), ): from tools.investigation.reporting.node import generate_report generate_report( _make_state( resolved_integrations={ "twilio": { "account_sid": "AC1", "auth_token": "tok", "sms": { "enabled": True, "from_number": "+14155551111", "default_to": "+14155550000", "messaging_service_sid": "", }, } } ) ) # type: ignore[arg-type] mock_sms.assert_called_once_with( "whatsapp report text", { "account_sid": "AC1", "auth_token": "tok", "from_number": "+14155551111", "messaging_service_sid": "", "to": "+14155550000", }, ) def test_twilio_sms_skipped_when_channel_disabled(monkeypatch: pytest.MonkeyPatch) -> None: _patch_generate_report_deps(monkeypatch) mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) mock_sms = MagicMock(return_value=(True, "", "SM-X")) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch("integrations.twilio.delivery.send_twilio_sms_report", mock_sms), ): from tools.investigation.reporting.node import generate_report generate_report( _make_state( resolved_integrations={ "twilio": { "account_sid": "AC1", "auth_token": "tok", "sms": {"enabled": False, "from_number": "+14155551111"}, } } ) ) # type: ignore[arg-type] mock_sms.assert_not_called() def test_twilio_sms_skipped_without_recipient(monkeypatch: pytest.MonkeyPatch) -> None: _patch_generate_report_deps(monkeypatch) mock_send_slack = MagicMock(return_value=(False, None)) mock_build_action_blocks = MagicMock(return_value=[]) mock_sms = MagicMock(return_value=(True, "", "SM-X")) with ( patch("integrations.slack.delivery.send_slack_report", mock_send_slack), patch("integrations.slack.delivery.build_action_blocks", mock_build_action_blocks), patch("integrations.twilio.delivery.send_twilio_sms_report", mock_sms), ): from tools.investigation.reporting.node import generate_report generate_report( _make_state( resolved_integrations={ "twilio": { "account_sid": "AC1", "auth_token": "tok", "sms": { "enabled": True, "from_number": "+14155551111", "default_to": "", "messaging_service_sid": "", }, } } ) ) # type: ignore[arg-type] mock_sms.assert_not_called()