Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""Regression tests: profile HERMES_HOME override in ephemeral agent threads (#50233).
|
||||
|
||||
Why: normal prompt turns bind ``session['profile_home']`` via
|
||||
``set_hermes_home_override`` before ``run_conversation`` so the turn runs against
|
||||
the correct profile home. The two ephemeral RPC paths — ``prompt.background`` and
|
||||
``preview.restart`` — spawn a fresh ``AIAgent`` on a NEW thread, and the
|
||||
``HERMES_HOME`` ContextVar set on the session-create thread does NOT propagate to
|
||||
those threads. Without an explicit re-bind, a background/preview-restart turn under
|
||||
a non-default profile would run against the wrong home. This module locks in:
|
||||
|
||||
1. ``prompt.background`` re-binds ``profile_home`` for the ephemeral turn.
|
||||
2. ``preview.restart`` re-binds ``profile_home`` for the ephemeral turn AND does
|
||||
NOT close the ephemeral agent (a task-wide ``AIAgent.close()`` would kill the
|
||||
background server the restart just started — maintainer problem #1).
|
||||
3. Both paths RESTORE the override after the turn (reset token from set), exactly
|
||||
like the normal prompt turn, and skip the bind entirely when no profile is set.
|
||||
|
||||
How to test: run this module with pytest; each test drives the real RPC handler
|
||||
from ``tui_gateway.server._methods`` with ``threading.Thread`` patched to run the
|
||||
target inline, then asserts on the recorded override set/reset calls and the agent.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tui_gateway import server as srv
|
||||
|
||||
|
||||
PROFILE_HOME = "/home/user/.hermes/profiles/work"
|
||||
|
||||
|
||||
class _InlineThread:
|
||||
"""Drop-in for ``threading.Thread`` that runs the target synchronously.
|
||||
|
||||
Why: the ephemeral RPC handlers do their real work inside ``run()`` on a
|
||||
spawned thread; running it inline makes the override set/reset observable
|
||||
within the test without racing a real background thread.
|
||||
"""
|
||||
|
||||
def __init__(self, target=None, daemon=None, **_kwargs):
|
||||
self._target = target
|
||||
|
||||
def start(self):
|
||||
if self._target is not None:
|
||||
self._target()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_session():
|
||||
"""A minimal session carrying a non-default ``profile_home``."""
|
||||
agent = MagicMock()
|
||||
return {"agent": agent, "session_key": "sess_k", "profile_home": PROFILE_HOME}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def override_calls():
|
||||
"""Patch set/reset override + AIAgent + emit/context helpers; record calls.
|
||||
|
||||
Returns a dict with the mocks so each test can assert on set/reset ordering
|
||||
and on whether the ephemeral agent was closed.
|
||||
"""
|
||||
agent_instance = MagicMock()
|
||||
# run_conversation returns a plain dict like the real agent does.
|
||||
agent_instance.run_conversation.return_value = {"final_response": "done"}
|
||||
|
||||
with patch("tui_gateway.server.threading.Thread", _InlineThread), \
|
||||
patch("tui_gateway.server.set_hermes_home_override", return_value="TOK") as m_set, \
|
||||
patch("tui_gateway.server.reset_hermes_home_override") as m_reset, \
|
||||
patch("tui_gateway.server._background_agent_kwargs", return_value={}), \
|
||||
patch("tui_gateway.server._ephemeral_preview_agent_kwargs", return_value={}), \
|
||||
patch("tui_gateway.server._preview_restart_callbacks", return_value={}), \
|
||||
patch("tui_gateway.server._preview_restart_history", return_value=[]), \
|
||||
patch("tui_gateway.server._set_session_context", return_value=None), \
|
||||
patch("tui_gateway.server._clear_session_context"), \
|
||||
patch("tui_gateway.server._session_cwd", return_value="/tmp"), \
|
||||
patch("tui_gateway.server._emit"), \
|
||||
patch("run_agent.AIAgent", return_value=agent_instance) as m_agent:
|
||||
yield {
|
||||
"set": m_set,
|
||||
"reset": m_reset,
|
||||
"agent_cls": m_agent,
|
||||
"agent": agent_instance,
|
||||
}
|
||||
|
||||
|
||||
def _run(method_name, params, session):
|
||||
"""Invoke a registered RPC handler with ``_sess`` patched to our session."""
|
||||
handler = srv._methods[method_name]
|
||||
with patch("tui_gateway.server._sess", return_value=(session, None)):
|
||||
return handler("rid1", params)
|
||||
|
||||
|
||||
class TestBackgroundProfileOverride:
|
||||
def test_background_binds_and_restores_profile_home(self, fake_session, override_calls):
|
||||
"""prompt.background binds profile_home for the ephemeral turn and restores it."""
|
||||
_run("prompt.background", {"text": "hi", "session_id": "ui1"}, fake_session)
|
||||
|
||||
override_calls["set"].assert_called_once_with(PROFILE_HOME)
|
||||
override_calls["reset"].assert_called_once_with("TOK")
|
||||
override_calls["agent"].run_conversation.assert_called_once()
|
||||
|
||||
def test_background_no_profile_skips_override(self, override_calls):
|
||||
"""With no profile_home the background path never touches the override."""
|
||||
session = {"agent": MagicMock(), "session_key": "sess_k", "profile_home": None}
|
||||
_run("prompt.background", {"text": "hi", "session_id": "ui1"}, session)
|
||||
|
||||
override_calls["set"].assert_not_called()
|
||||
override_calls["reset"].assert_not_called()
|
||||
override_calls["agent"].run_conversation.assert_called_once()
|
||||
|
||||
def test_background_restores_override_on_error(self, fake_session, override_calls):
|
||||
"""A failing turn must still restore the override (finally-block parity)."""
|
||||
override_calls["agent"].run_conversation.side_effect = RuntimeError("boom")
|
||||
_run("prompt.background", {"text": "hi", "session_id": "ui1"}, fake_session)
|
||||
|
||||
override_calls["set"].assert_called_once_with(PROFILE_HOME)
|
||||
override_calls["reset"].assert_called_once_with("TOK")
|
||||
|
||||
|
||||
class TestPreviewRestartProfileOverride:
|
||||
def test_preview_binds_and_restores_profile_home(self, fake_session, override_calls):
|
||||
"""preview.restart binds profile_home for the ephemeral turn and restores it."""
|
||||
_run(
|
||||
"preview.restart",
|
||||
{"url": "http://localhost:5173", "cwd": "", "session_id": "ui1"},
|
||||
fake_session,
|
||||
)
|
||||
|
||||
override_calls["set"].assert_called_once_with(PROFILE_HOME)
|
||||
override_calls["reset"].assert_called_once_with("TOK")
|
||||
override_calls["agent"].run_conversation.assert_called_once()
|
||||
|
||||
def test_preview_does_not_close_agent(self, fake_session, override_calls):
|
||||
"""The restarted preview server must survive: the ephemeral agent is NOT
|
||||
closed via task-wide process cleanup (maintainer problem #1)."""
|
||||
_run(
|
||||
"preview.restart",
|
||||
{"url": "http://localhost:5173", "cwd": "", "session_id": "ui1"},
|
||||
fake_session,
|
||||
)
|
||||
|
||||
# A task-wide AIAgent.close() would kill every process for this task_id,
|
||||
# tearing down the very background server the restart just launched.
|
||||
override_calls["agent"].close.assert_not_called()
|
||||
|
||||
def test_preview_no_profile_skips_override(self, override_calls):
|
||||
"""With no profile_home the preview path never touches the override."""
|
||||
session = {"agent": MagicMock(), "session_key": "sess_k", "profile_home": None}
|
||||
_run(
|
||||
"preview.restart",
|
||||
{"url": "http://localhost:5173", "cwd": "", "session_id": "ui1"},
|
||||
session,
|
||||
)
|
||||
|
||||
override_calls["set"].assert_not_called()
|
||||
override_calls["reset"].assert_not_called()
|
||||
override_calls["agent"].close.assert_not_called()
|
||||
Reference in New Issue
Block a user