Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
"""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 {})
|
||||
@@ -0,0 +1,187 @@
|
||||
"""Relay lane parity: flat in_channel continuable cron surface (Coatue F1).
|
||||
|
||||
Field report 2026-08-18: on relay-fronted Slack, cron briefs ALWAYS deliver
|
||||
into a dedicated thread — `cron_continuable_surface: in_channel` (the flat-DM
|
||||
continuable surface, native Slack's documented shape) is inert on the relay
|
||||
lane. Three gaps, each pinned here:
|
||||
|
||||
1. CapabilityDescriptor has no in_channel capability bit, so RelayAdapter
|
||||
inherits supports_inchannel_continuable=False from the base class and the
|
||||
scheduler fails safe to thread mode (D6 gate).
|
||||
2. The scheduler reads the surface knob flat off pconfig.extra; on the relay
|
||||
lane pconfig is platforms.relay, whose documented Slack knobs live under
|
||||
extra.slack.* (reply_in_thread precedent) — so the operator has no working
|
||||
place to put the knob.
|
||||
3. Descriptor renegotiation (_apply_descriptor) must preserve the new
|
||||
capability bit, same as supports_code_blocks.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.relay.descriptor import CapabilityDescriptor
|
||||
from cron.scheduler import _resolve_cron_surface_mode
|
||||
|
||||
|
||||
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 TestDescriptorCapabilityBit:
|
||||
def test_defaults_false(self):
|
||||
d = _descriptor()
|
||||
assert d.supports_inchannel_continuable is False
|
||||
|
||||
def test_from_json_reads_flag(self):
|
||||
import json
|
||||
|
||||
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_inchannel_continuable=True,
|
||||
)
|
||||
d = CapabilityDescriptor.from_json(json.dumps(payload))
|
||||
assert d.supports_inchannel_continuable is True
|
||||
|
||||
def test_from_json_missing_flag_defaults_false(self):
|
||||
"""Older connector that never sends the field — legacy-safe."""
|
||||
import json
|
||||
|
||||
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",
|
||||
)
|
||||
d = CapabilityDescriptor.from_json(json.dumps(payload))
|
||||
assert d.supports_inchannel_continuable is False
|
||||
|
||||
|
||||
class TestRelayAdapterCapabilityMapping:
|
||||
def _adapter(self, descriptor):
|
||||
from gateway.config import PlatformConfig
|
||||
from gateway.relay.adapter import RelayAdapter
|
||||
|
||||
config = PlatformConfig(enabled=True, extra={})
|
||||
return RelayAdapter(config, descriptor)
|
||||
|
||||
def test_adapter_maps_descriptor_flag_true(self):
|
||||
adapter = self._adapter(_descriptor(supports_inchannel_continuable=True))
|
||||
assert getattr(adapter, "supports_inchannel_continuable", False) is True
|
||||
|
||||
def test_adapter_maps_descriptor_flag_false(self):
|
||||
adapter = self._adapter(_descriptor())
|
||||
assert getattr(adapter, "supports_inchannel_continuable", True) is False
|
||||
|
||||
def test_renegotiation_updates_flag(self):
|
||||
"""_apply_descriptor must carry the bit, like supports_code_blocks."""
|
||||
adapter = self._adapter(_descriptor())
|
||||
adapter._apply_descriptor(_descriptor(supports_inchannel_continuable=True))
|
||||
assert adapter.supports_inchannel_continuable is True
|
||||
|
||||
|
||||
class TestPerPlatformCapability:
|
||||
"""One RelayAdapter fronts N platforms; the D6 gate must read the
|
||||
DESTINATION platform's negotiated descriptor, not the primary identity's
|
||||
scalar — the connector advertises the bit per platform at handshake."""
|
||||
|
||||
class _Transport:
|
||||
def __init__(self, by_platform):
|
||||
self._by_platform = by_platform
|
||||
self._identities = [(p, "hermes") for p in by_platform]
|
||||
|
||||
def descriptor_for_platform(self, platform):
|
||||
return self._by_platform.get(platform)
|
||||
|
||||
def _adapter(self, primary, by_platform):
|
||||
from gateway.config import PlatformConfig
|
||||
from gateway.relay.adapter import RelayAdapter
|
||||
|
||||
config = PlatformConfig(enabled=True, extra={})
|
||||
return RelayAdapter(config, primary, transport=self._Transport(by_platform))
|
||||
|
||||
def test_primary_true_does_not_leak_onto_other_platform(self):
|
||||
"""Slack-primary (capable) + Discord fronted (not advertised):
|
||||
Discord must NOT inherit Slack's bit through the scalar."""
|
||||
slack = _descriptor(supports_inchannel_continuable=True)
|
||||
discord = _descriptor(platform="discord", label="Discord",
|
||||
markdown_dialect="markdown")
|
||||
adapter = self._adapter(slack, {"slack": slack, "discord": discord})
|
||||
assert adapter.supports_inchannel_continuable_for_platform("slack") is True
|
||||
assert adapter.supports_inchannel_continuable_for_platform("discord") is False
|
||||
|
||||
def test_nonprimary_advertised_bit_is_honored(self):
|
||||
"""Discord-primary (not capable) + Slack fronted (advertised):
|
||||
Slack's own descriptor must win over the primary scalar False."""
|
||||
discord = _descriptor(platform="discord", label="Discord",
|
||||
markdown_dialect="markdown")
|
||||
slack = _descriptor(supports_inchannel_continuable=True)
|
||||
adapter = self._adapter(discord, {"slack": slack, "discord": discord})
|
||||
assert adapter.supports_inchannel_continuable_for_platform("slack") is True
|
||||
assert adapter.supports_inchannel_continuable_for_platform("discord") is False
|
||||
|
||||
def test_unknown_platform_falls_back_to_scalar(self):
|
||||
"""A platform the transport has no descriptor for (or a transport
|
||||
predating descriptor_for_platform) keeps the scalar behavior."""
|
||||
slack = _descriptor(supports_inchannel_continuable=True)
|
||||
adapter = self._adapter(slack, {"slack": slack})
|
||||
assert adapter.supports_inchannel_continuable_for_platform("matrix") is True
|
||||
|
||||
|
||||
class TestSurfaceKnobResolution:
|
||||
"""_resolve_cron_surface_mode reads the knob from BOTH config shapes."""
|
||||
|
||||
def test_native_flat_key(self):
|
||||
pconfig = SimpleNamespace(extra={"cron_continuable_surface": "in_channel"})
|
||||
assert _resolve_cron_surface_mode(pconfig, "slack") == "in_channel"
|
||||
|
||||
def test_relay_slack_subblock(self):
|
||||
"""The relay lane's documented shape: platforms.relay.extra.slack.*
|
||||
(same seam as reply_in_thread / dm_top_level_threads_as_sessions)."""
|
||||
pconfig = SimpleNamespace(
|
||||
extra={"slack": {"cron_continuable_surface": "in_channel"}}
|
||||
)
|
||||
assert _resolve_cron_surface_mode(pconfig, "slack") == "in_channel"
|
||||
|
||||
def test_subblock_is_per_logical_platform(self):
|
||||
"""A slack sub-block must not leak onto another fronted platform."""
|
||||
pconfig = SimpleNamespace(
|
||||
extra={"slack": {"cron_continuable_surface": "in_channel"}}
|
||||
)
|
||||
assert _resolve_cron_surface_mode(pconfig, "discord") == "thread"
|
||||
|
||||
def test_default_thread(self):
|
||||
pconfig = SimpleNamespace(extra={})
|
||||
assert _resolve_cron_surface_mode(pconfig, "slack") == "thread"
|
||||
|
||||
def test_subblock_wins_over_flat_key(self):
|
||||
"""Sub-block precedence: the per-logical-platform sub-block is the
|
||||
documented relay shape and wins when both are present — matches
|
||||
_relay_slack_extra (sub-dict preferred over flat extra)."""
|
||||
pconfig = SimpleNamespace(
|
||||
extra={
|
||||
"cron_continuable_surface": "thread",
|
||||
"slack": {"cron_continuable_surface": "in_channel"},
|
||||
}
|
||||
)
|
||||
# Sub-block is the documented relay shape and wins when present —
|
||||
# matches _relay_slack_extra (sub-dict preferred over flat extra).
|
||||
assert _resolve_cron_surface_mode(pconfig, "slack") == "in_channel"
|
||||
|
||||
def test_none_pconfig_defaults_thread(self):
|
||||
assert _resolve_cron_surface_mode(None, "slack") == "thread"
|
||||
@@ -0,0 +1,195 @@
|
||||
"""Prompt-ack sends must never seal an open draft stream.
|
||||
|
||||
Live finding (rc.4 staging, 100% reproducible on approval turns): the
|
||||
"✅ Approved once" acknowledgement the adapter sends after resolving an
|
||||
exec-approval prompt_response carried only placement metadata (thread_id)
|
||||
— no per-turn message identity and no interim marker. send()'s
|
||||
single-open-stream fallback (review B2: "a chat with exactly one open
|
||||
stream absorbs a turn-final arriving without identity") therefore matched
|
||||
the approval turn's OWN live draft and sealed it with the ack text:
|
||||
|
||||
* every subsequent append hit the post-seal tombstone (silently
|
||||
swallowed by design — built for millisecond stragglers), freezing the
|
||||
visible draft mid-word with zero log lines;
|
||||
* the turn-final then found no open draft and fell through to a plain
|
||||
send — the duplicate (fallback) message the user saw. Also silent:
|
||||
no suppression line, no seal-failed warning.
|
||||
|
||||
The fix marks every prompt-lifecycle system send (approval ack, slash-
|
||||
confirm ack, expiry notice) as an interim send, which bypasses draft
|
||||
matching entirely. These tests pin the whole class, plus the regression
|
||||
contract that real turn-finals still absorb into their stream.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
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=True,
|
||||
supports_edit=True,
|
||||
supports_threads=True,
|
||||
markdown_dialect="mrkdwn",
|
||||
len_unit="chars",
|
||||
supported_ops=("send", "edit", "draft", "prompt"),
|
||||
)
|
||||
base.update(overrides)
|
||||
return CapabilityDescriptor(**base)
|
||||
|
||||
|
||||
class FakeTransport:
|
||||
def __init__(self):
|
||||
self.frames = []
|
||||
self._identities = [("slack", "hermes")]
|
||||
|
||||
def descriptor_for_platform(self, platform):
|
||||
return None
|
||||
|
||||
async def send_outbound(self, frame, platform=None):
|
||||
self.frames.append((frame, platform))
|
||||
return {"success": True, "message_id": "1.2"}
|
||||
|
||||
|
||||
class FakeSource:
|
||||
chat_id = "D01"
|
||||
thread_id = "111.222"
|
||||
|
||||
|
||||
class FakeEvent:
|
||||
def __init__(self, prompt_id, option_id):
|
||||
self.prompt_response = {"prompt_id": prompt_id, "option_id": option_id}
|
||||
self.source = FakeSource()
|
||||
|
||||
|
||||
def _adapter():
|
||||
config = PlatformConfig(enabled=True, extra={})
|
||||
return RelayAdapter(config, _descriptor(), transport=FakeTransport())
|
||||
|
||||
|
||||
async def _open_turn_draft(a, chat_id="D01", draft_id=7):
|
||||
"""Open a live draft the way a streaming turn does."""
|
||||
await a.send_draft(chat_id, draft_id, "streaming partial…")
|
||||
key = a._draft_key(chat_id, None)
|
||||
candidates = [k for k in a._open_draft_by_chat if k.startswith(f"{chat_id}:")]
|
||||
assert candidates, "test setup: draft did not arm interception"
|
||||
return candidates[0]
|
||||
|
||||
|
||||
class TestPromptAckDoesNotSealDraft:
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_response_handler_does_not_block_on_ack_send(self):
|
||||
"""Live finding round 2 (rc.4): _consume_prompt_response runs ON the
|
||||
transport read loop (inbound frame -> _handle_frame -> _inbound).
|
||||
Awaiting the ack send there is a self-deadlock: the outbound_result
|
||||
that resolves the send's future arrives on the SAME read loop, which
|
||||
is blocked inside the handler. Every tap wedged the transport for
|
||||
the full outbound timeout — draft appends starved (frozen stream),
|
||||
a second approval card's ack couldn't be read (send timed out,
|
||||
'possibly-delivered'), and the seal timed out ambiguous (plain-send
|
||||
duplicate). The handler must RETURN without awaiting ack delivery;
|
||||
the ack is best-effort and rides a background task."""
|
||||
a = _adapter()
|
||||
gate = asyncio.Event()
|
||||
orig = a._transport.send_outbound
|
||||
|
||||
async def gated_send(frame, platform=None):
|
||||
if frame.get("op") == "send":
|
||||
await gate.wait() # simulate outbound_result not readable yet
|
||||
return await orig(frame, platform=platform)
|
||||
|
||||
a._transport.send_outbound = gated_send
|
||||
prompt_id = a._mint_prompt(
|
||||
"exec_approval", {"session_key": "sess-dl", "chat_id": "D01"}
|
||||
)
|
||||
# Pre-fix this hangs until the gate opens (deadlock shape) and the
|
||||
# wait_for trips. Post-fix it returns promptly.
|
||||
consumed = await asyncio.wait_for(
|
||||
a._consume_prompt_response(FakeEvent(prompt_id, "once")),
|
||||
timeout=1.0,
|
||||
)
|
||||
assert consumed is True
|
||||
# Release the gate; the background ack must still go out.
|
||||
gate.set()
|
||||
await asyncio.sleep(0.05)
|
||||
ack_frames = [f for f, _ in a._transport.frames if f["op"] == "send"]
|
||||
assert ack_frames, "background ack was never sent"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approval_ack_leaves_open_draft_untouched(self):
|
||||
"""The exact live failure: exec-approval tap resolves while the
|
||||
turn's draft stream is open; the ack must go out as its own plain
|
||||
send and the draft must STILL be armed afterwards."""
|
||||
a = _adapter()
|
||||
draft_key = await _open_turn_draft(a)
|
||||
|
||||
prompt_id = a._mint_prompt(
|
||||
"exec_approval", {"session_key": "sess-1", "chat_id": "D01"}
|
||||
)
|
||||
# resolve_gateway_approval is imported inside the handler; a session
|
||||
# with no waiting entry returns 0 -> "expired" label. Either label
|
||||
# shape exercises the same send path.
|
||||
consumed = await a._consume_prompt_response(
|
||||
FakeEvent(prompt_id, "once")
|
||||
)
|
||||
assert consumed is True
|
||||
# The ack rides a background task now (deadlock fix): yield so it runs.
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
# The draft interception must still be armed for the real turn-final.
|
||||
assert draft_key in a._open_draft_by_chat, (
|
||||
"prompt ack sealed the open draft — the live stuck-stream/"
|
||||
"duplicate-final bug"
|
||||
)
|
||||
# The ack egressed as a plain send op, not a draft seal.
|
||||
ack_frames = [
|
||||
f for f, _ in a._transport.frames if f["op"] == "send"
|
||||
]
|
||||
assert ack_frames, "ack was not sent at all"
|
||||
seal_frames = [
|
||||
f
|
||||
for f, _ in a._transport.frames
|
||||
if f["op"] == "draft" and f.get("final") is True
|
||||
]
|
||||
assert not seal_frames, "ack egressed as a draft seal"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expired_prompt_notice_leaves_open_draft_untouched(self):
|
||||
"""Same class: the expiry notice for an unknown prompt id must not
|
||||
absorb into an open stream either."""
|
||||
a = _adapter()
|
||||
draft_key = await _open_turn_draft(a)
|
||||
consumed = await a._consume_prompt_response(
|
||||
FakeEvent("prompt-never-minted", "once")
|
||||
)
|
||||
assert consumed is True
|
||||
assert draft_key in a._open_draft_by_chat
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_turn_final_still_absorbs_into_single_open_stream(self):
|
||||
"""Regression control (review B2 contract): a REAL turn-final
|
||||
without identity metadata, in a chat with exactly one open stream,
|
||||
must still seal that stream."""
|
||||
a = _adapter()
|
||||
draft_key = await _open_turn_draft(a)
|
||||
result = await a.send("D01", "the full final answer")
|
||||
assert result.success
|
||||
assert draft_key not in a._open_draft_by_chat, (
|
||||
"turn-final no longer absorbs into its stream — B2 fallback broken"
|
||||
)
|
||||
seal_frames = [
|
||||
f
|
||||
for f, _ in a._transport.frames
|
||||
if f["op"] == "draft" and f.get("final") is True
|
||||
]
|
||||
assert len(seal_frames) == 1
|
||||
Reference in New Issue
Block a user