android: what the phone says when things go wrong

Fifty-nine places put a raw Rust string into a field a person reads, and
the red toast drew it verbatim — a phone in a lift showed "could not reach
server: io: failed to lookup address information" under the password box.
One translator, src/mobile/trouble.js, called where an error is DRAWN
rather than where it is caught: the stores keep what Rust said and only
the sentence on screen is ours, so every sink is fixed at once. Five
screens that drew a failed read as an empty answer stop lying — Tracks no
longer reports a broken database as an empty library, a playlist that
could not be read is not an empty playlist, the lyrics face says a lookup
failed instead of "no lyrics" and finally carries the ⋯ that reaches Paste
lyrics, Gaps says why it could not reach the catalogue, and Search signed
out says so where it used to render nothing at all. The room list, which
is fire-and-forget over the wire, gets a twelve-second end and a Try
again.

rescan_library ran prune_missing() first, which deletes every track row
whose file does not exist — so one tap on You → Rescan library with the
folder unreachable emptied the whole index and returned Ok(()). It now
makes the root if it is merely not there yet and refuses if it cannot be
read, and You draws the path with what to do about it.

The mock grew a weather system so the walk can drive all of this: fail,
answer, offline and clear, checked before dispatch so a plugin command
arms like any other. Nine mock steps and one on the phone — the first
steps in thirty rounds that ever made a command reject or saw the client
signed out — and the last of them puts the weather away and leaves the app
as it found it.
This commit is contained in:
KeelrLeek committed 2026-09-14 01:23:26 -05:00
1 parent 44eda8dff5
commit 052e15302b
22 files changed
+537 -35

No files matched your search

+105
View File
@@ -251,6 +251,26 @@ const PROBE = `(function () {
halfSub: ((document.querySelector('.halfsheet .sheet-head .m') || {}).textContent || '').trim(),
halfSw: [].filter.call(document.querySelectorAll('.halfsheet .row'), function (r) { return !!r.querySelector('.sw.on'); }).map(function (r) { return ((r.querySelector('.t') || {}).textContent || '').trim(); }).join('|'),
contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),
/* ── the weather: what the screen says when a read fails, an answer
is empty, or nobody is signed in. Every one of these is a line
this round put there; before it they were an empty box, a spinner
with no end, or a Rust string in the red toast. */
slskOn: (function () { try { return __ss.slsk.connection; } catch (e) { return ''; } })(),
signin: !!document.querySelector('.signin'),
signinErr: (document.querySelector('.signin-err') || {}).textContent || '',
why: (document.querySelector('.stub.why') || {}).textContent || '',
/* the toast's one action, by its words: "Try again" or "Sign in" */
retry: (document.querySelector('.toast:not(.notice) .na') || {}).textContent || '',
again: !![].slice.call(document.querySelectorAll('.imp-foot .btn')).find(function (b) { return /Try again/.test(b.textContent); }),
roomsNote: (document.querySelector('.rooms-note') || {}).textContent || '',
offlineNote: (document.querySelector('.offline-note') || {}).textContent || '',
netAsk: !!document.querySelector('.net-ask'),
folderGone: (document.querySelector('.folder-gone') || {}).textContent || '',
lyrNote: (document.querySelector('.lyr-note') || {}).textContent || '',
lyrTools: !!document.querySelector('.lyr-tools'),
trks: document.querySelectorAll('.screen .trk').length,
/* the phone's trouble-words step leaves its eleven sentences here */
trouble: (function () { try { return String(window.__trouble || ''); } catch (e) { return ''; } })(),
syStrip: document.querySelectorAll('.sy-strip .sy-col').length,
syTicks: document.querySelectorAll('.sy-strip .sy-tick').length,
syEst: (document.querySelector('.sy-est') || {}).textContent || '',
@@ -745,6 +765,62 @@ const MOCK_STEPS = [
{ view: [411, 914], wait: 800 },
{ do: `document.querySelector('.mini').click()`, name: "portrait-again", wait: 1200 },
{ do: `__ss.closeSheet()`, wait: 600 },
/* ── the weather ────────────────────────────────────────────────────
Everything above this line is the happy path; nothing in thirty
rounds of walk had ever made a command reject or taken the client
offline, so none of these lines had ever been drawn. The mock's own
doors (__ssMock.fail / answer / offline / clear) are the weather, and
every assertion below is on SETTLED state — an armed failure stays
armed, a sign-in card stays up, the folder-gone row stays — because
the probe lands after the screenshot, well past the step's own wait.
The one timed assertion is the room list, and its timeout is shortened
to 1.2 s against the 3.1 s of waits between arming it and reading it.
The last step puts the weather away and
leaves the app exactly as it found it — anything appended after this
block inherits a signed-in app on Home.
The first step settles FIRST, and it has to: the landscape block above
leaves the library tab three screens deep (record → artist), so one tap
on the Library tab lands on the artist page and `.segtabs button` would
be its discography tabs, not Albums/Artists/Tracks. It also wipes any
error the happy path left standing, so `!i.toast` below means this
block's toast and no other. */
{ do: `(function () { __ss.player.error = ''; __ss.lib.error = ''; __ss.player.retry = null; return __settle(); })()`, wait: 800 },
{ do: `__ssMock.fail('library_track_index', 'unable to open database file')`, wait: 300 },
{ do: `document.querySelectorAll('.nav button')[2].click()`, wait: 700 },
{ do: `document.querySelectorAll('.segtabs button')[2].click()`, name: "weather-tracks", wait: 1300 },
{ do: `__ssMock.fail('library_track_index', '')`, wait: 200 },
{ do: `__click('.imp-foot .btn', 'Try again')`, name: "weather-tracks-ok", wait: 1300 },
/* signed out, in a lift: the card is back and nothing says why in Rust */
{ do: `__ssMock.offline(true)`, wait: 600 },
{ do: `document.querySelectorAll('.nav button')[3].click()`, name: "weather-signedout", wait: 1100 },
/* the single most visible raw-Rust site on the phone, twice over */
{ do: `__ssMock.fail('connect_and_login', 'login failed: INVALIDPASS')`, wait: 200 },
{ do: `(function () { var p = document.querySelectorAll('.signin .field')[1]; p.value = 'hunter2'; p.dispatchEvent(new Event('input', { bubbles: true })); return true; })()`, wait: 400 },
{ do: `__click('.signin .btn-fill', 'Sign in')`, name: "weather-badpass", wait: 1200 },
{ do: `__ssMock.fail('connect_and_login', 'could not reach server: io: failed to lookup address information: no address associated with hostname')`, wait: 200 },
{ do: `__click('.signin .btn-fill', 'Sign in')`, name: "weather-noserver", wait: 1200 },
/* Search signed out with a query typed showed NOTHING where the network
section lives — no row, no reason. Now it says so and offers the door */
{ do: `document.querySelectorAll('.nav button')[1].click()`, wait: 700 },
{ do: `(function () { var i = document.querySelector('.sfield input'); i.value = 'daft punk'; i.dispatchEvent(new Event('input', { bubbles: true })); return true; })()`, name: "weather-search-out", wait: 1800 },
/* the room list is fire-and-forget over the wire (the client sends it and
nothing answers if the server will not), so the wait is ours: an armed
answer that resolves and emits nothing is exactly that server. Community
is left on Buddies by the walk above, so the Rooms tab is asked for by
name — the note lives under that one. */
{ do: `__ssMock.fail('connect_and_login', ''); __ssMock.offline(false)`, wait: 1200 },
{ do: `(function () { __ss.slsk.roomsWaitMs = 1200; __ss.slsk.rooms = []; __ssMock.answer('slsk_room_list', null); __ss.refreshRooms(); return true; })()`, wait: 200 },
{ do: `document.querySelectorAll('.nav button')[3].click()`, wait: 900 },
{ do: `__click('.segtabs button', 'Rooms')`, name: "weather-rooms-late", wait: 2200 },
/* one tap on Rescan library used to empty the whole index and say Ok */
{ do: `__ssMock.fail('rescan_library', 'library-folder-missing: /storage/1A2B-3C4D/Music')`, wait: 200 },
{ do: `document.querySelectorAll('.nav button')[4].click()`, wait: 800 },
{ do: `__click('.screen .row', 'Rescan library')`, name: "weather-rescan", wait: 1500 },
/* and the weather clears */
{ do: `(function () { __ss.slsk.roomsWaitMs = 12000; __ssMock.clear(); __ss.refreshRooms(); __ss.player.error = ''; __ss.lib.error = ''; __ss.player.retry = null; return true; })()`, wait: 900 },
{ do: `__settle()`, wait: 900 },
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "weather-clear", wait: 1000 },
];
const MOCK_EXPECT = {
tracks: (i) => i.title.startsWith("Your library"),
@@ -791,6 +867,24 @@ const MOCK_EXPECT = {
"search-recent-pick": (i) => i.title.startsWith("Search") && (i.songs >= 4 || !!i.top),
"sleep-sheet": (i) => i.half.includes("up"),
"sleep-set": (i) => !i.half.includes("up"),
/* ── the weather ────────────────────────────────────────────────────
Each of these fails if the round's fix is undone: the stub if a failed
read goes back to being drawn as an empty library, the sign-in lines if
the toast or the card stops translating, the search note if the block
goes back to rendering nothing at all signed out, the room note if the
spinner loses its end, the folder row if the rescan stops refusing.
Every one also asserts the RAW string is gone — that is the round. */
"weather-tracks": (i) => /^Couldn't read the library index/.test(i.why) && /library folder isn't readable/.test(i.why)
&& i.again && i.trks === 0 && !/database file/.test(i.why + i.toast),
"weather-tracks-ok": (i) => !i.why && !i.again && i.trks > 0 && !i.toast,
"weather-signedout": (i) => i.slskOn === "offline" && i.signin && !i.signinErr && !i.toast,
"weather-badpass": (i) => i.signinErr === "Soulseek refused that password." && !/INVALIDPASS/.test(i.signinErr),
"weather-noserver": (i) => /^Couldn't reach the Soulseek server/.test(i.signinErr) && !/io:|address information/.test(i.signinErr),
"weather-search-out": (i) => i.title.startsWith("Search") && /Community/.test(i.offlineNote) && !i.netAsk,
"weather-rooms-late": (i) => i.slskOn === "online" && /hasn't sent its room list/.test(i.roomsNote) && i.again,
"weather-rescan": (i) => /1A2B-3C4D/.test(i.folderGone) && /isn't there any more/.test(i.toast) && /1A2B-3C4D/.test(i.toast)
&& !/library-folder-missing/.test(i.toast + i.folderGone),
"weather-clear": (i) => !i.toast && !i.why && !i.folderGone && !i.roomsNote && i.slskOn === "online" && /^(Good |Still)/.test(i.title),
/* sideways: the chrome budget, then every surface that was clipped. The
before-value of each is in the rig's now.json — 46% of the height as
chrome, a 879px cover with the transport 687px below the fold, a
@@ -1119,6 +1213,12 @@ const PHONE_STEPS = [
{ do: `history.back()`, wait: 800 },
{ do: `history.back()`, wait: 800 },
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 400 },
/* the translator, on the real build: the backend's own vocabulary in,
plain sentences out. No side effects — nothing is signed out (the
login throttle is a remembered trap), nothing is asked of the network,
nothing is written. It also catches a build where trouble.js simply
did not ship: __ss.say would not be there and the line stays empty. */
{ do: `(function () { var raw = ['not connected to server', 'not connected to Soulseek', 'io: failed to lookup address information: no address associated with hostname', 'protocol: unexpected end of frame', 'login failed: INVALIDPASS', 'could not reach server: io: connection refused', 'timed out', 'user dusty_crates is offline or unknown', 'no stored credentials', 'unable to open database file', 'library-folder-missing: /storage/1A2B-3C4D/Music']; window.__trouble = raw.map(function (r) { return __ss.say(r); }).join(' || '); return window.__trouble; })()`, name: "trouble-words", wait: 400 },
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "end", wait: 800 },
];
const PHONE_EXPECT = {
@@ -1197,6 +1297,11 @@ const PHONE_EXPECT = {
&& /^[a-z]+:(ready\+art|none):(ready|error):\d+$/.test(i.artFast),
"artist-more": (i) => i.half.includes("up") && /Choose hero photo/.test(i.halfText),
"catalog-album": (i) => i.screen.includes("tr-push") && !!i.title,
/* eleven real backend strings through the one translator on the device:
every output a sentence, and not one of them still carrying Rust */
"trouble-words": (i) => i.trouble.split(" || ").length === 11
&& i.trouble.split(" || ").every(function (s) { return s.length > 12; })
&& !/io:|protocol:|os error|INVALIDPASS|not connected|library-folder-missing|no stored credentials/i.test(i.trouble),
end: (i) => /^(Good |Still)/.test(i.title),
};