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