Files
aiturk-hermes-ide/tests/gateway/test_prompt_tail_freeze.py
T

330 lines
13 KiB
Python

"""Byte-stable gateway system prompts (the ephemeral session-context pin).
The composed system prompt used to change bytes nearly every gateway turn:
the "## Current Session Context" block was re-rendered from live platform
state per message (thread renames, voice-channel member/speaking state,
one-shot onboarding and auto-reset notes). Every byte change re-keys the
provider prompt cache AND changes the gateway agent-cache signature, forcing
a full AIAgent rebuild per message.
The fix pins the rendered session-context bytes per session keyed by a hash
of the exact renderer inputs (``_ephemeral_change_key``) and relocates
must-deliver per-turn facts onto the current user message (the api_content
sidecar), so a key hit reuses the pinned bytes verbatim.
The maintained invariant — every rendered input appears in the change key —
is guarded by the parity test below.
"""
from __future__ import annotations
import hashlib
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from gateway.config import GatewayConfig, HomeChannel, Platform, PlatformConfig
from gateway.session import (
SessionContext,
SessionSource,
build_session_context_prompt,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_runner(**attrs):
from gateway.run import GatewayRunner
runner = object.__new__(GatewayRunner)
runner._session_ephemeral_pin = {}
runner._session_vc_last = {}
runner._pending_turn_sidecar_notes = {}
runner._session_model_overrides = {}
runner._session_reasoning_overrides = {}
runner.adapters = {}
runner.session_store = MagicMock()
for key, value in attrs.items():
setattr(runner, key, value)
return runner
def _make_context(
*,
platform: Platform = Platform.DISCORD,
chat_id: str = "111222333",
chat_name: str = "general",
chat_type: str = "channel",
thread_id: str | None = "444555666",
parent_chat_id: str | None = "111222333",
chat_topic: str | None = "ops chatter",
user_name: str | None = "pix",
user_id: str | None = "9001",
guild_id: str | None = "777888999",
message_id: str | None = "1357",
shared_multi_user: bool = False,
connected: list[Platform] | None = None,
home_channels: dict | None = None,
) -> SessionContext:
source = SessionSource(
platform=platform,
chat_id=chat_id,
chat_name=chat_name,
chat_type=chat_type,
user_id=user_id,
user_name=user_name,
thread_id=thread_id,
chat_topic=chat_topic,
parent_chat_id=parent_chat_id,
scope_id=guild_id,
message_id=message_id,
)
connected = connected if connected is not None else [Platform.DISCORD, Platform.TELEGRAM]
if home_channels is None:
home_channels = {
Platform.DISCORD: HomeChannel(
platform=Platform.DISCORD, chat_id="111222333", name="general"
),
}
return SessionContext(
source=source,
connected_platforms=connected,
home_channels=home_channels,
shared_multi_user_session=shared_multi_user,
)
@pytest.fixture(autouse=True)
def _stable_discord_tools(monkeypatch):
"""Pin the config/env-dependent renderer gates so key<->render parity is
evaluated on the same footing in every environment."""
monkeypatch.setattr("gateway.session._discord_tools_loaded", lambda: True)
monkeypatch.setattr("gateway.session._slack_tools_loaded", lambda: False)
def _key(runner, context, redact_pii=False):
return runner._ephemeral_change_key(context, redact_pii) # noqa: SLF001
def _render(context, redact_pii=False):
return build_session_context_prompt(context, redact_pii=redact_pii)
# ---------------------------------------------------------------------------
# 1. Parity: key <-> render (the maintained invariant)
# ---------------------------------------------------------------------------
class TestEphemeralChangeKeyParity:
# Single-field mutations spanning every rendered input. For each:
# if the rendered bytes change, the key MUST change (staleness guard).
_MUTATIONS = [
("chat_name", dict(chat_name="renamed-thread")),
("chat_topic", dict(chat_topic="new topic")),
("chat_topic_cleared", dict(chat_topic=None)),
("thread_id", dict(thread_id="000111222")),
("thread_cleared", dict(thread_id=None, parent_chat_id=None)),
("chat_type", dict(chat_type="group")),
("user_name", dict(user_name="somebody-else")),
("user_name_cleared", dict(user_name=None)),
("user_id", dict(user_name=None, user_id="1234")),
("shared_multi_user", dict(shared_multi_user=True)),
("guild_id", dict(guild_id="123123123")),
("parent_chat_id", dict(parent_chat_id="999000111")),
("chat_id", dict(chat_id="999999999", parent_chat_id="999999999")),
("platform", dict(platform=Platform.TELEGRAM)),
("connected_platforms", dict(connected=[Platform.DISCORD])),
(
"home_channel_renamed",
dict(
home_channels={
Platform.DISCORD: HomeChannel(
platform=Platform.DISCORD, chat_id="111222333", name="ops-home"
)
}
),
),
(
"home_channel_added",
dict(
home_channels={
Platform.DISCORD: HomeChannel(
platform=Platform.DISCORD, chat_id="111222333", name="general"
),
Platform.TELEGRAM: HomeChannel(
platform=Platform.TELEGRAM, chat_id="tg1", name="tg-home"
),
}
),
),
("message_id_cleared", dict(message_id=None)),
]
def test_redact_pii_flip_changes_key(self):
# PII redaction only rewrites bytes on pii-safe platforms; the key
# must react wherever the render does.
runner = _make_runner()
ctx = _make_context(platform=Platform.TELEGRAM, thread_id=None, parent_chat_id=None)
assert _render(ctx, False) != _render(ctx, True)
assert _key(runner, ctx, False) != _key(runner, ctx, True)
def test_slack_note_byte_stable_across_turns_in_one_session(self):
"""Within one session (gate state constant), the Slack platform note
must be byte-stable turn over turn — the pin returns the identical
object, so the composed system prompt cannot drift mid-conversation."""
runner = _make_runner()
def _slack_ctx():
return _make_context(
platform=Platform.SLACK,
chat_id="C123",
thread_id=None,
parent_chat_id=None,
guild_id=None,
)
t1 = runner._pinned_session_context_prompt(_slack_ctx(), False, "sk-slack") # noqa: SLF001
t2 = runner._pinned_session_context_prompt(_slack_ctx(), False, "sk-slack") # noqa: SLF001
t3 = runner._pinned_session_context_prompt(_slack_ctx(), False, "sk-slack") # noqa: SLF001
assert t2 is t1 and t3 is t1
assert hashlib.sha256(t1.encode()).hexdigest() == hashlib.sha256(t3.encode()).hexdigest()
# ---------------------------------------------------------------------------
# 2. The pin: reuse verbatim on hit, exactly one legit bust on change
# ---------------------------------------------------------------------------
class TestSessionContextPin:
def test_pin_hit_returns_identical_object(self):
runner = _make_runner()
ctx = _make_context()
first = runner._pinned_session_context_prompt(ctx, False, "sk") # noqa: SLF001
second = runner._pinned_session_context_prompt(_make_context(), False, "sk") # noqa: SLF001
# Identity, not just equality: the pinned bytes are reused verbatim,
# immunizing against renderer nondeterminism.
assert second is first
# ---------------------------------------------------------------------------
# 3. Two-turn byte test: composed system prompt sha256 + codex cache key
# ---------------------------------------------------------------------------
def _compose(context_prompt: str) -> str:
"""Compose base + ephemeral exactly like conversation_loop does."""
base = "BASE IDENTITY PROMPT\n" + "x" * 8000
return (base + "\n\n" + context_prompt).strip()
class TestComposedPromptByteStability:
def test_turn2_equals_turn3_sha256(self):
runner = _make_runner()
name = "Fixing the flaky deploy"
t2 = _compose(
runner._pinned_session_context_prompt( # noqa: SLF001
_make_context(chat_name=name), False, "sk"
)
)
t3 = _compose(
runner._pinned_session_context_prompt( # noqa: SLF001
_make_context(chat_name=name), False, "sk"
)
)
assert hashlib.sha256(t2.encode()).hexdigest() == hashlib.sha256(t3.encode()).hexdigest()
# ---------------------------------------------------------------------------
# 4. Voice-channel sidecar note: only-when-changed
# ---------------------------------------------------------------------------
def _source():
return SessionSource(
platform=Platform.DISCORD, chat_id="c1", chat_type="channel", user_id="u1"
)
class _VcAdapter:
def __init__(self, value):
self.value = value
def get_voice_channel_context(self, guild_id):
return self.value
def _vc_runner(vc_value):
adapter = _VcAdapter(vc_value)
runner = _make_runner(adapters={Platform.DISCORD: adapter})
return runner, adapter
def _vc_event():
return SimpleNamespace(raw_message=SimpleNamespace(guild_id="777"))
class TestVoiceChannelSidecarNote:
def test_first_sighting_injects(self):
runner, _ = _vc_runner("**Voice:** dev-vc (2 members)")
note = runner._voice_channel_sidecar_note(_vc_event(), _source(), "sk") # noqa: SLF001
assert note == "[Voice channel now: **Voice:** dev-vc (2 members)]"
def test_unchanged_state_injects_nothing(self):
runner, _ = _vc_runner("**Voice:** dev-vc (2 members)")
assert runner._voice_channel_sidecar_note(_vc_event(), _source(), "sk") # noqa: SLF001
assert runner._voice_channel_sidecar_note(_vc_event(), _source(), "sk") is None # noqa: SLF001
def test_member_change_injects_again(self):
runner, adapter = _vc_runner("**Voice:** dev-vc (2 members)")
runner._voice_channel_sidecar_note(_vc_event(), _source(), "sk") # noqa: SLF001
adapter.value = "**Voice:** dev-vc (3 members)"
note = runner._voice_channel_sidecar_note(_vc_event(), _source(), "sk") # noqa: SLF001
assert note == "[Voice channel now: **Voice:** dev-vc (3 members)]"
def test_leaving_channel_injects_disconnect_note(self):
runner, adapter = _vc_runner("**Voice:** dev-vc (2 members)")
runner._voice_channel_sidecar_note(_vc_event(), _source(), "sk") # noqa: SLF001
adapter.value = ""
note = runner._voice_channel_sidecar_note(_vc_event(), _source(), "sk") # noqa: SLF001
assert note == "[Voice channel now: not connected to a voice channel]"
def test_never_in_channel_injects_nothing(self):
runner, _ = _vc_runner("")
assert runner._voice_channel_sidecar_note(_vc_event(), _source(), "sk") is None # noqa: SLF001
# ---------------------------------------------------------------------------
# 5. Sidecar note staging: one-shot per turn
# ---------------------------------------------------------------------------
class TestSidecarNoteStaging:
def test_set_then_consume_once(self):
runner = _make_runner()
runner._set_pending_turn_sidecar_notes("sk", ["[System note: reset]"]) # noqa: SLF001
assert runner._consume_pending_turn_sidecar_notes("sk") == ["[System note: reset]"] # noqa: SLF001
assert runner._consume_pending_turn_sidecar_notes("sk") == [] # noqa: SLF001
# ---------------------------------------------------------------------------
# 6. Connected platforms: stable order
# ---------------------------------------------------------------------------
class TestConnectedPlatformsOrder:
def test_sorted_regardless_of_insertion_order(self):
cfg_a = GatewayConfig(
platforms={
Platform.TELEGRAM: PlatformConfig(enabled=True, token="t"),
Platform.DISCORD: PlatformConfig(enabled=True, token="d"),
}
)
cfg_b = GatewayConfig(
platforms={
Platform.DISCORD: PlatformConfig(enabled=True, token="d"),
Platform.TELEGRAM: PlatformConfig(enabled=True, token="t"),
}
)
assert cfg_a.get_connected_platforms() == cfg_b.get_connected_platforms()
values = [p.value for p in cfg_a.get_connected_platforms()]
assert values == sorted(values)