mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
test(slack): regression tests for Block Kit boundary sanitizer and oversized edits
- sanitize_blocks: null column_settings repair (#56615), >3000-char section clamp incl. HTML-escape-inflated approval updates (#62054 / #53693), empty-block drops, header clamp, 50-block cap, plain-text fallback when nothing valid remains, garbage never raises - edit_message: oversized content truncates instead of failing with msg_too_long (#33224 behavior on the plugin adapter)
This commit is contained in:
parent
a43ac2af65
commit
b2ba069cb4
2 changed files with 105 additions and 0 deletions
|
|
@ -2912,6 +2912,15 @@ class TestEditMessage:
|
|||
kwargs = adapter._app.client.chat_update.call_args.kwargs
|
||||
assert kwargs["text"] == "AT&T < 5 > 3"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_message_truncates_oversized_content(self, adapter):
|
||||
"""Oversized edits are truncated instead of failing with msg_too_long."""
|
||||
adapter._app.client.chat_update = AsyncMock(return_value={"ok": True})
|
||||
result = await adapter.edit_message("C123", "1234.5678", "x" * 45000)
|
||||
assert result.success
|
||||
kwargs = adapter._app.client.chat_update.call_args.kwargs
|
||||
assert len(kwargs["text"]) <= adapter.MAX_MESSAGE_LENGTH
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestEditMessageStreamingPipeline
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from plugins.platforms.slack.block_kit import (
|
|||
MAX_HEADER_TEXT,
|
||||
MAX_SECTION_TEXT,
|
||||
render_blocks,
|
||||
sanitize_blocks,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -333,3 +334,98 @@ class TestEmptyContentGuards:
|
|||
assert any(b.get("type") == "header" for b in blocks)
|
||||
assert any(b.get("type") == "table" for b in blocks)
|
||||
self._assert_schema_valid(blocks)
|
||||
|
||||
|
||||
class TestSanitizeBlocks:
|
||||
"""Outbound boundary clamp: one bad block must never fail the whole call.
|
||||
|
||||
Regression coverage for the invalid_blocks / msg_too_long bug class
|
||||
(#56615 null column_settings, #62054 / #53693 >3000-char sections on
|
||||
approval chat.update after HTML-escaping inflation).
|
||||
"""
|
||||
|
||||
def test_none_and_empty_return_none(self):
|
||||
assert sanitize_blocks(None) is None
|
||||
assert sanitize_blocks([]) is None
|
||||
|
||||
def test_valid_payload_passes_through(self):
|
||||
blocks = render_blocks("# Title\n\nbody text\n\n- item")
|
||||
assert sanitize_blocks(blocks) == blocks
|
||||
|
||||
def test_oversized_section_text_is_clamped(self):
|
||||
blocks = [
|
||||
{"type": "section", "text": {"type": "mrkdwn", "text": "x" * 3500}},
|
||||
]
|
||||
out = sanitize_blocks(blocks)
|
||||
assert len(out[0]["text"]["text"]) <= MAX_SECTION_TEXT
|
||||
assert out[0]["text"]["text"].endswith("…")
|
||||
|
||||
def test_html_escape_inflated_approval_update_is_clamped(self):
|
||||
# #53693 / #62054: send path budgeted the RAW text to <=3000, but the
|
||||
# interaction payload echoes it back HTML-escaped (& -> &) so the
|
||||
# chat.update section exceeds the cap.
|
||||
inflated = "a" * 2990 + "&" * 10 # 3040 chars
|
||||
blocks = [
|
||||
{"type": "section", "text": {"type": "mrkdwn", "text": inflated}},
|
||||
{"type": "context", "elements": [{"type": "mrkdwn", "text": "✅ ok"}]},
|
||||
]
|
||||
out = sanitize_blocks(blocks)
|
||||
assert len(out[0]["text"]["text"]) <= MAX_SECTION_TEXT
|
||||
# context block untouched
|
||||
assert out[1] == blocks[1]
|
||||
|
||||
def test_null_column_settings_entries_are_fixed(self):
|
||||
# #56615: Slack rejects null entries in table column_settings.
|
||||
table = {
|
||||
"type": "table",
|
||||
"rows": [[{"type": "rich_text", "elements": []}]],
|
||||
"column_settings": [None, {"align": "center"}, None],
|
||||
}
|
||||
out = sanitize_blocks([table])
|
||||
cs = out[0]["column_settings"]
|
||||
assert cs == [{}, {"align": "center"}]
|
||||
assert all(isinstance(c, dict) for c in cs)
|
||||
|
||||
def test_all_null_column_settings_are_dropped(self):
|
||||
table = {
|
||||
"type": "table",
|
||||
"rows": [[{"type": "rich_text", "elements": []}]],
|
||||
"column_settings": [None, None],
|
||||
}
|
||||
out = sanitize_blocks([table])
|
||||
assert "column_settings" not in out[0]
|
||||
|
||||
def test_empty_blocks_are_dropped(self):
|
||||
blocks = [
|
||||
{"type": "section", "text": {"type": "mrkdwn", "text": " "}},
|
||||
{"type": "rich_text", "elements": []},
|
||||
{"type": "actions", "elements": []},
|
||||
{"type": "context", "elements": []},
|
||||
{"type": "header", "text": {"type": "plain_text", "text": ""}},
|
||||
{"type": "table", "rows": []},
|
||||
{"type": "section", "text": {"type": "mrkdwn", "text": "keep me"}},
|
||||
]
|
||||
out = sanitize_blocks(blocks)
|
||||
assert out == [blocks[-1]]
|
||||
|
||||
def test_all_invalid_returns_none_for_plain_text_fallback(self):
|
||||
blocks = [{"type": "section", "text": {"type": "mrkdwn", "text": ""}}]
|
||||
assert sanitize_blocks(blocks) is None
|
||||
|
||||
def test_oversized_header_is_clamped(self):
|
||||
blocks = [
|
||||
{"type": "header", "text": {"type": "plain_text", "text": "h" * 200}},
|
||||
]
|
||||
out = sanitize_blocks(blocks)
|
||||
assert len(out[0]["text"]["text"]) <= MAX_HEADER_TEXT
|
||||
|
||||
def test_payload_capped_at_50_blocks(self):
|
||||
blocks = [
|
||||
{"type": "section", "text": {"type": "mrkdwn", "text": f"b{i}"}}
|
||||
for i in range(60)
|
||||
]
|
||||
out = sanitize_blocks(blocks)
|
||||
assert len(out) == MAX_BLOCKS
|
||||
|
||||
def test_never_raises_on_garbage(self):
|
||||
assert sanitize_blocks([{"no_type": True}, "not-a-dict", 42]) is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue