Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Local integration trace: REAL StreamConsumer + REAL RelayAdapter + stub
|
||||
transport. Reproduces the multi-segment live-cards turn to reveal the exact
|
||||
op sequence the connector sees (finding #4/#5 forensics — Alice canary).
|
||||
|
||||
Run: python -m pytest tests/gateway/relay/test_live_cards_flow_trace.py -q -s
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.relay.adapter import RelayAdapter
|
||||
|
||||
|
||||
class TraceTransport:
|
||||
"""Stub connector transport recording every outbound op."""
|
||||
|
||||
def __init__(self, fail_ops=None):
|
||||
self.ops = []
|
||||
self.fail_ops = set(fail_ops or ())
|
||||
self._ts = 1000
|
||||
|
||||
async def send_outbound(self, payload, platform=None):
|
||||
op = payload.get("op")
|
||||
self.ops.append(dict(payload))
|
||||
if op in self.fail_ops:
|
||||
return {"success": False, "error": f"stub-forced {op} failure"}
|
||||
self._ts += 1
|
||||
return {"success": True, "message_id": f"{self._ts}.000"}
|
||||
|
||||
|
||||
def _mk_adapter(supported_ops=("send", "edit", "typing", "draft", "task_card", "task_card_stop")):
|
||||
# Reuse the live-cards test helper wiring (descriptor + config stubs).
|
||||
from tests.gateway.relay.test_relay_live_cards import _connected_adapter
|
||||
adapter, _ = _connected_adapter(supported_ops=supported_ops)
|
||||
t = TraceTransport()
|
||||
adapter._transport = t
|
||||
return adapter, t
|
||||
|
||||
|
||||
def test_trace_multisegment_draft_flow():
|
||||
"""Simulate the consumer's actual call pattern for a 3-segment turn:
|
||||
seg1 drafts -> segment-break finalize (send) -> seg2 drafts ->
|
||||
segment-break finalize (send) -> seg3 drafts -> turn-final send.
|
||||
Prints the op timeline; asserts the invariant we EXPECT enterprise-wise:
|
||||
at most ONE final message identity visible to the user.
|
||||
"""
|
||||
adapter, t = _mk_adapter()
|
||||
loop = asyncio.new_event_loop()
|
||||
md = {"thread_ts": "1700.100"}
|
||||
|
||||
async def turn():
|
||||
# segment 1 streaming
|
||||
await adapter.send_draft("C1", 7, "seg1 partial", metadata=md)
|
||||
await adapter.send_draft("C1", 7, "seg1 complete.", metadata=md)
|
||||
# tool boundary (fix #5): consumer now emits a cumulative draft
|
||||
# frame instead of a finalize send for stream-is-the-message
|
||||
# adapters — simulate that call shape.
|
||||
await adapter.send_draft("C1", 7, "seg1 complete.", metadata=md)
|
||||
# segment 2 streaming (fix #4: same draft_id)
|
||||
await adapter.send_draft("C1", 7, "seg1 complete.\nseg2 partial", metadata=md)
|
||||
await adapter.send_draft("C1", 7, "seg1 complete.\nseg2 complete.", metadata=md)
|
||||
# segment 3 + the ONE turn-final send (seal-intercepted)
|
||||
await adapter.send_draft(
|
||||
"C1", 7, "seg1 complete.\nseg2 complete.\nfinal answer partial", metadata=md
|
||||
)
|
||||
r3 = await adapter.send(
|
||||
"C1", "seg1 complete.\nseg2 complete.\nfinal answer complete.", metadata=md
|
||||
)
|
||||
return r3
|
||||
|
||||
r3 = loop.run_until_complete(turn())
|
||||
print("\n--- OP TIMELINE ---")
|
||||
for i, op in enumerate(t.ops):
|
||||
print(f"{i:2d} {op['op']:<16} final={op.get('final')} draft_id={op.get('draft_id')} "
|
||||
f"content={str(op.get('content'))[:40]!r}")
|
||||
finals = [o for o in t.ops if o["op"] == "draft" and o.get("final")]
|
||||
plain_sends = [o for o in t.ops if o["op"] == "send"]
|
||||
print(f"seal frames: {len(finals)}, plain sends: {len(plain_sends)}")
|
||||
# The user-visible message count = seals + plain sends (each seal ends a
|
||||
# visible stream message; each plain send posts a message).
|
||||
visible = len(finals) + len(plain_sends)
|
||||
print(f"user-visible messages this turn: {visible}")
|
||||
assert visible == 1, (
|
||||
f"turn produced {visible} user-visible messages (expected 1): "
|
||||
f"each segment-break send() gets converted to draft(final=true) by the "
|
||||
f"adapter's seal-interception, sealing a stream PER SEGMENT"
|
||||
)
|
||||
|
||||
|
||||
def test_trace_parallel_turns_do_not_collide():
|
||||
"""Finding #10 (live): three concurrent turns in ONE flat DM must keep
|
||||
fully independent stream + card identities. Per-chat keying merged
|
||||
turn B's card into turn A's and clobbered seal state (3x duplicates)."""
|
||||
adapter, t = _mk_adapter()
|
||||
loop = asyncio.new_event_loop()
|
||||
# Each turn's metadata carries its own thread anchor (inbound stamps
|
||||
# thread_ts = event.thread_ts or ts on every top-level message).
|
||||
md_a = {"thread_ts": "100.1"}
|
||||
md_b = {"thread_ts": "200.2"}
|
||||
|
||||
async def interleaved():
|
||||
# A and B stream interleaved on the SAME chat with different anchors
|
||||
await adapter.send_draft("C1", 11, "A partial", metadata=md_a)
|
||||
await adapter.send_draft("C1", 12, "B partial", metadata=md_b)
|
||||
# A's card and B's card must be distinct card_ids
|
||||
await adapter.send_native_task_card_progress(
|
||||
"C1", [{"id": "t1", "title": "x", "status": "in_progress"}],
|
||||
reply_to=None, metadata=md_a)
|
||||
await adapter.send_native_task_card_progress(
|
||||
"C1", [{"id": "t2", "title": "y", "status": "in_progress"}],
|
||||
reply_to=None, metadata=md_b)
|
||||
# A seals; B keeps streaming — B's state must survive A's seal
|
||||
ra = await adapter.send("C1", "A final.", metadata=md_a)
|
||||
await adapter.send_draft("C1", 12, "B partial more", metadata=md_b)
|
||||
rb = await adapter.send("C1", "B final.", metadata=md_b)
|
||||
return ra, rb
|
||||
|
||||
ra, rb = loop.run_until_complete(interleaved())
|
||||
drafts = [o for o in t.ops if o["op"] == "draft"]
|
||||
seals = [o for o in drafts if o.get("final")]
|
||||
cards = [o for o in t.ops if o["op"] == "task_card"]
|
||||
plain = [o for o in t.ops if o["op"] == "send"]
|
||||
# Distinct card identities per turn:
|
||||
assert len({c["card_id"] for c in cards}) == 2, cards
|
||||
# Each turn sealed its OWN stream (2 seals, matching draft_ids 11/12):
|
||||
assert sorted(s["draft_id"] for s in seals) == [11, 12], seals
|
||||
# No leaked plain send: both finals absorbed by their own seals:
|
||||
assert not plain, plain
|
||||
# B's post-A-seal frame was NOT dropped by A's tombstone:
|
||||
b_frames = [d for d in drafts if d["draft_id"] == 12 and not d.get("final")]
|
||||
assert len(b_frames) == 2, b_frames
|
||||
Reference in New Issue
Block a user