4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
241 lines
9.0 KiB
Python
241 lines
9.0 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Generator
|
|
|
|
import pytest
|
|
|
|
from platform.terminal.theme import DEFAULT_THEME_NAME, set_active_theme
|
|
from tools.investigation.reporting.renderers.terminal import (
|
|
_rich_line_with_links,
|
|
_strip_mrkdwn,
|
|
_strip_slack_links,
|
|
render_report,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def restore_active_theme() -> Generator[None]:
|
|
"""Restore the default active theme after tests mutate the global palette."""
|
|
yield
|
|
set_active_theme(DEFAULT_THEME_NAME)
|
|
|
|
|
|
_GREEN_BRAND_HEX = "#66A17D"
|
|
_AMBER_BRAND_HEX = "#C99944"
|
|
_GREEN_HIGHLIGHT_HEX = "#B9EDAF"
|
|
_AMBER_HIGHLIGHT_HEX = "#F2D48A"
|
|
|
|
|
|
def test_rich_line_with_links_tracks_active_brand(restore_active_theme: None) -> None:
|
|
"""Link styling must follow set_active_theme BRAND, not a static palette."""
|
|
set_active_theme("green")
|
|
green = _rich_line_with_links("<https://example.com|grafana>")
|
|
green_styles = [str(span.style) for span in green.spans if span.style]
|
|
assert any(_GREEN_BRAND_HEX in style for style in green_styles)
|
|
|
|
set_active_theme("amber")
|
|
amber = _rich_line_with_links("<https://example.com|grafana>")
|
|
amber_styles = [str(span.style) for span in amber.spans if span.style]
|
|
assert any(_AMBER_BRAND_HEX in style for style in amber_styles)
|
|
assert not any(_GREEN_BRAND_HEX in style for style in amber_styles)
|
|
|
|
|
|
def test_render_rich_section_heading_tracks_active_highlight(restore_active_theme: None) -> None:
|
|
"""Section headings must follow set_active_theme HIGHLIGHT, not a static palette."""
|
|
from tools.investigation.reporting.renderers.terminal import _render_rich_section_heading
|
|
|
|
captured: list[object] = []
|
|
|
|
class FakeConsole:
|
|
def print(self, *args: object, **_kwargs: object) -> None:
|
|
if args:
|
|
captured.append(args[0])
|
|
|
|
set_active_theme("green")
|
|
_render_rich_section_heading(FakeConsole(), "Findings")
|
|
assert _GREEN_HIGHLIGHT_HEX in str(captured[0])
|
|
|
|
captured.clear()
|
|
set_active_theme("amber")
|
|
_render_rich_section_heading(FakeConsole(), "Findings")
|
|
amber_rule = str(captured[0])
|
|
assert _AMBER_HIGHLIGHT_HEX in amber_rule
|
|
assert _GREEN_HIGHLIGHT_HEX not in amber_rule
|
|
|
|
|
|
def test_strip_mrkdwn_does_not_cross_lines_with_metric_regex() -> None:
|
|
text = 'Run `{__name__=~"pipeline_runs_.*"}`\n\n*Cited Evidence:*'
|
|
|
|
assert _strip_mrkdwn(text).endswith("\n\nCited Evidence:")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw, expected",
|
|
[
|
|
# Bold markers — the only formatting the function actually strips.
|
|
("*bold*", "bold"),
|
|
("text *bold* more", "text bold more"),
|
|
("*one*\n*two*", "one\ntwo"),
|
|
# Italic, inline code, and fenced code are not handled by the renderer
|
|
# and must pass through verbatim. These assertions document that
|
|
# contract so a future change is forced to update the tests too.
|
|
("_italic_", "_italic_"),
|
|
("`code`", "`code`"),
|
|
("```block```", "```block```"),
|
|
# Nested formatting: only bold is unwrapped; the inner italic markers
|
|
# remain because the renderer does not touch them.
|
|
("*bold _italic_*", "bold _italic_"),
|
|
# Empty and whitespace-only inputs are returned unchanged.
|
|
("", ""),
|
|
(" ", " "),
|
|
("\n\n", "\n\n"),
|
|
# Unicode and emoji content is preserved when wrappers are stripped.
|
|
("*café*", "café"),
|
|
("*🎉 party 🎉*", "🎉 party 🎉"),
|
|
("héllo wörld", "héllo wörld"),
|
|
# No markdown -> pure passthrough.
|
|
("hello world", "hello world"),
|
|
("plain text with no markers at all.", "plain text with no markers at all."),
|
|
# A lone `*` without a closing partner is left as-is.
|
|
("a * b", "a * b"),
|
|
# The bold regex must not span newlines (regression test for the
|
|
# existing happy-path case, kept parametrized for symmetry).
|
|
("*line1\nline2*", "*line1\nline2*"),
|
|
],
|
|
)
|
|
def test_strip_mrkdwn(raw: str, expected: str) -> None:
|
|
assert _strip_mrkdwn(raw) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw, expected",
|
|
[
|
|
# Bare URL link -> the URL alone.
|
|
("<https://example.com>", "https://example.com"),
|
|
# Labeled link -> "label (url)".
|
|
("<https://example.com|Example>", "Example (https://example.com)"),
|
|
# Labeled link embedded in surrounding prose.
|
|
(
|
|
"Click <https://example.com|here> for details.",
|
|
"Click here (https://example.com) for details.",
|
|
),
|
|
# Multiple links in the same string are each rewritten.
|
|
(
|
|
"<https://a.test|A> and <https://b.test|B>",
|
|
"A (https://a.test) and B (https://b.test)",
|
|
),
|
|
# No Slack-style link -> passthrough (including bare URLs, which
|
|
# _strip_slack_links is not responsible for).
|
|
("plain text", "plain text"),
|
|
(
|
|
"https://example.com without angle brackets",
|
|
"https://example.com without angle brackets",
|
|
),
|
|
# Empty input.
|
|
("", ""),
|
|
],
|
|
)
|
|
def test_strip_slack_links(raw: str, expected: str) -> None:
|
|
assert _strip_slack_links(raw) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw, expected_plain",
|
|
[
|
|
# Plain text round-trips as-is.
|
|
("hello world", "hello world"),
|
|
# Labeled Slack link is rendered using the label, not the URL.
|
|
("Click <https://example.com|here>!", "Click here!"),
|
|
# Bare URL is preserved verbatim in the plain projection.
|
|
("see https://example.com for details", "see https://example.com for details"),
|
|
# Bare Slack link (no label) falls back to the URL.
|
|
("<https://example.com>", "https://example.com"),
|
|
# Empty string yields empty plain text.
|
|
("", ""),
|
|
],
|
|
)
|
|
def test_rich_line_with_links_plain_projection(raw: str, expected_plain: str) -> None:
|
|
result = _rich_line_with_links(raw)
|
|
|
|
assert result.plain == expected_plain
|
|
|
|
|
|
def test_rich_line_with_links_emits_link_span_for_labeled_url() -> None:
|
|
result = _rich_line_with_links("Click <https://example.com|here>!")
|
|
|
|
link_spans = [span for span in result.spans if "link https://example.com" in str(span.style)]
|
|
assert link_spans, "expected a styled link span for the labeled URL"
|
|
|
|
|
|
# NOTE: documents current behavior, not desired behavior.
|
|
#
|
|
# `_URL_RE = r"https?://\S+"` greedily matches trailing punctuation because
|
|
# `.`, `,`, `;`, `)` are all non-whitespace. The renderer then `rstrip`s those
|
|
# characters off the URL used in the link span, but `sub_cursor` still advances
|
|
# to `m.end()` — so the punctuation is dropped from `result.plain` as well.
|
|
#
|
|
# A future fix that re-emits the stripped characters into the plain text should
|
|
# update the `expected_plain` column below to include the trailing punctuation.
|
|
@pytest.mark.parametrize(
|
|
"raw, expected_url, expected_plain",
|
|
[
|
|
("see https://example.com.", "https://example.com", "see https://example.com"),
|
|
(
|
|
"see https://example.com, then go.",
|
|
"https://example.com",
|
|
"see https://example.com then go.",
|
|
),
|
|
("see https://example.com; done", "https://example.com", "see https://example.com done"),
|
|
("see (https://example.com)", "https://example.com", "see (https://example.com"),
|
|
],
|
|
)
|
|
def test_rich_line_with_links_strips_trailing_punctuation_from_bare_url(
|
|
raw: str, expected_url: str, expected_plain: str
|
|
) -> None:
|
|
result = _rich_line_with_links(raw)
|
|
|
|
link_spans = [span for span in result.spans if f"link {expected_url} " in str(span.style)]
|
|
assert link_spans, f"expected a styled link span for the bare URL {expected_url!r}"
|
|
assert result.plain == expected_plain
|
|
|
|
|
|
def test_rich_line_with_links_emits_link_span_for_bare_url() -> None:
|
|
result = _rich_line_with_links("see https://example.com for details")
|
|
|
|
link_spans = [span for span in result.spans if "link https://example.com" in str(span.style)]
|
|
assert link_spans, "expected a styled link span for the bare URL"
|
|
assert result.plain == "see https://example.com for details"
|
|
|
|
|
|
def test_render_report_plain_mode_strips_mrkdwn_and_slack_links(
|
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
monkeypatch.setenv("TRACER_OUTPUT_FORMAT", "text")
|
|
|
|
slack_message = (
|
|
"## Findings\n"
|
|
"- *high* cpu on <https://dashboards.example.com|grafana>\n"
|
|
"*Cited Evidence:*\n"
|
|
"- <https://logs.example.com|loki>\n"
|
|
)
|
|
|
|
render_report(slack_message)
|
|
|
|
out = capsys.readouterr().out
|
|
# Bold markers stripped; Slack links rewritten to "label (url)".
|
|
assert "*high*" not in out
|
|
assert "high cpu on grafana (https://dashboards.example.com)" in out
|
|
assert "Cited Evidence:" in out
|
|
assert "<https://logs.example.com|loki>" not in out
|
|
assert "loki (https://logs.example.com)" in out
|
|
|
|
|
|
def test_render_report_empty_message_prints_no_report_generated(
|
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
monkeypatch.setenv("TRACER_OUTPUT_FORMAT", "text")
|
|
|
|
render_report("")
|
|
|
|
assert "No report generated." in capsys.readouterr().out
|