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.
This commit is contained in:
teknium1
2026-09-13 19:49:20 -07:00
committed by Teknium
parent a74e0155b6
commit aea84cbb1a
2 changed files with 46 additions and 129 deletions
+9 -13
View File
@@ -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)
+37 -116
View File
@@ -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