1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
"""
What an automatic reconnect has to re-read, and who is allowed to stop
listening for one.
Both defects here produce a plausible screen rather than an error, so they read
the source — the only evidence available for the SPA (CLAUDE.md), and the right
kind for a fault whose whole symptom is a page that looks fine and is stale.
- `_reconnectLoop` re-did the handshake and nothing else. Nobody asked for an
index again, and a push sent while the old channel was dying reached
nobody, so the page stayed frozen at whatever it last saw until someone
reloaded it. Survivable while a node that came back came back with the same
answers; a *restarted* one does not. It rebuilds its index from
`index_cache.db` — path/mtime/size/hash/type, no enrichment — so during its
re-enrichment pass the index it serves carries no artist and no album on
any track, and a client that reconnected inside that window drew an empty
Music grid for as long as it stayed open.
- `onReconnected` was one setter. The video player took it on open and set it
back to `null` on close, which silently disabled every other consumer's
handler — including, once the group page had one, the refresh above.
"""
import re
from pathlib import Path
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
TRANSPORT = STATIC / "transport.js"
GROUP_PAGE = STATIC / "group-page.js"
VIDEO_PLAYER = STATIC / "video-player.js"
pytestmark = pytest.mark.skipif(
not TRANSPORT.exists(), reason="the SPA sources are not available")
@pytest.fixture(scope="module")
def transport():
return TRANSPORT.read_text()
@pytest.fixture(scope="module")
def group_page():
return GROUP_PAGE.read_text()
@pytest.fixture(scope="module")
def video_player():
return VIDEO_PLAYER.read_text()
def _reconnect_loop(transport: str) -> str:
body = transport[transport.index("async _reconnectLoop()"):]
return body[:body.index("\n /**")]
def test_a_reconnect_is_announced_to_every_listener(transport):
"""One consumer unsubscribing must not silence the others."""
assert "addReconnectListener(fn)" in transport
assert "set onReconnected(" not in transport, (
"a single slot is what let the video player unset the group page's handler")
assert "this._reconnectListeners = new Set()" in transport
def test_addReconnectListener_hands_back_its_own_unsubscribe(transport):
body = transport[transport.index("addReconnectListener(fn) {"):]
body = body[:body.index("\n }")]
assert "this._reconnectListeners.add(fn)" in body
assert "return () => this._reconnectListeners.delete(fn)" in body, (
"without it a caller can only stop listening by clearing the whole set")
def test_the_reconnect_carries_the_fresh_ack(transport):
"""
The page's whole view of the node — which folders each app reads, which
apps are on, the roots — was answered by a process that may since have
restarted.
"""
loop = _reconnect_loop(transport)
assert re.search(r"ack\s*=\s*await this\.connect\(", loop), (
"the reconnect's own handshake answer was thrown away")
assert re.search(r"fn\(ack\)", loop)
def test_one_listener_throwing_does_not_rob_the_next(transport):
loop = _reconnect_loop(transport)
notify = loop[loop.index("_reconnectListeners"):]
assert "try {" in notify and "catch" in notify
def test_nothing_assigns_the_old_setter():
"""`grep onReconnected =` is what this is, spelled so it cannot rot."""
offenders = [p.name for p in STATIC.glob("*.js")
if re.search(r"\.onReconnected\s*=", p.read_text())]
assert offenders == [], (
f"{offenders} still assign a slot that no longer exists")
def test_the_group_page_refetches_the_index_on_reconnect(group_page):
assert "addReconnectListener(" in group_page, (
"a reconnect that re-reads nothing leaves the page a node-restart old")
listener = group_page[group_page.index("addReconnectListener("):]
listener = listener[:listener.index("\n });")]
assert "applyAck(" in listener, "the ack is a restarted node's answers, not the old one's"
assert "transport.fetchIndex()" in listener, (
"a delta is computed against a snapshot only the node has, and this "
"session was never told what it missed")
assert "applyIndex(" in listener
def test_the_group_page_reimports_the_gek_on_reconnect(group_page):
"""A chat epoch or a re-key while we were away hands back a different GEK."""
listener = group_page[group_page.index("addReconnectListener("):]
listener = listener[:listener.index("\n });")]
assert "importGEK" in listener
def test_the_ack_is_applied_by_one_implementation(group_page):
"""
Two copies drifting is how `helloworld`'s directories went missing from one
of them; the connect path and the reconnect path read the same function.
"""
assert group_page.count("const applyAck = useCallback(") == 1
assert group_page.count("applyAck(ack);") == 1
assert group_page.count("applyAck(reack);") == 1
body = group_page[group_page.index("const applyAck = useCallback("):]
body = body[:body.index("\n }, [")]
for setter in ("setEnabledApps", "setAppDirectories", "setMusicbrainzConfig",
"setTmdbConfig", "setChatDirectory", "setSearchListed"):
assert setter in body, f"{setter} is not re-read on a reconnect"
def test_every_listener_is_dropped_by_whoever_registered_it(group_page, video_player):
"""
A transport handed on to a running download (`releaseWhenIdle`) outlives
the page that opened it, and would go on driving a component that is gone.
"""
for name, source in (("group-page.js", group_page),
("video-player.js", video_player)):
assert re.search(r"=\s*transport\.addReconnectListener\(", source), (
f"{name} must keep the unsubscribe it is handed")
assert "offReconnect()" in source, (
f"{name} registers a reconnect listener it never drops")
def test_every_harness_that_renders_the_group_page_stubs_the_subscription():
"""
The fast half of a guard `test_group_tab_fallback` already provides slowly.
GroupPage subscribes on connect and calls the unsubscribe on unmount, so a
stub node without this throws inside `connect()` and the page renders its
error state — a whole harness reporting a layout it never drew. Three of
these stubs answer an unknown method through a Proxy, which hands back a
promise where an unsubscribe belongs and defers the same failure to the
teardown. Nine minutes of browser tests found it; this finds it in a
fraction of a second.
"""
harness = Path(__file__).parent / "harness"
stubs = [p for p in harness.glob("*.py")
if "window.MeshBayTransport" in p.read_text()
and "GroupPage" in p.read_text()]
assert stubs, "no harness stubs the transport any more — has this moved?"
missing = [p.name for p in stubs
if "addReconnectListener" not in p.read_text()]
assert missing == [], (
f"{missing} render GroupPage against a node that cannot be subscribed to")
|