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

+68 -1
View File
@@ -363,6 +363,10 @@ const enabled = (() => {
/** Returns a Promise for mocked commands, or undefined to pass through. */
export function mockInvoke(cmd, args) {
if (!enabled) return undefined;
/* the weather, BEFORE any dispatch, so a plugin: command and a catalog
command arm exactly the same way (see the doors below) */
const forecast = weather(cmd);
if (forecast) return forecast;
if (cmd === "app_status") return Promise.resolve({ version: "mock", connected_as: null });
if (cmd === "library_albums") return Promise.resolve(albums);
if (cmd === "library_album_stats") return Promise.resolve(albums.map(statsFor));
@@ -419,7 +423,70 @@ export function mockEmit(event, payload) {
}
}
const later = (ms, fn) => setTimeout(fn, ms);
if (enabled && typeof window !== "undefined") window.__ssMock = { emit: mockEmit };
/* ── the weather ────────────────────────────────────────────────────────
Thirty rounds of walk drove the happy path and nothing else: not one step
ever made a command reject, and not one ever saw the client signed out.
This mock IS the whole backend behind the browser walk, so it is the only
place the unhappy paths can be driven from.
Four doors, all of them checked in mockInvoke BEFORE any dispatch, so a
`plugin:` command and a catalog command arm exactly the same way:
__ssMock.fail(cmd, message) the next and every call to `cmd` rejects
with that exact string; "" disarms it.
__ssMock.answer(cmd, value) `cmd` resolves with `value` — an empty
list, a null page — instead of its
fixture, so the walk names what "empty"
means rather than the mock guessing.
__ssMock.forget(cmd) takes one armed answer away.
__ssMock.offline(on) the client goes down: everything the
backend funnels through with_client()
answers "not connected to Soulseek",
connect_stored / connect_and_login answer
the server string, and soulseek:state
goes out. off restores and re-connects.
__ssMock.clear() all of it away, for the walk's last step.
They reject with a PLAIN STRING, because that is what Tauri rejects with:
an Error would arrive as "Error: …" and hide half of what say() has to
cope with. All of it sits behind `enabled`, so a real build never sees it. */
const armedFail = new Map();
const armedAnswer = new Map();
let offlineNow = false;
/* what src-tauri/src/commands.rs runs through with_client(), plus the two
searches and a download, which check the connection themselves */
const needsClient = (cmd) => /^(slsk_|buddy_)/.test(cmd) || cmd === "start_search" || cmd === "stop_search" || cmd === "download_file";
function weather(cmd) {
const f = armedFail.get(cmd);
if (f != null) return Promise.reject(f);
if (offlineNow) {
if (cmd === "connect_stored" || cmd === "connect_and_login") {
return Promise.reject("could not reach server: io: failed to lookup address information: no address associated with hostname");
}
if (needsClient(cmd)) return Promise.reject("not connected to Soulseek");
}
if (armedAnswer.has(cmd)) return Promise.resolve(armedAnswer.get(cmd));
return undefined;
}
if (enabled && typeof window !== "undefined") {
window.__ssMock = {
emit: mockEmit,
fail(cmd, message) { if (message) armedFail.set(cmd, String(message)); else armedFail.delete(cmd); return true; },
answer(cmd, value) { armedAnswer.set(cmd, value); return true; },
forget(cmd) { armedAnswer.delete(cmd); return true; },
offline(on) {
offlineNow = !!on;
mockEmit("soulseek:state", offlineNow ? { kind: "disconnected" } : { kind: "connected", username: MOCK_USER });
return offlineNow;
},
clear() {
armedFail.clear();
armedAnswer.clear();
if (offlineNow) { offlineNow = false; mockEmit("soulseek:state", { kind: "connected", username: MOCK_USER }); }
return true;
},
};
}
const MOCK_USER = "mockuser";
const ROOMS = [["Lounge", 412], ["Electronic", 98], ["Vinyl Rips", 37], ["Jazz", 61], ["Lossless Only", 23], ...Array.from({ length: 90 }, (_, i) => ["Room " + (i + 1), 90 - i])];