4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
410 lines
14 KiB
Python
410 lines
14 KiB
Python
"""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 "<details>" in result
|
|
assert "<summary>Investigation summary</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("</details>")
|
|
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()
|