From aea84cbb1aae76a79d65985281017737b053b807 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:49:20 -0700 Subject: [PATCH] test(slack): collapse pasted-table tests to five invariants, cover live unfurl path The rebase moved the inbound attachment loop into SlackAdapter._append_link_unfurls, so the nested-table hunk now lives there and is asserted directly. Drop the source- provenance references and duplicate cell-level cases; one ragged/malformed-row test covers raw_text, rich_text, None and unknown cell types. --- plugins/platforms/slack/adapter.py | 22 ++-- tests/gateway/test_slack_pasted_tables.py | 153 ++++++---------------- 2 files changed, 46 insertions(+), 129 deletions(-) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index e60f860d04..9dea15ca99 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -471,9 +471,9 @@ def _render_inline_elements(elements: list) -> str: def _extract_text_from_slack_blocks(blocks: list) -> str: - """Render ``rich_text`` blocks to readable lines, preserving quotes, lists and code. - Quoted/forwarded content lives in nested ``rich_text_quote`` elements that the event's plain - ``text`` field omits.""" + """Render ``rich_text`` blocks to readable lines (quotes, lists, code) and ``table`` blocks as + pipe rows. Quoted/forwarded content lives in nested ``rich_text_quote`` elements and pasted + tables in ``table`` blocks; the event's plain ``text`` field omits both.""" if not blocks: return "" parts: list[str] = [] @@ -560,13 +560,10 @@ def _render_slack_table_block( ) -> str: """Render a Slack ``table`` block as ``cell | cell | cell`` lines. - Slack represents a **pasted table** as ``blocks[]`` entries of type - ``table`` (usually nested inside ``attachments[].blocks[]``). The table - appears in neither the message ``text`` nor the file list, so without - this projection the agent receives the sentence before the table and - nothing else — the table silently does not exist. - - Ported from qwibitai/nanoclaw#3666 (``slack-raw-text.ts``). + Slack represents a **pasted table** as ``blocks[]`` entries of type ``table`` (usually nested + inside ``attachments[].blocks[]``). It appears in neither the message ``text`` nor the file + list, so without this projection the agent receives the sentence before the table and + nothing else. """ rows = block.get("rows") if isinstance(block, dict) else None if not isinstance(rows, list): @@ -680,9 +677,8 @@ def _extract_additional_text_from_slack_blocks( for block in blocks or []: block_type = (block or {}).get("type") if block_type == "table": - # Pasted tables (qwibitai/nanoclaw#3666): a top-level ``table`` - # block never appears in the plain text, and the JSON serializer - # drops ``rows``, so this is the only path that surfaces it. + # A top-level ``table`` block never appears in the plain text and the JSON serializer + # drops ``rows``, so this is the only path that surfaces a pasted table. table_text = _render_slack_table_block(block) if table_text: parts.append(table_text) diff --git a/tests/gateway/test_slack_pasted_tables.py b/tests/gateway/test_slack_pasted_tables.py index fc9624bbc4..7fa4990363 100644 --- a/tests/gateway/test_slack_pasted_tables.py +++ b/tests/gateway/test_slack_pasted_tables.py @@ -1,145 +1,66 @@ -"""Slack pasted-table recovery (ported from qwibitai/nanoclaw#3666). +"""Slack pasted-table recovery. Slack represents a pasted table as ``table`` blocks — usually nested inside -``attachments[].blocks[]``, sometimes top-level. The table appears in neither -the message ``text`` nor the file list, so before this port the agent -received the sentence before the table and nothing else. +``attachments[].blocks[]``, sometimes top-level. The table appears in neither the message +``text`` nor the file list, so the agent used to receive the sentence before it and nothing else. """ from plugins.platforms.slack.adapter import ( _SLACK_TABLE_MAX_CHARS, - _collect_slack_table_cell_text, + SlackAdapter, _extract_additional_text_from_slack_blocks, _extract_text_from_slack_attachments, - _extract_text_from_slack_blocks, _render_slack_table_block, _serialize_slack_blocks_for_agent, ) -def _raw_cell(text: str) -> dict: +def _raw(text: str) -> dict: return {"type": "raw_text", "text": text} -def _rich_cell(text: str, bold: bool = False) -> dict: - style = {"bold": True} if bold else {} - return { - "type": "rich_text", - "elements": [ - { - "type": "rich_text_section", - "elements": [{"type": "text", "text": text, "style": style}], - } - ], - } +def _rich(text: str) -> dict: + return {"type": "rich_text", "elements": [ + {"type": "rich_text_section", "elements": [{"type": "text", "text": text, "style": {"bold": True}}]}]} -def _table_block(rows) -> dict: +def _table(rows) -> dict: return {"type": "table", "rows": rows} -class TestCellText: - def test_raw_text_cell(self): - assert _collect_slack_table_cell_text(_raw_cell("Name")) == "Name" - - def test_rich_text_cell_collects_leaves(self): - assert _collect_slack_table_cell_text(_rich_cell("Bold header", bold=True)) == ( - "Bold header" - ) - - def test_non_dict_cell_is_empty(self): - assert _collect_slack_table_cell_text("stray") == "" - assert _collect_slack_table_cell_text(None) == "" - - def test_list_of_nodes(self): - cells = [_raw_cell("a"), _raw_cell("b")] - assert _collect_slack_table_cell_text(cells) == "a b" +def test_render_handles_raw_rich_ragged_and_malformed_cells(): + block = _table([ + [_raw("Name"), _rich("Status")], + "not-a-row", + [_raw(""), None], + [_raw("Hermes"), _rich("ok"), {"type": "mystery"}], + ]) + assert _render_slack_table_block(block) == "Name | Status\nHermes | ok | " + assert _render_slack_table_block({"type": "table"}) == "" + assert _render_slack_table_block({"type": "table", "rows": "bad"}) == "" -class TestRenderTableBlock: - def test_projects_rows_pipe_separated(self): - block = _table_block( - [ - [_raw_cell("Name"), _raw_cell("Status")], - [_raw_cell("Hermes"), _rich_cell("ok")], - ] - ) - assert _render_slack_table_block(block) == "Name | Status\nHermes | ok" - - def test_no_rows_returns_empty(self): - assert _render_slack_table_block({"type": "table"}) == "" - assert _render_slack_table_block({"type": "table", "rows": "bad"}) == "" - assert _render_slack_table_block(_table_block([])) == "" - - def test_malformed_row_skipped(self): - block = _table_block(["not-a-row", [_raw_cell("x"), _raw_cell("y")]]) - assert _render_slack_table_block(block) == "x | y" - - def test_empty_rows_dropped(self): - block = _table_block([[_raw_cell(""), _raw_cell("")], [_raw_cell("k")]]) - assert _render_slack_table_block(block) == "k" - - def test_truncation_cap(self): - big = _table_block([[_raw_cell("x" * 5000)] for _ in range(10)]) - out = _render_slack_table_block(big) - assert out.endswith("[table truncated]") - assert len(out) <= _SLACK_TABLE_MAX_CHARS +def test_render_caps_huge_tables_with_visible_marker(): + out = _render_slack_table_block(_table([[_raw("x" * 5000)] for _ in range(10)])) + assert out.endswith("[table truncated]") + assert len(out) <= _SLACK_TABLE_MAX_CHARS -class TestBlockExtraction: - def test_top_level_table_block_rendered(self): - blocks = [_table_block([[_raw_cell("a"), _raw_cell("b")]])] - assert _extract_text_from_slack_blocks(blocks) == "a | b" - - def test_table_alongside_rich_text(self): - blocks = [ - { - "type": "rich_text", - "elements": [ - { - "type": "rich_text_section", - "elements": [{"type": "text", "text": "See table:"}], - } - ], - }, - _table_block([[_raw_cell("k"), _raw_cell("v")]]), - ] - out = _extract_text_from_slack_blocks(blocks) - assert "See table:" in out - assert "k | v" in out - - def test_additional_text_path_surfaces_table(self): - # The live inbound path routes top-level blocks through - # _extract_additional_text_from_slack_blocks with the flat text as - # the dedupe reference — the table must survive that dedupe. - blocks = [_table_block([[_raw_cell("col1"), _raw_cell("col2")]])] - out = _extract_additional_text_from_slack_blocks(blocks, "intro sentence") - assert "col1 | col2" in out +def test_top_level_table_survives_inbound_dedupe(): + # The live inbound path routes top-level blocks through the dedupe-against-flat-text helper. + out = _extract_additional_text_from_slack_blocks( + [_table([[_raw("col1"), _raw("col2")]])], "intro sentence") + assert "col1 | col2" in out -class TestAttachmentNestedTable: - def test_attachment_blocks_table_recovered(self): - # The real-world shape: pasted table arrives as - # attachments[].blocks[] with type "table" and nothing in text. - attachments = [ - {"blocks": [_table_block([[_raw_cell("Item"), _raw_cell("Qty")]])]} - ] - out = _extract_text_from_slack_attachments(attachments) - assert "Item | Qty" in out +def test_attachment_nested_table_reaches_live_and_history_paths(): + attachments = [{"blocks": [_table([[_raw("Item"), _raw("Qty")]])]}] + assert "Item | Qty" in SlackAdapter._append_link_unfurls("intro", attachments) + assert "Item | Qty" in _extract_text_from_slack_attachments(attachments) -class TestSerializerSkipsTables: - def test_table_block_not_json_dumped(self): - # Table blocks are rendered as text; the JSON serializer must not - # emit an empty {"type": "table"} husk for them. - blocks = [_table_block([[_raw_cell("a")]])] - assert _serialize_slack_blocks_for_agent(blocks) == "" - - def test_other_blocks_still_serialized(self): - blocks = [ - _table_block([[_raw_cell("a")]]), - {"type": "section", "text": {"type": "mrkdwn", "text": "hello"}}, - ] - out = _serialize_slack_blocks_for_agent(blocks) - assert "section" in out - assert '"table"' not in out +def test_serializer_skips_table_husk_but_keeps_other_blocks(): + blocks = [_table([[_raw("a")]]), {"type": "section", "text": {"type": "mrkdwn", "text": "hello"}}] + assert _serialize_slack_blocks_for_agent([blocks[0]]) == "" + out = _serialize_slack_blocks_for_agent(blocks) + assert "section" in out and '"table"' not in out