Files
hermes-agent/tests/relay/test_relay_format_hints.py
T
Victor Kyriazakos 5210dd48b8 fix(gateway+relay): approval prompts survive ambiguity without duplicates; streamed finals keep block formatting
Three live findings from rc.4 staging, all on the relay-fronted Slack
path, all with the failure observed in live logs before the fix:

1. Approval-send timeout is AMBIGUOUS, not failed (no re-ask).
   send_exec_approval through the connector can time out with the card
   already rendered — the connector may ack after the deadline (slow
   platform API call, transient backpressure, event-loop stall) — and
   the timeout-as-failure path re-sent and produced duplicate cards.
   The outcome is now tri-state: sent / failed / ambiguous. Ambiguous =
   no re-send, no text fallback; the prompt registration stays armed so
   a late tap still resolves. Only a definite send error falls back to
   text.

2. pending_approval tool results forbid re-issuing the command.
   With one card correctly armed, the agent could still mint a SECOND
   card by re-running a rephrased variant of the gated command after
   reading the pending_approval tool result (observed live: same
   command re-issued in a different form, two cards). The tool message
   now instructs: do not re-run/rephrase; wait or report pending.
   Applied to both the terminal and execute_code arms.

3. Draft interim AND seal frames carry format_hints.
   format_hints are stamped on send, edit, and send_for_platform, but
   both draft-frame builders (send_draft interim + _seal_open_draft
   seal) shipped bare metadata. A streamed final therefore arrived at
   the connector hintless and sealed as a plain code block while
   non-streamed sends rendered native markdown blocks (observed live:
   language-tagged block on send/edit, downgrade on streamed seal).
   Both sites now stamp _with_format_hints_for_chat
   (destination-resolved, same pattern as the existing lanes).
   Verified live after the fix against the platform's stored message
   payload: rich_text_preformatted with language field on a streamed
   seal.

Tests: tri-state outcome unit tests (5), draft/seal hint stamping + knobs-
off regression control (2, RED-first), existing format-hints suite intact
(14/14). Mutation-verified: reverting the adapter hunk sends
test_draft_interim_and_seal_frames_carry_hints red; restore -> green.

Boundary sweep (text egress lanes crossing the frame contract): send ✓
(pre-existing) edit ✓ (pre-existing) send_for_platform ✓ (pre-existing)
draft-interim ✓ (this PR) draft-seal ✓ (this PR); task_card lane carries
no text content — exempt.
2026-08-20 19:56:01 -07:00

309 lines
13 KiB
Python

"""Relay lane parity: block formatting hints on outbound frames (Coatue F2).
Field report 2026-08-18, finding 2: identical agent output renders as native
rich_text lists / Block Kit tables / highlighted code on native Slack, but
literal `-` bullets and code-fence tables on the relay lane. Native reads
platforms.slack.extra.rich_blocks / markdown_blocks; relay frames carry no
formatting signal at all, and the connector has no way to know the operator
wants block rendering.
Contract (additive, v1): the connector advertises
``supports_block_formatting`` in its capability descriptor; when the operator
enables the knobs (relay shape: platforms.relay.extra.slack.rich_blocks /
markdown_blocks — same sub-block as the other relay Slack knobs), the gateway
stamps ``format_hints`` into outbound send metadata. Old connectors never
advertise, so no hint is ever sent (no dead metadata); old gateways never
stamp, so connectors keep rendering plain text.
"""
import json
from types import SimpleNamespace
import pytest
from gateway.config import PlatformConfig
from gateway.relay.adapter import RelayAdapter
from gateway.relay.descriptor import CapabilityDescriptor
def _descriptor(**overrides):
base = dict(
contract_version=1,
platform="slack",
label="Slack",
max_message_length=4000,
supports_draft_streaming=False,
supports_edit=True,
supports_threads=True,
markdown_dialect="mrkdwn",
len_unit="chars",
)
base.update(overrides)
return CapabilityDescriptor(**base)
class FakeTransport:
def __init__(self, descriptors_by_platform=None, identities=None):
self.frames = []
# Phase 1.5 multi-platform: per-platform negotiated descriptors and
# the handshaked identity set (fronts_platform reads _identities).
self._descriptors_by_platform = descriptors_by_platform or {}
self._identities = identities or [("slack", "hermes")]
def descriptor_for_platform(self, platform):
return self._descriptors_by_platform.get(platform)
async def send_outbound(self, frame, platform=None):
self.frames.append((frame, platform))
return {"success": True, "message_id": "1.2"}
def _adapter(extra=None, descriptor=None, transport=None):
config = PlatformConfig(enabled=True, extra=extra or {})
a = RelayAdapter(
config, descriptor or _descriptor(), transport=transport or FakeTransport()
)
return a
class TestDescriptorBit:
def test_default_false(self):
assert _descriptor().supports_block_formatting is False
def test_from_json_reads_flag(self):
payload = dict(
contract_version=1, platform="slack", label="Slack",
max_message_length=4000, supports_draft_streaming=False,
supports_edit=True, supports_threads=True,
markdown_dialect="mrkdwn", len_unit="chars",
supports_block_formatting=True,
)
assert CapabilityDescriptor.from_json(
json.dumps(payload)
).supports_block_formatting is True
class TestFormatHintsStamping:
@pytest.mark.asyncio
async def test_hints_stamped_when_capable_and_enabled(self):
a = _adapter(
extra={"slack": {"rich_blocks": True, "markdown_blocks": True}},
descriptor=_descriptor(supports_block_formatting=True),
)
await a.send("D01", "# Report\n\n| a | b |\n|---|---|\n| 1 | 2 |")
frame, _ = a._transport.frames[-1]
hints = (frame.get("metadata") or {}).get("format_hints")
assert hints == {"rich_blocks": True, "markdown_blocks": True}
@pytest.mark.asyncio
async def test_no_hints_when_connector_lacks_capability(self):
"""Old connector: knob on, capability absent -> no dead metadata."""
a = _adapter(
extra={"slack": {"rich_blocks": True}},
descriptor=_descriptor(),
)
await a.send("D01", "text")
frame, _ = a._transport.frames[-1]
assert "format_hints" not in (frame.get("metadata") or {})
@pytest.mark.asyncio
async def test_no_hints_when_knobs_off(self):
"""Capable connector, operator never opted in -> no hint (native
parity: rich_blocks/markdown_blocks are opt-in on native too)."""
a = _adapter(
extra={},
descriptor=_descriptor(supports_block_formatting=True),
)
await a.send("D01", "text")
frame, _ = a._transport.frames[-1]
assert "format_hints" not in (frame.get("metadata") or {})
@pytest.mark.asyncio
async def test_quoted_false_knob_stays_off(self):
"""YAML-quoted 'false' must coerce off — same _coerce_flag semantics
as the other relay Slack knobs."""
a = _adapter(
extra={"slack": {"rich_blocks": "false", "markdown_blocks": "false"}},
descriptor=_descriptor(supports_block_formatting=True),
)
await a.send("D01", "text")
frame, _ = a._transport.frames[-1]
assert "format_hints" not in (frame.get("metadata") or {})
@pytest.mark.asyncio
async def test_partial_knobs_stamp_only_enabled(self):
a = _adapter(
extra={"slack": {"markdown_blocks": True}},
descriptor=_descriptor(supports_block_formatting=True),
)
await a.send("D01", "text")
frame, _ = a._transport.frames[-1]
hints = (frame.get("metadata") or {}).get("format_hints")
assert hints == {"markdown_blocks": True}
@pytest.mark.asyncio
async def test_edit_lane_carries_hints_too(self):
"""Boundary rule: every text egress lane crossing the frame contract
gets the hint — send AND edit (streaming final edits render blocks
on native)."""
a = _adapter(
extra={"slack": {"rich_blocks": True}},
descriptor=_descriptor(supports_block_formatting=True),
)
edit = getattr(a, "edit_message", None)
if edit is None:
pytest.skip("relay adapter has no edit lane")
await edit("D01", "1.2", "updated **content**")
frame, _ = a._transport.frames[-1]
hints = (frame.get("metadata") or {}).get("format_hints")
assert hints == {"rich_blocks": True}
@pytest.mark.asyncio
async def test_draft_interim_and_seal_frames_carry_hints(self):
"""Observed in live relay testing: a STREAMED bash
snippet sealed as a plain code block while send/edit rendered
blocks. The draft lane (interim frame AND the final=true seal
frame) is a text egress lane crossing the same frame contract —
the connector's seal reconcile can only attach the markdown block
if the seal frame carries the hint. Boundary rule: send, edit,
send_for_platform, AND draft/seal all stamp format_hints."""
a = _adapter(
extra={"slack": {"markdown_blocks": True}},
descriptor=_descriptor(
supports_block_formatting=True,
supports_draft_streaming=True,
supported_ops=("send", "edit", "draft"),
),
)
code = "```bash\necho hi\n```"
await a.send_draft("D01", 7, "```bash\necho")
interim, _ = a._transport.frames[-1]
assert interim["op"] == "draft" and interim["final"] is False
assert (interim.get("metadata") or {}).get("format_hints") == {
"markdown_blocks": True
}
seal = getattr(a, "seal_draft", None) or getattr(
a, "send_draft_final", None
)
if seal is not None:
await seal("D01", 7, code)
else:
# The seal frame is built by _seal_open_draft (send()
# interception converts an armed final into draft final=true).
await a._seal_open_draft("D01", code, None)
final_frame, _ = a._transport.frames[-1]
assert final_frame["op"] == "draft" and final_frame["final"] is True
assert (final_frame.get("metadata") or {}).get("format_hints") == {
"markdown_blocks": True
}
@pytest.mark.asyncio
async def test_draft_frames_no_hints_when_knobs_off(self):
"""Regression control: knob off -> draft frames byte-identical to
today (no format_hints key)."""
a = _adapter(
extra={},
descriptor=_descriptor(
supports_block_formatting=True,
supports_draft_streaming=True,
supported_ops=("send", "edit", "draft"),
),
)
await a.send_draft("D01", 8, "plain text")
interim, _ = a._transport.frames[-1]
assert "format_hints" not in (interim.get("metadata") or {})
class TestMultiPlatformResolution:
"""One RelayAdapter fronts N platforms: capability must resolve from the
DESTINATION platform's negotiated descriptor, never the primary identity's
scalar — a Slack-primary adapter must not leak Slack hints onto Discord,
and a Discord-primary adapter must not suppress hints for Slack."""
def _two_platform_transport(self, slack_capable=True, discord_capable=False):
return FakeTransport(
descriptors_by_platform={
"slack": _descriptor(supports_block_formatting=slack_capable),
"discord": _descriptor(
platform="discord", label="Discord",
markdown_dialect="markdown", max_message_length=2000,
supports_block_formatting=discord_capable,
),
},
identities=[("slack", "hermes"), ("discord", "hermes")],
)
@pytest.mark.asyncio
async def test_slack_primary_does_not_leak_hints_onto_discord_chat(self):
"""REGRESSION: _format_hints read self.descriptor (Slack primary,
capable) and the Slack config sub-block for EVERY chat — a known
Discord chat got Slack's format_hints stamped."""
transport = self._two_platform_transport()
a = _adapter(
extra={"slack": {"rich_blocks": True}},
descriptor=_descriptor(supports_block_formatting=True),
transport=transport,
)
# The adapter learned this chat is Discord from inbound traffic.
a._platform_by_chat["777"] = "discord"
await a.send("777", "text")
frame, _ = transport.frames[-1]
assert "format_hints" not in (frame.get("metadata") or {}), (
"Slack-primary adapter stamped Slack format hints on a Discord chat"
)
@pytest.mark.asyncio
async def test_discord_primary_still_stamps_hints_for_slack_chat(self):
"""The inverse: a non-Slack primary must not suppress hints for a
chat whose own (Slack) descriptor advertises the capability."""
transport = self._two_platform_transport()
a = _adapter(
extra={"slack": {"rich_blocks": True}},
descriptor=_descriptor(
platform="discord", label="Discord",
markdown_dialect="markdown", max_message_length=2000,
),
transport=transport,
)
a._platform_by_chat["D01"] = "slack"
await a.send("D01", "text")
frame, _ = transport.frames[-1]
hints = (frame.get("metadata") or {}).get("format_hints")
assert hints == {"rich_blocks": True}, (
"Discord-primary adapter suppressed hints for a capable Slack chat"
)
@pytest.mark.asyncio
async def test_send_for_platform_stamps_hints(self):
"""REGRESSION: the scheduled/persisted-home lane (gateway/delivery.py
→ send_for_platform) never stamped hints at all — yet it is the cron
delivery path, the flagship consumer of block formatting."""
transport = self._two_platform_transport()
a = _adapter(
extra={"slack": {"rich_blocks": True, "markdown_blocks": True}},
descriptor=_descriptor(supports_block_formatting=True),
transport=transport,
)
# No inbound ever seen for this chat — the explicit platform routes it.
await a.send_for_platform("slack", "C123", "| a |\n|---|")
frame, _ = transport.frames[-1]
hints = (frame.get("metadata") or {}).get("format_hints")
assert hints == {"rich_blocks": True, "markdown_blocks": True}
@pytest.mark.asyncio
async def test_send_for_platform_no_hints_for_incapable_platform(self):
"""Explicit-platform sends to a platform whose descriptor does not
advertise the bit stay clean, whatever the primary identity says."""
transport = self._two_platform_transport()
a = _adapter(
extra={
"slack": {"rich_blocks": True},
"discord": {"rich_blocks": True},
},
descriptor=_descriptor(supports_block_formatting=True),
transport=transport,
)
await a.send_for_platform("discord", "777", "text")
frame, _ = transport.frames[-1]
assert "format_hints" not in (frame.get("metadata") or {})