clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / tinyagent / hermes / tests / test_client.py
test_client.py 4.9K
1"""TinyRPC process handling against a fake helper; no Hermes or network needed."""23import asyncio4import json5import sys6from pathlib import Path78import pytest910sys.path.insert(0, str(Path(__file__).parents[1]))1112import client as module13from client import TinyRPC141516class FakeStdin:17 def __init__(self, process):18 self.process = process19 self.lines = []2021 def write(self, data):22 self.lines.append(json.loads(data))23 self.process.on_request(self.lines[-1])2425 async def drain(self):26 pass272829class FakeProcess:30 """Answers every request immediately and dies on demand."""3132 def __init__(self):33 self.stdout = asyncio.StreamReader()34 self.stdin = FakeStdin(self)35 self.returncode = None36 self.terminated = False3738 def on_request(self, request):39 if request["method"] == "hang":40 return41 reply = {"id": request["id"], "result": {"method": request["method"]}}42 self.stdout.feed_data((json.dumps(reply) + "\n").encode())43 if request["method"] == "subscribe":44 self.stdout.feed_data(json.dumps({"event": "connected", "subscription": request["params"]["subscription"]}).encode() + b"\n")4546 def die(self):47 self.returncode = 148 self.stdout.feed_eof()4950 def terminate(self):51 self.terminated = True52 self.returncode = -1553 self.stdout.feed_eof()5455 async def wait(self):56 return self.returncode575859@pytest.fixture60def spawn(monkeypatch):61 spawned = []6263 async def create(*args, **kwargs):64 process = FakeProcess()65 spawned.append((args, kwargs, process))66 return process6768 monkeypatch.setattr(module.asyncio, "create_subprocess_exec", create)69 return spawned707172def test_helper_gets_a_scoped_environment(spawn, monkeypatch):73 monkeypatch.setenv("TINY_PRIVATE_KEY", "secret")74 monkeypatch.setenv("HTTPS_PROXY", "http://proxy")75 monkeypatch.setenv("no_proxy", "localhost")76 monkeypatch.setenv("OPENAI_API_KEY", "leak")77 monkeypatch.setenv("HERMES_HOME", "/somewhere")7879 async def run():80 rpc = TinyRPC("http://relay", cli_path="/bin/tinyagent")81 await rpc.connect()82 await rpc.close()8384 asyncio.run(run())85 args, kwargs, _process = spawn[0]86 assert args[:2] == ("/bin/tinyagent", "rpc")87 env = kwargs["env"]88 assert env["TINY_PRIVATE_KEY"] == "secret"89 assert env["HTTPS_PROXY"] == "http://proxy" and env["no_proxy"] == "localhost"90 assert "OPENAI_API_KEY" not in env and "HERMES_HOME" not in env91 assert set(env) <= {"PATH", "HOME", "TMPDIR", "TINY_PRIVATE_KEY", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",92 "http_proxy", "https_proxy", "no_proxy"}939495def test_helper_death_fails_pending_and_reports_once(spawn):96 exits = []9798 async def run():99 rpc = TinyRPC("http://relay", on_exit=exits.append)100 assert (await rpc.call("identity")) == {"method": "identity"}101 process = spawn[0][2]102 hanging = asyncio.ensure_future(rpc.call("hang"))103 await asyncio.sleep(0)104 process.die()105 with pytest.raises(RuntimeError, match="tiny rpc exited"):106 await hanging107 await asyncio.sleep(0)108 assert exits == [rpc]109 assert not rpc.alive110 # No silent respawn: the owner rebuilds the session instead.111 with pytest.raises(RuntimeError, match="tiny rpc exited"):112 await rpc.call("identity")113 assert len(spawn) == 1114 await rpc.close()115 assert exits == [rpc]116117 asyncio.run(run())118119120def test_close_does_not_report_an_exit(spawn):121 exits = []122123 async def run():124 rpc = TinyRPC("http://relay", on_exit=exits.append)125 await rpc.connect()126 await rpc.close()127 await asyncio.sleep(0)128129 asyncio.run(run())130 assert exits == []131 assert spawn[0][2].terminated132133134def test_async_exit_handler_is_awaited(spawn):135 seen = []136137 async def run():138 done = asyncio.Event()139140 async def on_exit(rpc):141 seen.append(rpc)142 done.set()143144 rpc = TinyRPC("http://relay", on_exit=on_exit)145 await rpc.connect()146 spawn[0][2].die()147 await asyncio.wait_for(done.wait(), 1)148149 asyncio.run(run())150 assert len(seen) == 1151152153def test_dead_helper_keeps_in_flight_callbacks(spawn):154 async def run():155 finished = asyncio.Event()156157 async def callback(event):158 await asyncio.sleep(0.01)159 finished.set()160161 rpc = TinyRPC("http://relay")162 await rpc.subscribe("s", {"kinds": [9]}, callback)163 process = spawn[0][2]164 process.stdout.feed_data(json.dumps({"event": "event", "subscription": "s", "data": {"id": "x"}}).encode() + b"\n")165 await asyncio.sleep(0)166 await asyncio.sleep(0)167 process.die()168 await asyncio.sleep(0)169 await rpc.close()170 await asyncio.wait_for(finished.wait(), 1)171172 asyncio.run(run())