Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Per-turn Slack egress identity (R3-5, connector PR gateway-gateway#210).
|
||||
|
||||
The relay connector fills chat.startStream's recipient_user_id /
|
||||
recipient_team_id (required for channel streams) from metadata.user_id /
|
||||
metadata.scope_id. Before this fix the gateway only stamped slack_team_id
|
||||
per-turn; user_id (and scope_id) were left for the relay adapter's
|
||||
_with_scope fallback, which reads per-chat caches keyed only by chat_id —
|
||||
mutable state that a CONCURRENT turn overwrites. Two users with
|
||||
overlapping turns in one channel could open U1's stream with U2 as the
|
||||
recipient.
|
||||
|
||||
These tests pin the fix: _thread_metadata_for_source stamps the authentic
|
||||
per-turn identity from the turn's OWN source, and the cache fallback in
|
||||
RelayAdapter._with_scope no longer decides for turns that carry it.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from gateway.platforms.base import Platform
|
||||
from gateway.run import GatewayRunner
|
||||
|
||||
|
||||
def _slack_source(user_id: str, chat_id: str = "C_SHARED") -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
platform=Platform.SLACK,
|
||||
chat_id=chat_id,
|
||||
thread_id="1700.100",
|
||||
chat_type="channel",
|
||||
message_id="1700.100",
|
||||
scope_id="T_TEAM",
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
|
||||
def test_thread_metadata_stamps_per_turn_user_and_scope():
|
||||
runner = object.__new__(GatewayRunner)
|
||||
meta = runner._thread_metadata_for_source(_slack_source("U_ALICE"))
|
||||
assert meta is not None
|
||||
assert meta["user_id"] == "U_ALICE"
|
||||
assert meta["scope_id"] == "T_TEAM"
|
||||
assert meta["slack_team_id"] == "T_TEAM"
|
||||
|
||||
|
||||
def test_concurrent_turns_carry_their_own_identity():
|
||||
"""The R3-5 interleaving: U2's turn starting must not change what U1's
|
||||
already-built metadata carries — identity is per-turn source data, not
|
||||
shared per-chat state."""
|
||||
runner = object.__new__(GatewayRunner)
|
||||
meta_alice = runner._thread_metadata_for_source(_slack_source("U_ALICE"))
|
||||
# U2's turn begins in the SAME channel before U1's stream opens.
|
||||
meta_bob = runner._thread_metadata_for_source(_slack_source("U_BOB"))
|
||||
assert meta_alice is not None and meta_bob is not None
|
||||
assert meta_alice["user_id"] == "U_ALICE"
|
||||
assert meta_bob["user_id"] == "U_BOB"
|
||||
|
||||
|
||||
def test_relay_with_scope_does_not_override_per_turn_identity():
|
||||
"""_with_scope must be fill-only: a turn that already carries user_id /
|
||||
scope_id keeps them even when the per-chat cache holds another user
|
||||
(the overwrite that caused the R3-5 cross-recipient hazard)."""
|
||||
from gateway.relay.adapter import RelayAdapter
|
||||
|
||||
adapter = object.__new__(RelayAdapter)
|
||||
# The mutable cache holds the OTHER user (their turn arrived later).
|
||||
adapter._scope_by_chat = {"C_SHARED": "T_CACHED"}
|
||||
adapter._dm_user_by_chat = {"C_SHARED": "U_BOB"}
|
||||
merged = adapter._with_scope(
|
||||
"C_SHARED", {"user_id": "U_ALICE", "scope_id": "T_TEAM"}
|
||||
)
|
||||
assert merged["user_id"] == "U_ALICE"
|
||||
assert merged["scope_id"] == "T_TEAM"
|
||||
|
||||
|
||||
def test_relay_with_scope_still_fills_when_turn_carries_nothing():
|
||||
"""Restart/synthetic sends (no per-turn identity) keep the cache
|
||||
fallback — the fix narrows the cache's authority, not its existence."""
|
||||
from gateway.relay.adapter import RelayAdapter
|
||||
|
||||
adapter = object.__new__(RelayAdapter)
|
||||
adapter._scope_by_chat = {"C_SHARED": "T_CACHED"}
|
||||
adapter._dm_user_by_chat = {"C_SHARED": "U_CACHED"}
|
||||
merged = adapter._with_scope("C_SHARED", {"thread_id": "1700.100"})
|
||||
assert merged["user_id"] == "U_CACHED"
|
||||
assert merged["scope_id"] == "T_CACHED"
|
||||
Reference in New Issue
Block a user