#!/usr/bin/env python3 import json import os import socket import sys import tempfile import threading import time import unittest from pathlib import Path from unittest import mock PROJECT_ROOT = Path(__file__).resolve().parents[1] SDK_SRC = PROJECT_ROOT / "packages" / "jwebot-sdk" / "src" KERNEL_SRC = PROJECT_ROOT / "packages" / "jwebot-kernel" / "src" sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)] from jwebot_kernel import ( # noqa: E402 ExtensionHostClient, HostClientError, HostTimeout, RuntimeTelemetry, UnixExtensionHostClient, ) class HostClientTests(unittest.TestCase): def helper(self, source: str): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) root = Path(temporary.name) release = root / "release" release.mkdir() script = root / "host.py" script.write_text(source, encoding="utf-8") return root, release, script def test_client_uses_minimal_environment_and_parses_response(self): root, release, script = self.helper( "\n".join( ( "import json, os, sys", "request = json.loads(sys.stdin.readline())", "print(json.dumps({", " 'status': 'ok',", " 'event_id': request['event_id'],", " 'secret_present': 'JWEBOT_TEST_SECRET' in os.environ,", " 'dont_write_bytecode': sys.dont_write_bytecode,", "}), flush=True)", ) ) ) del root os.environ["JWEBOT_TEST_SECRET"] = "must-not-cross" self.addCleanup(os.environ.pop, "JWEBOT_TEST_SECRET", None) response = ExtensionHostClient( command=(sys.executable, str(script)), ).invoke(release, {"event_id": "event-1"}) self.assertEqual(response["event_id"], "event-1") self.assertFalse(response["secret_present"]) self.assertTrue(response["dont_write_bytecode"]) def test_client_rejects_invalid_json(self): _, release, script = self.helper("print('not-json')\n") with self.assertRaisesRegex(HostClientError, "invalid JSON"): ExtensionHostClient( command=(sys.executable, str(script)), ).invoke(release, {"event_id": "event-1"}) def test_client_rejects_host_error_status(self): _, release, script = self.helper( "print('{\"status\":\"error\",\"error\":\"ExtensionFailed\"}')\n" ) with self.assertRaisesRegex(HostClientError, "returned an error"): ExtensionHostClient( command=(sys.executable, str(script)), ).invoke(release, {"event_id": "event-1"}) def test_client_enforces_request_limit(self): _, release, script = self.helper("print('{\"status\":\"ok\"}')\n") with self.assertRaisesRegex(HostClientError, "size limit"): ExtensionHostClient( command=(sys.executable, str(script)), ).invoke(release, {"value": "x" * (256 * 1024)}) def test_timeout_kills_the_host_process_group(self): root, release, script = self.helper( "\n".join( ( "import subprocess, sys, time", "marker = sys.argv[1]", "subprocess.Popen([", " sys.executable, '-c',", " 'import pathlib,time,sys; time.sleep(0.3); '", " 'pathlib.Path(sys.argv[1]).write_text(\"escaped\")',", " marker,", "])", "time.sleep(10)", ) ) ) marker = root / "escaped.txt" client = ExtensionHostClient( command=(sys.executable, str(script), str(marker)), timeout_seconds=0.05, ) with self.assertRaises(HostTimeout): client.invoke(release, {"event_id": "event-1"}) time.sleep(0.5) self.assertFalse(marker.exists()) def test_client_services_one_bounded_broker_request(self): _, release, script = self.helper( "\n".join( ( "import json, sys", "request = json.loads(sys.stdin.readline())", "print(json.dumps({", " 'status': 'service_request',", " 'call_id': '1',", " 'profile': 'coin',", " 'operation': 'execute',", " 'payload': {'command': '/翻硬币'},", "}), flush=True)", "service = json.loads(sys.stdin.readline())", "print(json.dumps({", " 'status': 'ok',", " 'plugin_id': 'coin',", " 'request_op': request['op'],", " 'text': service['result']['text'],", "}), flush=True)", ) ) ) calls = [] observed = [] def service(profile, operation, payload): calls.append((profile, operation, payload)) return {"text": "硬币落地:正面喵"} response = ExtensionHostClient( command=(sys.executable, str(script)), plugin_id="coin", telemetry=RuntimeTelemetry( component="host-client", observer=observed.append, logger=mock.Mock(), ), ).invoke( release, {"op": "invoke_command"}, service_handler=service, ) self.assertEqual(response["text"], "硬币落地:正面喵") self.assertEqual( calls, [ ( "coin", "execute", {"command": "/翻硬币"}, ) ], ) names = [value["name"] for value in observed] self.assertIn("host.process.spawn", names) self.assertIn("host.service.request", names) self.assertIn("host.service.complete", names) self.assertIn("host.invoke", names) def test_service_handler_cannot_exceed_host_deadline(self): _, release, script = self.helper( "\n".join( ( "import json, sys", "json.loads(sys.stdin.readline())", "print(json.dumps({", " 'status': 'service_request',", " 'call_id': '1',", " 'profile': 'coin',", " 'operation': 'execute',", " 'payload': {},", "}), flush=True)", "sys.stdin.readline()", ) ) ) def slow_service(profile, operation, payload): del profile, operation, payload time.sleep(0.65) return {"text": "late"} with self.assertRaisesRegex(HostTimeout, "service exceeded"): ExtensionHostClient( command=(sys.executable, str(script)), timeout_seconds=0.5, ).invoke( release, {"op": "invoke_command"}, service_handler=slow_service, ) class UnixHostClientTests(unittest.TestCase): def server(self, response: bytes, *, delay: float = 0.0): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) path = Path(temporary.name) / "host.sock" listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) listener.bind(str(path)) listener.listen(1) received: list[bytes] = [] def serve(): try: connection, _ = listener.accept() with connection: stream = connection.makefile("rwb", buffering=0) received.append(stream.readline(256 * 1024 + 1)) if delay: time.sleep(delay) try: stream.write(response) except OSError: pass stream.close() finally: listener.close() thread = threading.Thread(target=serve, daemon=True) thread.start() self.addCleanup(thread.join, 1) return path, received def test_unix_client_round_trip(self): path, received = self.server( b'{"status":"ok","plugin_id":"hello-command"}\n' ) response = UnixExtensionHostClient(path).invoke( {"op": "invoke_command", "command": "/hello"} ) self.assertEqual(response["plugin_id"], "hello-command") self.assertEqual( json.loads(received[0]), {"command": "/hello", "op": "invoke_command"}, ) def test_unix_client_timeout_is_bounded(self): path, _ = self.server( b'{"status":"ok"}\n', delay=0.3, ) with self.assertRaises(HostTimeout): UnixExtensionHostClient( path, timeout_seconds=0.05, ).invoke({"op": "invoke_command"}) def test_unix_client_rejects_error_response(self): path, _ = self.server( b'{"status":"error","error":"ExtensionExecutionError"}\n' ) with self.assertRaisesRegex(HostClientError, "returned an error"): UnixExtensionHostClient(path).invoke({"op": "invoke_command"}) def test_unix_client_fails_closed_when_socket_is_missing(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "missing.sock" with self.assertRaisesRegex(HostClientError, "unavailable"): UnixExtensionHostClient(path).invoke( {"op": "invoke_command"} ) def test_unix_client_services_broker_request(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) path = Path(temporary.name) / "host.sock" listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) listener.bind(str(path)) listener.listen(1) received = [] def serve(): try: connection, _ = listener.accept() with connection: stream = connection.makefile("rwb", buffering=0) received.append(json.loads(stream.readline())) stream.write( b'{"status":"service_request","call_id":"1",' b'"profile":"coin","operation":"execute",' b'"payload":{"command":"/\\u7ffb\\u786c\\u5e01"}}\n' ) received.append(json.loads(stream.readline())) stream.write( b'{"status":"ok","plugin_id":"coin"}\n' ) stream.close() finally: listener.close() thread = threading.Thread(target=serve, daemon=True) thread.start() self.addCleanup(thread.join, 1) calls = [] response = UnixExtensionHostClient(path).invoke( {"op": "invoke_command", "command": "/翻硬币"}, service_handler=lambda profile, operation, payload: ( calls.append((profile, operation, payload)) or {"text": "ok"} ), ) self.assertEqual(response["plugin_id"], "coin") self.assertEqual(received[1]["op"], "service_result") self.assertEqual(calls[0][0:2], ("coin", "execute")) if __name__ == "__main__": unittest.main()