From 34d20dacaf308757454228061b61b361bf2e39f7 Mon Sep 17 00:00:00 2001 From: KarthikAvinashFI Date: Fri, 9 Oct 2026 09:57:33 +0530 Subject: [PATCH] fix(harness): import fi.simulate on the callback adapter's main thread --- src/fi/alk/harness/callback_http_adapter.py | 5 +- tests/harness/test_callback_http_adapter.py | 73 +++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/harness/test_callback_http_adapter.py diff --git a/src/fi/alk/harness/callback_http_adapter.py b/src/fi/alk/harness/callback_http_adapter.py index 39c4fa820..e78fa969e 100644 --- a/src/fi/alk/harness/callback_http_adapter.py +++ b/src/fi/alk/harness/callback_http_adapter.py @@ -17,6 +17,9 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Any +# Main thread only: with livekit installed this import registers plugins. +from fi.simulate import AgentInput + def _load_callback() -> Any: target = os.environ["ALK_CALLBACK_ENTRYPOINT"] @@ -46,8 +49,6 @@ def _json_body(value: Any) -> dict[str, Any]: def _invoke(payload: dict[str, Any]) -> dict[str, Any]: - from fi.simulate import AgentInput - result = CALLBACK(AgentInput.model_validate(payload)) if inspect.isawaitable(result): result = asyncio.run(result) diff --git a/tests/harness/test_callback_http_adapter.py b/tests/harness/test_callback_http_adapter.py new file mode 100644 index 000000000..cb21eb3ae --- /dev/null +++ b/tests/harness/test_callback_http_adapter.py @@ -0,0 +1,73 @@ +import json +import os +import socket +import subprocess +import sys +import time +import urllib.request +from pathlib import Path + +import pytest + +ADAPTER = ( + Path(__file__).resolve().parents[2] / "src/fi/alk/harness/callback_http_adapter.py" +) + + +def _free_port() -> int: + with socket.socket() as probe: + probe.bind(("127.0.0.1", 0)) + return probe.getsockname()[1] + + +@pytest.fixture +def adapter(tmp_path): + (tmp_path / "chat_agent.py").write_text( + "def reply(agent_input):\n" + " return {'content': 'thread ' + agent_input.thread_id}\n", + encoding="utf-8", + ) + # Bundles embed the adapter's source and run it from the bundle, not from the package. + script = tmp_path / "adapter.py" + script.write_text(ADAPTER.read_text(encoding="utf-8"), encoding="utf-8") + port = _free_port() + process = subprocess.Popen( + [sys.executable, str(script)], + env={ + **os.environ, + "ALK_CALLBACK_ENTRYPOINT": "chat_agent:reply", + "PORT": str(port), + "PYTHONPATH": str(tmp_path), + }, + stderr=subprocess.PIPE, + ) + base = f"http://127.0.0.1:{port}" + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + try: + urllib.request.urlopen(f"{base}/health", timeout=1) + break + except OSError: + if process.poll() is not None: + pytest.fail(process.stderr.read().decode()) + time.sleep(0.2) + yield base + process.terminate() + process.wait(timeout=10) + + +def test_a_callback_answers_from_the_request_thread(adapter): + """The request thread is not the main thread; nothing imported there may require it.""" + body = json.dumps( + { + "thread_id": "t1", + "messages": [{"role": "user", "content": "hello"}], + "new_message": {"role": "user", "content": "hello"}, + } + ).encode() + request = urllib.request.Request( + f"{adapter}/invoke", data=body, headers={"Content-Type": "application/json"} + ) + with urllib.request.urlopen(request, timeout=30) as response: + assert response.status == 200 + assert json.loads(response.read())["content"] == "thread t1"