diff options
Diffstat (limited to 'packages/meshbay-hub/tests/harness')
| -rw-r--r-- | packages/meshbay-hub/tests/harness/chat_send_probe.py | 63 |
1 files changed, 60 insertions, 3 deletions
diff --git a/packages/meshbay-hub/tests/harness/chat_send_probe.py b/packages/meshbay-hub/tests/harness/chat_send_probe.py index 28635b0..2df12e3 100644 --- a/packages/meshbay-hub/tests/harness/chat_send_probe.py +++ b/packages/meshbay-hub/tests/harness/chat_send_probe.py @@ -36,6 +36,18 @@ Both are run with an older request already pending — the condition that turns "guess by arrival order" from usually-right into wrong — and both must come back inside a second and a half. +A third scenario, `reconnect`, is about the *other* way this tab freezes, and +the one no timeout ends: + + `reconnect` the connection is untouched, but its **device identity** is + cleared and settled again, which is what every reconnect does to + it — `connect()` drops it on the way in and `device_hello` + restores it on the way out. The composer gates on that identity, + and used to read it off the transport during render, where a ref + changing re-renders nothing: it latched shut on whatever + unrelated re-render came next (a message arriving) and had no + event that would open it again. + Since MNP 2.0 a send also has to **seal and sign for real** before it goes anywhere, so this drives `chatKeys()`, `openGroup`, `sealChat` and a genuine Ed25519 signature rather than a model of any of them. The device key is @@ -110,7 +122,7 @@ PAGE_TEMPLATE = r"""<!doctype html><html><head><meta charset=utf-8> <script src="/keyderive.js"></script> <script src="/transport.js"></script> <script type="module"> -import { html, render, useRef } from '/vendor/htm-preact.js'; +import { html, render, useRef, useState, useEffect } from '/vendor/htm-preact.js'; import { ChatPanel } from '/chat-app.js'; const log = []; @@ -192,11 +204,23 @@ function makeTransport(name, chatReply) { return tp; } +// Stands in for group-page.js the way the stub above stands in for the node, +// and for the same reason: what is under test is the seam between them. These +// are its three lines — state, the callback wired before connect() runs, and +// the prop — because the defect was that ChatPanel read `devicePk` off the +// transport during render instead, and a ref changing re-renders nothing. function Host({ tp }) { const transportRef = useRef(tp); const gekRef = useRef(null); + // Seeded from the transport because makeTransport hands over a connection + // whose handshake is already done; in the page the callback below is what + // sets it, since it is wired before connect() and connect() is where + // device_hello runs. + const [deviceReady, setDeviceReady] = useState(!!tp.devicePk); + useEffect(() => { tp.onDeviceIdentity = (ok) => setDeviceReady(ok); }, [tp]); return html`<${ChatPanel} transportRef=${transportRef} gekRef=${gekRef} - username="me" userId="user-me" entries=${[]} status="connected" />`; + username="me" userId="user-me" entries=${[]} status="connected" + deviceReady=${deviceReady} />`; } function typeInto(root, text) { @@ -208,7 +232,7 @@ function typeInto(root, text) { c.dispatchEvent(new Event('input', { bubbles: true })); } -async function runScenario(name, chatReply) { +async function runScenario(name, chatReply, duringSession) { const root = document.createElement('div'); document.getElementById('root').appendChild(root); const tp = makeTransport(name, chatReply); @@ -225,6 +249,10 @@ async function runScenario(name, chatReply) { // a send is in flight. composerDisabled: c ? c.disabled : null, composerValue: c ? c.value : null, + // Which of the composer's two reasons it is. "Disabled" alone was all + // the field report could say, and it is the half that does not identify + // the defect. + composerPlaceholder: c ? c.placeholder : null, pending: tp._pending.size, }); }; @@ -241,6 +269,8 @@ async function runScenario(name, chatReply) { await wait(100); snap('older request pending'); + if (duringSession) await duringSession(tp, snap); + typeInto(root, 'hello'); await wait(100); root.querySelector('.chat-input').dispatchEvent(new KeyboardEvent('keydown', @@ -259,6 +289,33 @@ async function runScenario(name, chatReply) { out.scenarios.push(await runScenario('ack', { type: 'ack', v: '0.14' })); out.scenarios.push(await runScenario( 'error', { type: 'error', detail: 'Request failed' })); + // The connection survives, the device identity does not — which is exactly + // what a reconnect does: connect() clears it on the way in and device_hello + // settles it again on the way out. Nothing about the channel changes, so + // nothing else in the page moves, and the composer has to follow this on its + // own or it never comes back. + out.scenarios.push(await runScenario( + 'reconnect', { type: 'ack', v: '0.14' }, + async (tp, snap) => { + tp._setDevicePk('', 'new connection'); + await wait(100); + snap('device identity cleared'); + // A live message arrives — the ordinary thing that re-renders this + // panel, and the step that made the old defect permanent. The composer + // read `devicePk` off the transport during render, so it went disabled + // *here*, on an unrelated re-render, long after the identity was + // actually lost; and since nothing re-rendered it when the identity came + // back, it stayed that way for the rest of the session. + if (tp._onChat) { + tp._onChat({ id: 'live-1', sender_id: 'someone', sender_name: 'someone', + payload: 'still there?', timestamp: now, verified: true }); + } + await wait(100); + snap('a message arrived meanwhile'); + tp._setDevicePk(DEVICE_PK_B64, 'device_hello_ack'); + await wait(100); + snap('device identity restored'); + })); fetch('/log', { method: 'POST', body: JSON.stringify(out) }); })(); </script></body></html>""" |