test_http_proxy_worker.py raw

   1  """Worker readiness and relay-proxy verification tests."""
   2  
   3  import json
   4  import time
   5  
   6  import pytest
   7  import websocket
   8  
   9  from nostr_helpers import TEST_SECKEY, make_event
  10  
  11  
  12  class TestWorkerReadiness:
  13      def test_supervisor_launches_workers_to_ready(self, relay, browser):
  14          browser.get(relay["url"])
  15          deadline = time.time() + 25
  16          while time.time() < deadline:
  17              state = browser.execute_script(
  18                  "return JSON.stringify({wasm: document.body.getAttribute('data-wasm-ready')||'',"
  19                  " rp: !!window.__rpReady, rpErr: window.__rpError||null})"
  20              )
  21              data = json.loads(state)
  22              if data.get("rp") and data.get("wasm") == "1":
  23                  break
  24              time.sleep(0.3)
  25          assert data.get("wasm") == "1", f"app worker did not reach ready: {state}"
  26          assert data.get("rp") is True, f"relay-proxy worker did not reach ready: {state}"
  27          assert not data.get("rpErr"), f"relay-proxy worker reported error: {data.get('rpErr')}"
  28  
  29  
  30  class TestRelayProxyWorker:
  31      def test_relay_proxy_handles_req_returns_eose(self, relay, browser):
  32          """Construct a fresh relay-proxy worker, send REQ with empty filter,
  33          verify the worker queries IDB and returns EOSE."""
  34          browser.get(relay["url"])
  35          # Wait for page boot to settle.
  36          deadline = time.time() + 25
  37          while time.time() < deadline:
  38              ready = browser.execute_script(
  39                  "return document.body.getAttribute('data-wasm-ready') || ''"
  40              )
  41              if ready == "1":
  42                  break
  43              time.sleep(0.3)
  44          time.sleep(2)
  45  
  46          result_str = browser.execute_async_script(
  47              """
  48              const done = arguments[arguments.length-1];
  49              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
  50              window.__rpTestWorker = w;
  51              const log = [];
  52              let booted = false, finished = false;
  53              const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
  54              w.onmessage = (e) => {
  55                  if (typeof e.data !== 'string') return;
  56                  log.push(e.data.slice(0, 120));
  57                  if (e.data.startsWith('["READY"]') && !booted) {
  58                      booted = true;
  59                      w.postMessage(JSON.stringify(['REQ', 'sub-test', {kinds: [99999], limit: 1}]));
  60                  } else if (e.data.startsWith('["S_QUERY"')) {
  61                      // Mock store: return empty result for the query.
  62                      try {
  63                          const parsed = JSON.parse(e.data);
  64                          const reqID = parsed[1];
  65                          w.postMessage(JSON.stringify(['SR_QUERY', reqID, '[]']));
  66                      } catch (_) {}
  67                  } else if (e.data.startsWith('["EOSE","sub-test"]')) {
  68                      finish({ ok: true, log });
  69                  } else if (e.data.startsWith('["__ERROR"')) {
  70                      finish({ ok: false, error: e.data, log });
  71                  }
  72              };
  73              w.onerror = (e) => finish({ ok: false, error: e.message, log });
  74              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
  75              setTimeout(() => finish({ ok: false, error: 'timeout', log }), 15000);
  76              """
  77          )
  78          result = json.loads(result_str)
  79          assert result.get("ok"), f"REQ -> EOSE round-trip failed: {result}"
  80          # Clean up.
  81          browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
  82  
  83      def test_relay_proxy_publishes_via_publish_to(self, relay, browser):
  84          """Send a signed EVENT via PUBLISH_TO; verify the test relay
  85          received it (independent WS sub)."""
  86          unique_content = f"phase2-2-publish-to-{int(time.time() * 1000)}"
  87          ev = make_event(TEST_SECKEY, unique_content, kind=1)
  88  
  89          browser.get(relay["url"])
  90          deadline = time.time() + 25
  91          while time.time() < deadline:
  92              ready = browser.execute_script(
  93                  "return document.body.getAttribute('data-wasm-ready') || ''"
  94              )
  95              if ready == "1":
  96                  break
  97              time.sleep(0.3)
  98          time.sleep(2)
  99  
 100          # Subscribe to the test relay BEFORE publishing so we can observe
 101          # the event arrive.
 102          ws_obs = websocket.create_connection(relay["ws"], timeout=5)
 103          try:
 104              ws_obs.send(json.dumps(["REQ", "obs", {"kinds": [1], "limit": 0}]))
 105              # Drain initial EOSE.
 106              deadline = time.time() + 3
 107              while time.time() < deadline:
 108                  try:
 109                      ws_obs.settimeout(0.5)
 110                      msg = ws_obs.recv()
 111                      if json.loads(msg)[0] == "EOSE":
 112                          break
 113                  except Exception:
 114                      break
 115  
 116              # Drive the worker to PUBLISH_TO.
 117              result_str = browser.execute_async_script(
 118                  """
 119                  const done = arguments[arguments.length-1];
 120                  const wsURL = arguments[0];
 121                  const eventJSON = arguments[1];
 122                  const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 123                  window.__rpTestWorker = w;
 124                  const log = [];
 125                  let booted = false, finished = false;
 126                  const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
 127                  w.onmessage = (e) => {
 128                      if (typeof e.data !== 'string') return;
 129                      log.push(e.data.slice(0, 160));
 130                      if (e.data.startsWith('["READY"]') && !booted) {
 131                          booted = true;
 132                          const msg = '["PUBLISH_TO",' + eventJSON + ',["' + wsURL + '"]]';
 133                          w.postMessage(msg);
 134                      } else if (e.data.startsWith('["OK"')) {
 135                          finish({ ok: true, msg: e.data, log });
 136                      } else if (e.data.startsWith('["__ERROR"')) {
 137                          finish({ ok: false, error: e.data, log });
 138                      }
 139                  };
 140                  w.onerror = (e) => finish({ ok: false, error: e.message, log });
 141                  w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 142                  setTimeout(() => finish({ ok: false, error: 'timeout', log }), 12000);
 143                  """,
 144                  relay["ws"],
 145                  json.dumps(ev),
 146              )
 147              result = json.loads(result_str)
 148              assert result.get("ok"), f"PUBLISH_TO didn't get OK: {result}"
 149  
 150              # Now observe the test relay forwarding the new event on our sub.
 151              ws_obs.settimeout(8)
 152              saw_event = False
 153              deadline = time.time() + 8
 154              while time.time() < deadline:
 155                  try:
 156                      msg = ws_obs.recv()
 157                      parsed = json.loads(msg)
 158                      if parsed[0] == "EVENT" and parsed[1] == "obs" and parsed[2].get("content") == unique_content:
 159                          saw_event = True
 160                          break
 161                  except Exception:
 162                      break
 163              assert saw_event, "test relay did not receive the published event"
 164          finally:
 165              ws_obs.close()
 166              browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
 167  
 168      def test_relay_proxy_event_uses_set_write_relays(self, relay, browser):
 169          """SET_WRITE_RELAYS configures the worker's default relay list;
 170          a subsequent EVENT (no explicit list) publishes to that list."""
 171          unique_content = f"phase2-3-default-relays-{int(time.time() * 1000)}"
 172          ev = make_event(TEST_SECKEY, unique_content, kind=1)
 173  
 174          browser.get(relay["url"])
 175          deadline = time.time() + 25
 176          while time.time() < deadline:
 177              ready = browser.execute_script(
 178                  "return document.body.getAttribute('data-wasm-ready') || ''"
 179              )
 180              if ready == "1":
 181                  break
 182              time.sleep(0.3)
 183          time.sleep(2)
 184  
 185          ws_obs = websocket.create_connection(relay["ws"], timeout=5)
 186          try:
 187              ws_obs.send(json.dumps(["REQ", "obs2", {"kinds": [1], "limit": 0}]))
 188              deadline = time.time() + 3
 189              while time.time() < deadline:
 190                  try:
 191                      ws_obs.settimeout(0.5)
 192                      msg = ws_obs.recv()
 193                      if json.loads(msg)[0] == "EOSE":
 194                          break
 195                  except Exception:
 196                      break
 197  
 198              result_str = browser.execute_async_script(
 199                  """
 200                  const done = arguments[arguments.length-1];
 201                  const wsURL = arguments[0];
 202                  const eventJSON = arguments[1];
 203                  const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 204                  window.__rpTestWorker = w;
 205                  const log = [];
 206                  let booted = false, finished = false;
 207                  const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
 208                  w.onmessage = (e) => {
 209                      if (typeof e.data !== 'string') return;
 210                      log.push(e.data.slice(0, 160));
 211                      if (e.data.startsWith('["READY"]') && !booted) {
 212                          booted = true;
 213                          // Configure write relays, then publish (no explicit URL list).
 214                          w.postMessage(JSON.stringify(['SET_WRITE_RELAYS', [wsURL]]));
 215                          w.postMessage('["EVENT",' + eventJSON + ']');
 216                      } else if (e.data.startsWith('["OK"')) {
 217                          finish({ ok: true, msg: e.data, log });
 218                      } else if (e.data.startsWith('["__ERROR"')) {
 219                          finish({ ok: false, error: e.data, log });
 220                      }
 221                  };
 222                  w.onerror = (e) => finish({ ok: false, error: e.message, log });
 223                  w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 224                  setTimeout(() => finish({ ok: false, error: 'timeout', log }), 12000);
 225                  """,
 226                  relay["ws"],
 227                  json.dumps(ev),
 228              )
 229              result = json.loads(result_str)
 230              assert result.get("ok"), f"EVENT default-relays didn't OK: {result}"
 231  
 232              ws_obs.settimeout(8)
 233              saw_event = False
 234              deadline = time.time() + 8
 235              while time.time() < deadline:
 236                  try:
 237                      msg = ws_obs.recv()
 238                      parsed = json.loads(msg)
 239                      if parsed[0] == "EVENT" and parsed[1] == "obs2" and parsed[2].get("content") == unique_content:
 240                          saw_event = True
 241                          break
 242                  except Exception:
 243                      break
 244              assert saw_event, "test relay did not receive event published via writeRelays"
 245          finally:
 246              ws_obs.close()
 247              browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
 248  
 249      @pytest.mark.skip(reason="DM_LIST is handled by the DM worker, not relay-proxy; needs full worker topology")
 250      def test_relay_proxy_dm_list_returns_array(self, relay, browser):
 251          """DM_LIST queries IDB conversation list. Empty database => empty array."""
 252          browser.get(relay["url"])
 253          deadline = time.time() + 25
 254          while time.time() < deadline:
 255              ready = browser.execute_script(
 256                  "return document.body.getAttribute('data-wasm-ready') || ''"
 257              )
 258              if ready == "1":
 259                  break
 260              time.sleep(0.3)
 261          time.sleep(2)
 262  
 263          browser.set_script_timeout(15)
 264          result_str = browser.execute_async_script(
 265              """
 266              const done = arguments[arguments.length-1];
 267              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 268              window.__rpTestWorker = w;
 269              const log = [];
 270              let booted = false, finished = false;
 271              const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
 272              w.onmessage = (e) => {
 273                  if (typeof e.data !== 'string') return;
 274                  log.push(e.data.slice(0, 160));
 275                  if (e.data.startsWith('["READY"]') && !booted) {
 276                      booted = true;
 277                      w.postMessage('["DM_LIST"]');
 278                  } else if (e.data.startsWith('["DM_LIST",')) {
 279                      finish({ ok: true, msg: e.data, log });
 280                  } else if (e.data.startsWith('["__ERROR"')) {
 281                      finish({ ok: false, error: e.data, log });
 282                  }
 283              };
 284              w.onerror = (e) => finish({ ok: false, error: e.message, log });
 285              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 286              setTimeout(() => finish({ ok: false, error: 'timeout', log }), 12000);
 287              """
 288          )
 289          result = json.loads(result_str)
 290          assert result.get("ok"), f"DM_LIST failed: {result}"
 291          # Format is ["DM_LIST", <json-array>]; just confirm the array exists.
 292          assert "[]" in result["msg"] or "[{" in result["msg"], f"unexpected DM_LIST shape: {result['msg']}"
 293          browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
 294  
 295      def test_relay_proxy_close_stops_event_flow(self, relay, browser):
 296          """PROXY → first event arrives. CLOSE the sub. Subsequent
 297          publishes to the same relay must NOT produce EVENT messages on
 298          the closed subID."""
 299          browser.get(relay["url"])
 300          deadline = time.time() + 25
 301          while time.time() < deadline:
 302              ready = browser.execute_script(
 303                  "return document.body.getAttribute('data-wasm-ready') || ''"
 304              )
 305              if ready == "1":
 306                  break
 307              time.sleep(0.3)
 308          time.sleep(2)
 309  
 310          # Seed initial event before subscribing.
 311          first_content = f"phase2-8-close-first-{int(time.time() * 1000)}"
 312          first_ev = make_event(TEST_SECKEY, first_content, kind=1)
 313          ws_seed = websocket.create_connection(relay["ws"], timeout=5)
 314          try:
 315              ws_seed.send(json.dumps(["EVENT", first_ev]))
 316              ws_seed.recv()  # OK
 317          finally:
 318              ws_seed.close()
 319  
 320          # Subscribe via PROXY, wait for first event, CLOSE, then publish a
 321          # second event and verify the worker does not forward it.
 322          late_content = f"phase2-8-close-late-{int(time.time() * 1000)}"
 323          late_ev = make_event(TEST_SECKEY, late_content, kind=1)
 324          result_str = browser.execute_async_script(
 325              """
 326              const done = arguments[arguments.length-1];
 327              const wsURL = arguments[0];
 328              const expectedFirst = arguments[1];
 329              const expectedLate = arguments[2];
 330              const lateEvent = arguments[3];
 331              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 332              window.__rpTestWorker = w;
 333              const log = [];
 334              const seenLate = [];
 335              let booted = false, sawFirst = false, closed = false, finished = false;
 336              const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
 337              w.onmessage = (e) => {
 338                  if (typeof e.data !== 'string') return;
 339                  log.push(e.data.slice(0, 160));
 340                  if (e.data.startsWith('["READY"]') && !booted) {
 341                      booted = true;
 342                      w.postMessage(JSON.stringify(['PROXY', 'sub-close', {kinds: [1], limit: 50}, [wsURL]]));
 343                  } else if (!closed && e.data.startsWith('["EVENT","sub-close"') && e.data.indexOf(expectedFirst) >= 0) {
 344                      sawFirst = true;
 345                      closed = true;
 346                      w.postMessage(JSON.stringify(['CLOSE', 'sub-close']));
 347                      // Publish a SECOND event via a separate WS that the test
 348                      // controls; we use a fetch to the relay's HTTP doesn't help, so
 349                      // we open a WS in the page itself and publish.
 350                      const ws2 = new WebSocket(wsURL);
 351                      ws2.onopen = () => { ws2.send(JSON.stringify(['EVENT', JSON.parse(lateEvent)])); };
 352                      ws2.onmessage = (m) => {
 353                          // Drain OK, then close after 3s observation window.
 354                          setTimeout(() => { ws2.close(); finish({ ok: true, sawFirst, seenLate, log }); }, 3000);
 355                      };
 356                  } else if (closed && e.data.startsWith('["EVENT","sub-close"') && e.data.indexOf(expectedLate) >= 0) {
 357                      // BAD: worker forwarded a late event to a closed sub.
 358                      seenLate.push(e.data);
 359                  } else if (e.data.startsWith('["__ERROR"')) {
 360                      finish({ ok: false, error: e.data, log });
 361                  }
 362              };
 363              w.onerror = (e) => finish({ ok: false, error: e.message, log });
 364              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 365              setTimeout(() => finish({ ok: false, error: 'timeout', sawFirst, seenLate, log }), 20000);
 366              """,
 367              relay["ws"], first_content, late_content, json.dumps(late_ev),
 368          )
 369          result = json.loads(result_str)
 370          assert result.get("ok"), f"close test failed: {result}"
 371          assert result.get("sawFirst"), f"first event never arrived: {result}"
 372          assert not result.get("seenLate"), f"worker forwarded late event after CLOSE: {result.get('seenLate')}"
 373          browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
 374  
 375      def test_relay_proxy_dedupes_duplicate_relay_urls(self, relay, browser):
 376          """PROXY with the same URL listed 3x should produce a single
 377          connection and a single delivery per event."""
 378          unique_content = f"phase2-8-dedup-{int(time.time() * 1000)}"
 379          ev = make_event(TEST_SECKEY, unique_content, kind=1)
 380          ws_seed = websocket.create_connection(relay["ws"], timeout=5)
 381          try:
 382              ws_seed.send(json.dumps(["EVENT", ev]))
 383              ws_seed.recv()
 384          finally:
 385              ws_seed.close()
 386  
 387          browser.get(relay["url"])
 388          deadline = time.time() + 25
 389          while time.time() < deadline:
 390              ready = browser.execute_script(
 391                  "return document.body.getAttribute('data-wasm-ready') || ''"
 392              )
 393              if ready == "1":
 394                  break
 395              time.sleep(0.3)
 396          time.sleep(2)
 397  
 398          result_str = browser.execute_async_script(
 399              """
 400              const done = arguments[arguments.length-1];
 401              const wsURL = arguments[0];
 402              const expected = arguments[1];
 403              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 404              window.__rpTestWorker = w;
 405              const log = [];
 406              const matchingDeliveries = [];
 407              let booted = false, finished = false;
 408              const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
 409              w.onmessage = (e) => {
 410                  if (typeof e.data !== 'string') return;
 411                  log.push(e.data.slice(0, 160));
 412                  if (e.data.startsWith('["READY"]') && !booted) {
 413                      booted = true;
 414                      // Same URL three times in the relay list.
 415                      w.postMessage(JSON.stringify(['PROXY', 'sub-dedup', {kinds: [1], limit: 50}, [wsURL, wsURL, wsURL]]));
 416                  } else if (e.data.startsWith('["EVENT","sub-dedup"') && e.data.indexOf(expected) >= 0) {
 417                      matchingDeliveries.push(e.data);
 418                      // Wait 3s after first delivery to see if duplicates arrive.
 419                      if (matchingDeliveries.length === 1) {
 420                          setTimeout(() => finish({ ok: true, matchingDeliveries, log }), 3000);
 421                      }
 422                  } else if (e.data.startsWith('["__ERROR"')) {
 423                      finish({ ok: false, error: e.data, log });
 424                  }
 425              };
 426              w.onerror = (e) => finish({ ok: false, error: e.message, log });
 427              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 428              setTimeout(() => finish({ ok: false, error: 'timeout', matchingDeliveries, log }), 12000);
 429              """,
 430              relay["ws"], unique_content,
 431          )
 432          result = json.loads(result_str)
 433          assert result.get("ok"), f"dedup test failed: {result}"
 434          # Exactly one delivery for the deduped relay set.
 435          assert len(result["matchingDeliveries"]) == 1, f"expected 1 delivery, got {len(result['matchingDeliveries'])}: {result['matchingDeliveries']}"
 436          browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
 437  
 438      def test_relay_proxy_eose_timer_fires_with_no_relays(self, relay, browser):
 439          """PROXY with empty relay list. EOSE should fire after the timer
 440          expires (test-overridden to 500ms via SET_PROXY_EOSE_MS)."""
 441          browser.get(relay["url"])
 442          deadline = time.time() + 25
 443          while time.time() < deadline:
 444              ready = browser.execute_script(
 445                  "return document.body.getAttribute('data-wasm-ready') || ''"
 446              )
 447              if ready == "1":
 448                  break
 449              time.sleep(0.3)
 450          time.sleep(2)
 451  
 452          result_str = browser.execute_async_script(
 453              """
 454              const done = arguments[arguments.length-1];
 455              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 456              window.__rpTestWorker = w;
 457              const log = [];
 458              let booted = false, finished = false, sentReq = false;
 459              let reqStart = 0;
 460              const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
 461              w.onmessage = (e) => {
 462                  if (typeof e.data !== 'string') return;
 463                  log.push(e.data.slice(0, 160));
 464                  if (e.data.startsWith('["READY"]') && !booted) {
 465                      booted = true;
 466                      w.postMessage(JSON.stringify(['SET_PROXY_EOSE_MS', 500]));
 467                      // Empty relay list triggers timer-only path.
 468                      reqStart = Date.now();
 469                      w.postMessage(JSON.stringify(['PROXY', 'sub-eose', {kinds: [99999], limit: 1}, []]));
 470                      sentReq = true;
 471                  } else if (sentReq && e.data.startsWith('["EOSE","sub-eose"]')) {
 472                      const elapsed = Date.now() - reqStart;
 473                      finish({ ok: true, elapsedMs: elapsed, log });
 474                  } else if (e.data.startsWith('["__ERROR"')) {
 475                      finish({ ok: false, error: e.data, log });
 476                  }
 477              };
 478              w.onerror = (e) => finish({ ok: false, error: e.message, log });
 479              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 480              setTimeout(() => finish({ ok: false, error: 'timeout', log }), 6000);
 481              """
 482          )
 483          result = json.loads(result_str)
 484          assert result.get("ok"), f"EOSE timer test failed: {result}"
 485          # 500ms timer: should fire within 400-2500ms (loose to absorb worker boot jitter).
 486          elapsed = result.get("elapsedMs", 0)
 487          assert 300 <= elapsed <= 3000, f"EOSE timer fired in {elapsed}ms (expected ~500): {result}"
 488          browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
 489  
 490      def test_relay_proxy_high_event_volume(self, relay, browser):
 491          """Seed THOUSANDS of events into the test relay; PROXY-subscribe;
 492          observe whether the worker's bridge calls fail under sustained
 493          WS event ingestion."""
 494          N = 2000
 495          # Big tag list per event to inflate the JSON size, simulating
 496          # production events with mentions / references.
 497          big_tags = [["e", "a" * 64], ["p", "b" * 64], ["t", "tag1"], ["t", "tag2"]]
 498          events = []
 499          for i in range(N):
 500              content = "x" * 200 + f"-{i}"  # ~250 byte content
 501              events.append(make_event(TEST_SECKEY, content, kind=42, tags=big_tags, created_at=2000000 + i))
 502  
 503          ws_seed = websocket.create_connection(relay["ws"], timeout=15)
 504          try:
 505              for ev in events:
 506                  ws_seed.send(json.dumps(["EVENT", ev]))
 507              ws_seed.settimeout(30)
 508              ok_count = 0
 509              deadline = time.time() + 60
 510              while ok_count < N and time.time() < deadline:
 511                  try:
 512                      msg = ws_seed.recv()
 513                      if json.loads(msg)[0] == "OK":
 514                          ok_count += 1
 515                  except Exception:
 516                      break
 517              print(f"\nseeded {ok_count}/{N} large events to test relay")
 518          finally:
 519              ws_seed.close()
 520  
 521          browser.get(relay["url"])
 522          deadline = time.time() + 25
 523          while time.time() < deadline:
 524              ready = browser.execute_script(
 525                  "return document.body.getAttribute('data-wasm-ready') || ''"
 526              )
 527              if ready == "1":
 528                  break
 529              time.sleep(0.3)
 530          time.sleep(2)
 531  
 532          result_str = browser.execute_async_script(
 533              """
 534              const done = arguments[arguments.length-1];
 535              const wsURL = arguments[0];
 536              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 537              window.__rpVolWorker = w;
 538              const errors = [];
 539              let eventCount = 0, gotEose = false;
 540              let booted = false;
 541              w.onmessage = (e) => {
 542                  if (typeof e.data !== 'string') return;
 543                  if (e.data.startsWith('["__ERROR"')) errors.push(e.data);
 544                  if (e.data.startsWith('["READY"]') && !booted) {
 545                      booted = true;
 546                      // No limit on the filter to receive everything the relay holds.
 547                      w.postMessage(JSON.stringify(['PROXY', 'sub-vol', {kinds: [42], limit: 5000}, [wsURL]]));
 548                  } else if (e.data.startsWith('["EVENT","sub-vol"')) {
 549                      eventCount++;
 550                  } else if (e.data.startsWith('["EOSE","sub-vol"]')) {
 551                      gotEose = true;
 552                      setTimeout(() => done(JSON.stringify({ eventCount, errors, gotEose })), 1500);
 553                  }
 554              };
 555              w.onerror = (e) => errors.push('onerror: ' + e.message + ' at ' + e.filename + ':' + e.lineno);
 556              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 557              setTimeout(() => done(JSON.stringify({ eventCount, errors, gotEose, finalState: 'timeout' })), 35000);
 558              """,
 559              relay["ws"],
 560          )
 561          result = json.loads(result_str)
 562          print(f"\n--- high-volume event ingestion result ---")
 563          print(f"events received: {result.get('eventCount')}")
 564          print(f"got EOSE: {result.get('gotEose')}")
 565          print(f"errors ({len(result.get('errors', []))}):")
 566          for e in result.get("errors", [])[:10]:
 567              print(f"  {e[:300]}")
 568          browser.execute_script("if (window.__rpVolWorker) { window.__rpVolWorker.terminate(); window.__rpVolWorker = null; }")
 569          # The diagnostic mode wrappers emit __ERROR messages BEFORE re-throwing,
 570          # so we have ptr/len/bufLen in the error text. If errors appear, they
 571          # are exactly the production bug we are hunting.
 572          if result.get("errors"):
 573              assert False, f"REPRODUCED: {result['errors'][0][:400]}"
 574          assert result.get("eventCount", 0) > 100, f"too few events delivered: {result}"
 575  
 576      def test_relay_proxy_query_with_seeded_idb(self, relay, browser):
 577          """Seed many events into the worker's IDB; query them; verify no
 578          RangeError. This is the production-shape regime where the wasm
 579          bridge handles a large IDB-result JSON string."""
 580          browser.get(relay["url"])
 581          deadline = time.time() + 25
 582          while time.time() < deadline:
 583              ready = browser.execute_script(
 584                  "return document.body.getAttribute('data-wasm-ready') || ''"
 585              )
 586              if ready == "1":
 587                  break
 588              time.sleep(0.3)
 589          time.sleep(2)
 590  
 591          # Seed N events with a single shared kind.
 592          # Each event ~600 bytes JSON; 500 events ~300KB total.
 593          N = 500
 594          events = []
 595          for i in range(N):
 596              content = f"phase2-stress-seed-{i}"
 597              events.append(make_event(TEST_SECKEY, content, kind=42, created_at=1000000 + i))
 598  
 599          # Send each EVENT to a fresh worker via SaveEvent. The simplest way
 600          # is to construct a worker, then drive idb.SaveEvent for each event.
 601          # The worker doesn't expose SaveEvent directly via its wire protocol,
 602          # so we use the relay's WS to publish, then have the worker PROXY-subscribe
 603          # to populate IDB the natural way.
 604          ws_seed = websocket.create_connection(relay["ws"], timeout=10)
 605          try:
 606              for ev in events:
 607                  ws_seed.send(json.dumps(["EVENT", ev]))
 608              # Drain the OK responses (one per event).
 609              ws_seed.settimeout(15)
 610              ok_count = 0
 611              deadline = time.time() + 20
 612              while ok_count < N and time.time() < deadline:
 613                  try:
 614                      msg = ws_seed.recv()
 615                      if json.loads(msg)[0] == "OK":
 616                          ok_count += 1
 617                  except Exception:
 618                      break
 619              print(f"\nseeded {ok_count}/{N} events to test relay")
 620          finally:
 621              ws_seed.close()
 622  
 623          # Subscribe via PROXY; worker writes incoming events to IDB.
 624          result_str = browser.execute_async_script(
 625              """
 626              const done = arguments[arguments.length-1];
 627              const wsURL = arguments[0];
 628              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 629              window.__rpStressWorker = w;
 630              const errors = [];
 631              let eventCount = 0, gotEose = false;
 632              let booted = false;
 633              w.onmessage = (e) => {
 634                  if (typeof e.data !== 'string') return;
 635                  if (e.data.startsWith('["__ERROR"')) errors.push(e.data);
 636                  if (e.data.startsWith('["READY"]') && !booted) {
 637                      booted = true;
 638                      w.postMessage(JSON.stringify(['PROXY', 'sub-stress', {kinds: [42], limit: 1000}, [wsURL]]));
 639                  } else if (e.data.startsWith('["EVENT","sub-stress"')) {
 640                      eventCount++;
 641                  } else if (e.data.startsWith('["EOSE","sub-stress"]')) {
 642                      gotEose = true;
 643                      setTimeout(() => done(JSON.stringify({ eventCount, errors, gotEose })), 1000);
 644                  }
 645              };
 646              w.onerror = (e) => errors.push('onerror: ' + e.message + ' at ' + e.filename + ':' + e.lineno);
 647              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 648              // Generous timeout because PROXY EOSE timer is 15s by default.
 649              setTimeout(() => done(JSON.stringify({ eventCount, errors, gotEose, finalState: 'timeout' })), 25000);
 650              """,
 651              relay["ws"],
 652          )
 653          result = json.loads(result_str)
 654          print(f"\n--- seeded-IDB stress result ---")
 655          print(f"events received via PROXY: {result.get('eventCount')}")
 656          print(f"got EOSE: {result.get('gotEose')}")
 657          print(f"errors ({len(result.get('errors', []))}):")
 658          for e in result.get("errors", [])[:5]:
 659              print(f"  {e[:200]}")
 660          # Cleanup before assertions to avoid hanging Firefox.
 661          browser.execute_script("if (window.__rpStressWorker) { window.__rpStressWorker.terminate(); window.__rpStressWorker = null; }")
 662          assert not result.get("errors"), f"worker errored: {result.get('errors', [])[:3]}"
 663          # We don't strictly require all N events; just confirm the worker survived.
 664          assert result.get("eventCount", 0) > 0, f"worker didn't deliver any events: {result}"
 665  
 666      def test_relay_proxy_rapid_message_flurry(self, relay, browser):
 667          """Send a tight burst of REQ + CLOSE + PROXY + EVENT messages
 668          with no inter-message delay, mimicking a feed-re-init storm."""
 669          unique_content = f"phase2-flurry-{int(time.time() * 1000)}"
 670          ev = make_event(TEST_SECKEY, unique_content, kind=1)
 671  
 672          browser.get(relay["url"])
 673          deadline = time.time() + 25
 674          while time.time() < deadline:
 675              ready = browser.execute_script(
 676                  "return document.body.getAttribute('data-wasm-ready') || ''"
 677              )
 678              if ready == "1":
 679                  break
 680              time.sleep(0.3)
 681          time.sleep(2)
 682  
 683          result_str = browser.execute_async_script(
 684              """
 685              const done = arguments[arguments.length-1];
 686              const wsURL = arguments[0];
 687              const eventJSON = arguments[1];
 688              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 689              window.__rpFlurryWorker = w;
 690              const errors = [];
 691              const log = [];
 692              let booted = false;
 693              w.onmessage = (e) => {
 694                  if (typeof e.data !== 'string') return;
 695                  log.push(e.data.slice(0, 80));
 696                  if (e.data.startsWith('["__ERROR"')) errors.push(e.data);
 697                  if (e.data.startsWith('["READY"]') && !booted) {
 698                      booted = true;
 699                      // Tight burst: 100 REQs with different subIDs, all in one JS turn.
 700                      for (let i = 0; i < 100; i++) {
 701                          w.postMessage('["REQ","sub' + i + '",{"kinds":[' + (i % 20) + '],"limit":50}]');
 702                      }
 703                      // CLOSE half of them.
 704                      for (let i = 0; i < 50; i++) {
 705                          w.postMessage('["CLOSE","sub' + i + '"]');
 706                      }
 707                      // 30 PROXY messages with the same relay URL.
 708                      for (let i = 0; i < 30; i++) {
 709                          w.postMessage('["PROXY","p' + i + '",{"kinds":[1],"limit":10},["' + wsURL + '"]]');
 710                      }
 711                      // 10 EVENT publishes (signed).
 712                      for (let i = 0; i < 10; i++) {
 713                          w.postMessage('["PUBLISH_TO",' + eventJSON + ',["' + wsURL + '"]]');
 714                      }
 715                      setTimeout(() => done(JSON.stringify({ errors, msgCount: log.length, lastLogs: log.slice(-15) })), 8000);
 716                  }
 717              };
 718              w.onerror = (e) => errors.push('onerror: ' + e.message + ' at ' + e.filename + ':' + e.lineno);
 719              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 720              setTimeout(() => done(JSON.stringify({ errors, msgCount: log.length, finalState: 'timeout' })), 15000);
 721              """,
 722              relay["ws"], json.dumps(ev),
 723          )
 724          result = json.loads(result_str)
 725          print(f"\n--- flurry stress result ---")
 726          print(f"msg count: {result.get('msgCount')}")
 727          print(f"errors ({len(result.get('errors', []))}):")
 728          for e in result.get("errors", [])[:5]:
 729              print(f"  {e[:200]}")
 730          print(f"last 5 messages:")
 731          for m in result.get("lastLogs", [])[-5:]:
 732              print(f"  {m[:120]}")
 733          browser.execute_script("if (window.__rpFlurryWorker) { window.__rpFlurryWorker.terminate(); window.__rpFlurryWorker = null; }")
 734          assert not result.get("errors"), f"worker errored under message flurry: {result.get('errors', [])[:3]}"
 735  
 736      def test_relay_proxy_production_boot_sequence(self, relay, browser):
 737          """Reproduce the production volume-regime bug: send the same
 738          boot-time message sequence the app sends, observe whether the
 739          worker's bridge calls start throwing RangeError."""
 740          browser.get(relay["url"])
 741          deadline = time.time() + 25
 742          while time.time() < deadline:
 743              ready = browser.execute_script(
 744                  "return document.body.getAttribute('data-wasm-ready') || ''"
 745              )
 746              if ready == "1":
 747                  break
 748              time.sleep(0.3)
 749          time.sleep(2)
 750  
 751          # Hex pubkey; 23 authors filter (matching the production [sub] feed follows=23).
 752          pubkey_hex = "00" * 32
 753          authors = [pubkey_hex[:62] + f"{i:02x}" for i in range(23)]
 754          feed_filter = json.dumps({"kinds": [1], "authors": authors, "limit": 200})
 755          prof_filter = json.dumps({"kinds": [0, 3, 10002, 10000, 10050]})
 756  
 757          result_str = browser.execute_async_script(
 758              """
 759              const done = arguments[arguments.length-1];
 760              const wsURL = arguments[0];
 761              const pubkey = arguments[1];
 762              const profFilter = arguments[2];
 763              const feedFilter = arguments[3];
 764              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 765              window.__rpStressWorker = w;
 766              const errors = [];
 767              const log = [];
 768              let booted = false;
 769              const finishAfter = (ms) => setTimeout(() => {
 770                  done(JSON.stringify({ errors, log: log.slice(-30), msgCount: log.length }));
 771              }, ms);
 772              w.onmessage = (e) => {
 773                  if (typeof e.data !== 'string') return;
 774                  log.push(e.data.slice(0, 200));
 775                  if (e.data.startsWith('["__ERROR"')) errors.push(e.data);
 776                  if (e.data.startsWith('["READY"]') && !booted) {
 777                      booted = true;
 778                      // The exact sequence the app sends on boot.
 779                      w.postMessage('["SET_PUBKEY","' + pubkey + '"]');
 780                      w.postMessage('["ENC_KEY","' + ('aa'.repeat(32)) + '"]');
 781                      w.postMessage('["PROXY","prof",' + profFilter + ',["' + wsURL + '"]]');
 782                      w.postMessage('["PROXY","prof-settings",{"kinds":[30078],"#d":["smesh-settings"],"limit":1},["' + wsURL + '"]]');
 783                      w.postMessage('["PROXY","feed",' + feedFilter + ',["' + wsURL + '"]]');
 784                      w.postMessage('["PROXY","ntf",{"kinds":[6,7,9735],"#e":[],"limit":500},["' + wsURL + '"]]');
 785                      finishAfter(8000);
 786                  }
 787              };
 788              w.onerror = (e) => errors.push('onerror: ' + e.message);
 789              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 790              setTimeout(() => done(JSON.stringify({ errors, log: log.slice(-30), msgCount: log.length, finalState: 'never-finished' })), 15000);
 791              """,
 792              relay["ws"], pubkey_hex, prof_filter, feed_filter,
 793          )
 794          result = json.loads(result_str)
 795          # Diagnostic-only; print structure.
 796          print(f"\n--- production boot stress result ---")
 797          print(f"msg count: {result.get('msgCount')}")
 798          print(f"errors ({len(result.get('errors', []))}):")
 799          for e in result.get("errors", [])[:5]:
 800              print(f"  {e[:200]}")
 801          print(f"last 5 messages:")
 802          for m in result.get("log", [])[-5:]:
 803              print(f"  {m[:160]}")
 804          # The bug manifests as __ERROR messages from the worker.
 805          assert not result.get("errors"), f"worker errored under production-shape boot: {result.get('errors', [])[:3]}"
 806          browser.execute_script("if (window.__rpStressWorker) { window.__rpStressWorker.terminate(); window.__rpStressWorker = null; }")
 807  
 808      @pytest.mark.skip(reason="DM_HISTORY is handled by the DM worker, not relay-proxy; needs full worker topology")
 809      def test_relay_proxy_dm_history_returns_array(self, relay, browser):
 810          """DM_HISTORY queries IDB DMs for a peer. Empty database => empty array."""
 811          browser.get(relay["url"])
 812          deadline = time.time() + 25
 813          while time.time() < deadline:
 814              ready = browser.execute_script(
 815                  "return document.body.getAttribute('data-wasm-ready') || ''"
 816              )
 817              if ready == "1":
 818                  break
 819              time.sleep(0.3)
 820          time.sleep(2)
 821  
 822          # 64-hex-char dummy peer pubkey.
 823          peer = "00" * 32
 824  
 825          browser.set_script_timeout(15)
 826          result_str = browser.execute_async_script(
 827              """
 828              const done = arguments[arguments.length-1];
 829              const peer = arguments[0];
 830              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 831              window.__rpTestWorker = w;
 832              const log = [];
 833              let booted = false, finished = false;
 834              const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
 835              w.onmessage = (e) => {
 836                  if (typeof e.data !== 'string') return;
 837                  log.push(e.data.slice(0, 160));
 838                  if (e.data.startsWith('["READY"]') && !booted) {
 839                      booted = true;
 840                      // ["DM_HISTORY", peer, limit, until]
 841                      w.postMessage('["DM_HISTORY","' + peer + '",50,0]');
 842                  } else if (e.data.startsWith('["DM_HISTORY",')) {
 843                      finish({ ok: true, msg: e.data, log });
 844                  } else if (e.data.startsWith('["__ERROR"')) {
 845                      finish({ ok: false, error: e.data, log });
 846                  }
 847              };
 848              w.onerror = (e) => finish({ ok: false, error: e.message, log });
 849              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 850              setTimeout(() => finish({ ok: false, error: 'timeout', log }), 12000);
 851              """,
 852              peer,
 853          )
 854          result = json.loads(result_str)
 855          assert result.get("ok"), f"DM_HISTORY failed: {result}"
 856          # Format ["DM_HISTORY", peer, <array>] - confirm shape.
 857          assert peer in result["msg"], f"peer missing from response: {result['msg']}"
 858          assert "[]" in result["msg"] or "[{" in result["msg"], f"unexpected DM_HISTORY shape: {result['msg']}"
 859          browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
 860  
 861      def test_relay_proxy_handles_proxy_remote_subscription(self, relay, browser):
 862          """Seed an event into the test relay; tell the relay-proxy worker
 863          to PROXY-subscribe to that relay; verify the worker forwards the
 864          event back. Exercises the full WS pool + remote sub + forward path."""
 865          # Publish a unique kind:1 event to the test relay via raw WS.
 866          unique_content = f"phase2-1b-test-{int(time.time())}"
 867          ev = make_event(TEST_SECKEY, unique_content, kind=1)
 868          ws_seed = websocket.create_connection(relay["ws"], timeout=5)
 869          try:
 870              ws_seed.send(json.dumps(["EVENT", ev]))
 871              # Read OK.
 872              resp = ws_seed.recv()
 873              assert json.loads(resp)[0] == "OK", f"seed publish failed: {resp}"
 874          finally:
 875              ws_seed.close()
 876  
 877          # Now drive a relay-proxy worker via PROXY.
 878          browser.get(relay["url"])
 879          deadline = time.time() + 25
 880          while time.time() < deadline:
 881              ready = browser.execute_script(
 882                  "return document.body.getAttribute('data-wasm-ready') || ''"
 883              )
 884              if ready == "1":
 885                  break
 886              time.sleep(0.3)
 887          time.sleep(2)
 888  
 889          result_str = browser.execute_async_script(
 890              """
 891              const done = arguments[arguments.length-1];
 892              const wsURL = arguments[0];
 893              const expected = arguments[1];
 894              const w = new Worker('/relay-proxy-wasm-host.mjs', { type: 'module' });
 895              window.__rpTestWorker = w;
 896              const log = [];
 897              let booted = false, finished = false, evMatch = null;
 898              const finish = (out) => { if (finished) return; finished = true; done(JSON.stringify(out)); };
 899              w.onmessage = (e) => {
 900                  if (typeof e.data !== 'string') return;
 901                  log.push(e.data.slice(0, 160));
 902                  if (e.data.startsWith('["READY"]') && !booted) {
 903                      booted = true;
 904                      w.postMessage(JSON.stringify(['PROXY', 'sub-proxy', {kinds: [1], limit: 50}, [wsURL]]));
 905                  } else if (e.data.startsWith('["EVENT","sub-proxy"')) {
 906                      if (e.data.indexOf(expected) >= 0) {
 907                          evMatch = e.data;
 908                          finish({ ok: true, msg: e.data, log });
 909                      }
 910                  } else if (e.data.startsWith('["__ERROR"')) {
 911                      finish({ ok: false, error: e.data, log });
 912                  }
 913              };
 914              w.onerror = (e) => finish({ ok: false, error: e.message, log });
 915              w.postMessage({ type: 'init', mode: 'root', wasmUrl: '/relay-proxy.wasm' });
 916              setTimeout(() => finish({ ok: false, error: 'timeout', log }), 12000);
 917              """,
 918              relay["ws"],
 919              unique_content,
 920          )
 921          result = json.loads(result_str)
 922          assert result.get("ok"), f"PROXY didn't deliver event: {result}"
 923          assert unique_content in result.get("msg", ""), f"event content missing: {result}"
 924          browser.execute_script("if (window.__rpTestWorker) { window.__rpTestWorker.terminate(); window.__rpTestWorker = null; }")
 925