android: the faces the walk had never seen — six failure states, read back in their own words

Round thirty-one gave the phone honest error states and shipped six of them
unobserved, because neither `loadHome` nor `loadLyrics` was reachable from the
`__ss` handle and both answer a second ask with nothing. The handle carries the
two stores and their two loaders now, and a block of mock steps arms each
failure with the mock's own weather, drives the face, and asserts the sentence
a person reads as well as the absence of the Rust string behind it: Home's
catalog line, the Gaps line and its second tap, a catalog record's stub, the
artist page's two different answers, a playlist that would not read, the lyrics
lookup that failed, a refused transfer, and the library toast's Try again,
pressed for the first time.

It sits before the first-run block, which reloads and empties the mock's
follows; every failure is disarmed by the step that proves the face heals, and
a last step asserts nothing was left armed. The phone walk gets a safe step of
its own — a terminal transfer row written into the store and taken away,
nothing fetched and nothing reaching the shade. ANDROID.md now names what the
walk still cannot see: three screens that draw a raw backend string, six error
faces with no expectation, and the history mirror printed on every step and
asserted on none.
This commit is contained in:
KeelrLeek committed 2026-09-14 03:59:09 -05:00
1 parent f2cde4ec7c
commit 537bfdd5af
3 files changed
+235 -5

No files matched your search

+47 -3
View File
@@ -940,6 +940,39 @@ merely not there yet (a fresh install still works), and refuse with
into a row under Library folder with the path and what to do about it, and the
toast says it in English.
**What the walk has seen.** The round that wrote all of the above was straight
about what it had not proved: six of these fixes shipped with no expectation at
all, because neither `loadHome` nor `loadLyrics` was reachable from the `__ss`
handle the walk drives the app through, and both answer a second ask with
nothing — `loadHome` is gated on `started`, `loadLyrics` on the track's own
`key` — so no amount of tapping re-runs them. They are driven now: Home's
`catalogDown`, the Gaps line, a catalog record's stub, the artist page's two
different sentences, a playlist that would not read, the lyrics `failed` face,
and `failed · <sentence>` on a transfer row. Each is armed with
`__ssMock.fail`, asserted both by the words a person reads and by the Rust
string being absent from them, and disarmed by the step that proves the face
heals. The handle grew four names for it and no more — `home` and `loadHome`,
`lyr` and `loadLyrics` — because a test seam that exports the internals
wholesale stops being a seam. The toast's own **Try again** is pressed for the
first time too, through You → **Rescan library**: `lib.error` is written by
`loadLibrary` alone, and that row is the only door in the app that reaches it
a second time.
What is still unseen, honestly. **Three sites draw a raw backend string**, so
the rule above has holes: `screens/Browse.svelte` — a peer's shares that would
not read, which is a full error face, Try again and all, that was simply missed
— and `screens/Import.svelte`, twice. `SyncRoom.svelte` is a fourth, by way of
`saveTapSync`'s catch. **Six error faces have no expectation either**: Browse's,
Community's "The server did not answer.", Room's "Not in this room.", Album's
"That record is gone.", Playlist's "Playlist not found." and Peer's "Not
compared yet." — all reachable, the first two through the mock's weather and
the rest by pushing a nav entry with a dead id. And of the probe's fields, six
are still read out on every step and asserted on none: `accent`, `xlive`, `hd`,
`hwant`, `navMs` and `a11yWho`. The cheapest real win left in the harness is a
global `hd === hwant` check on every named step — the history mirror is how the
blank page after a confirmed delete was found — but it would fire during the
first-run reloads and needs its own thinking about the suppress window first.
## The downloads in the shade
Transfers is a screen inside the app; the desktop says what a Save is doing
@@ -1255,7 +1288,14 @@ weather: a library index that will not read and its Try again, the client taken
offline, a refused password and a server that cannot be reached, Search with
nowhere to search, a room list that never arrives, and a rescan that refuses
because the folder is gone — then all of it cleared and the app left as it was
found. The mock answers every command the phone
found. After the weather come the faces that failure actually draws, each one
armed by the mock and read back by its own words: Home with every follow's
catalog page refused, the Gaps line and its second tap, a catalog record's stub
and its Try again, an artist page whose lookup failed beside one that was
answered with nothing, a playlist that would not read, the lyrics face on a
lookup that never came back, a transfer that was refused, and the library
toast's own Try again pressed for the first time — then all of that disarmed
too, and asserted disarmed. The mock answers every command the phone
uses; one it does not know is named in the report (`[mock] no mock for X`)
rather than surfacing as a TypeError toast, console warnings carry their top
frames (the smoke bundle is built unminified for that), and every probe
@@ -1269,8 +1309,12 @@ backend funnels through `with_client()` answers "not connected to Soulseek",
the two connects answer the server string, and `soulseek:state` goes out — and
`clear()` puts all of it back. They reject with a plain string because that is
what Tauri rejects with. Fourteen mock steps drive them, and the phone walk carries
one safe step of its own (`trouble-words`: eleven real backend strings through
`__ss.say` on the device, every output asserted free of Rust). A mock step can
two safe steps of its own (`trouble-words`: eleven real backend strings through
`__ss.say` on the device, every output asserted free of Rust; and
`trouble-xfer`: one already-failed transfer row written into the store and
taken away again — nothing fetched, and a terminal row is not in flight, so
nothing reaches the shade). Fifteen more mock steps drive the failure faces
themselves, which are described under **When things go wrong**. A mock step can
also set the
viewport — `{ view: [915, 411] }` sends `Emulation.setDeviceMetricsOverride`
through the same client, which is how the walk turns the phone over and back;
+3 -2
View File
@@ -36,7 +36,8 @@
import Diagnostics from "./screens/Diagnostics.svelte";
import { transfers, bindTransfers, active as activeTransfers } from "./transfers.svelte.js";
import { xfernote, syncShade, bindXferNote } from "./xfernote.svelte.js";
import { bindHome } from "./home.svelte.js";
import { home, bindHome, loadHome } from "./home.svelte.js";
import { lyr, loadLyrics } from "./lyrics.svelte.js";
import Stub from "./screens/Stub.svelte";
import { lib, loadLibrary, refreshRecents, albumById, albumsBy, watchLibrary } from "./lib.svelte.js";
import { art, wantArt, accentFor, applyAccent } from "./art.svelte.js";
@@ -316,7 +317,7 @@
requestAnimationFrame(() => requestAnimationFrame(() => { clearTimeout(sealTimer); sealBoot(); }));
/* the probe handle — the desktop has __soulstream for the same reason:
tests/mobile/smoke.mjs drives the real app through it */
window.__ss = { nav, top, go, push, back, openSheet, closeSheet, openHalf, closeHalf, player, lib, art, catalog, slsk, boot, net, resolver, streamSong, streamCatalogAlbum, sources, api, transfers, xfn: xfernote, pls, notice, importer, imp: { startFolder, startFile }, setup, openSetup, setupState, wifi: { downloadsAllowed, cellular }, next, prev, toggle, seekSecs, playQueueAt, patch, contacts, loadContacts, say, refreshRooms };
window.__ss = { nav, top, go, push, back, openSheet, closeSheet, openHalf, closeHalf, player, lib, art, catalog, home, loadHome, lyr, loadLyrics, slsk, boot, net, resolver, streamSong, streamCatalogAlbum, sources, api, transfers, xfn: xfernote, pls, notice, importer, imp: { startFolder, startFile }, setup, openSetup, setupState, wifi: { downloadsAllowed, cellular }, next, prev, toggle, seekSecs, playQueueAt, patch, contacts, loadContacts, say, refreshRooms };
return () => { if (unlisten) unlisten(); };
});
</script>
+185
View File
@@ -347,6 +347,26 @@ const PROBE = `(function () {
folderGone: (document.querySelector('.folder-gone') || {}).textContent || '',
lyrNote: (document.querySelector('.lyr-note') || {}).textContent || '',
lyrTools: !!document.querySelector('.lyr-tools'),
/* ── the faces round thirty-one drew and no step had ever read ──
Home's own line and the slots behind it (how many follows it
walked, how many came back empty-handed); the catalog's note on
an artist page; whatever stub is up, whitespace collapsed; the
Gaps line, the BUTTON only — the '.gapline.done' variant is a
div and says something else entirely; the failed transfer's
sentence; and whether the lyrics lookup itself failed, as
opposed to having found nothing. Every backslash here is doubled
and there is not one backtick, comment included — this block
lives inside a template literal, and a backtick would END it
(the file still PARSES, so node --check says nothing: what it
becomes is a tagged template, and smoke.mjs throws on import). */
catDown: (document.querySelector('.sec-note.cat-down') || {}).textContent || '',
homeSlots: (function () { try { return __ss.home.order.length; } catch (e) { return -1; } })(),
homeErr: (function () { try { return __ss.home.order.filter(function (id) { var s = __ss.home.slots[id]; return !!(s && s.error); }).length; } catch (e) { return -1; } })(),
catNote: (function () { var n = [].slice.call(document.querySelectorAll('.screen .sec-note:not(.cat-down)')).find(function (x) { return /catalog/i.test(x.textContent || ''); }); return n ? n.textContent.replace(/\\s+/g, ' ').trim() : ''; })(),
stubText: ((document.querySelector('.screen .stub') || {}).textContent || '').replace(/\\s+/g, ' ').trim(),
gapLine: ((document.querySelector('button.gapline') || {}).textContent || '').replace(/\\s+/g, ' ').trim(),
xferFail: (document.querySelector('.row.xfer.failed .m') || {}).textContent || '',
lyrFailed: (function () { try { return __ss.lyr.failed ? 'yes' : 'no'; } catch (e) { return ''; } })(),
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 ''; } })(),
@@ -971,6 +991,105 @@ const MOCK_STEPS = [
{ 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 },
/* ── the faces no walk had seen ─────────────────────────────────────
Round thirty-one gave six screens an honest failure and its own
verifier was straight about what it had shipped unobserved: Home's
`catalogDown`, the lyrics `failed` note, Gaps' `gapErr`, Playlist's
`loadErr`, the catalog record's and the artist page's stubs, and
`failed · <sentence>` on a transfer row. It could drive none of them
because neither `loadHome` nor `loadLyrics` was reachable from the
`__ss` handle — both are gated on `started` / `key` and answer a
second ask with nothing at all, so no amount of tapping re-runs
them. The handle carries the two stores and their two loaders now,
and this block is what they were added for.
It sits HERE, before the first run, and not at the end of the list:
the block below reloads the page twice, and the mock's follows live
in a module-level `let` that a reload empties — after `fresh-off`
nobody is followed, and Home's `catalogDown` (which needs a follow)
and the Gaps shelf (which IS the follows) are both structurally
unreachable. Here the block inherits what the weather left behind:
signed in, on Home, Daft Punk followed, a track on the mini bar.
Every assertion is on settled state — an armed failure stays armed,
an error stub stays up, the toast stays until it is dismissed — and
the two forced loads return their promise, which `cdp.evaluate`
awaits, so the fetch is over before the step's wait even begins.
Every failure is disarmed by the step that proves the face heals,
and `faces-clear` is the proof that nothing was left armed. */
{ do: `(function () { __ssMock.clear(); __ss.player.error = ''; __ss.lib.error = ''; __ss.player.retry = null; return __settle(); })()`, wait: 800 },
/* Home with every follow's catalog page refused. `home.svelte.js` has
always set `slot.error` there; until round thirty-one nothing read
it, and until this step nothing had ever seen the line that does. */
{ do: `(function () { __ssMock.fail('catalog_artist', 'io: failed to lookup address information: no address associated with hostname'); return __ss.loadHome(true); })()`, name: "faces-home-down", wait: 1200 },
/* the same armed failure, on the other screen that asks the same
command: Gaps. Home stays poisoned through these three steps on
purpose — it is asserted above, and the second tap below is what
clears the weather for both. */
{ do: `document.querySelectorAll('.nav button')[2].click()`, wait: 800 },
{ do: `__click('.segtabs button', 'Gaps')`, wait: 700 },
{ do: `__click('.gapline', null)`, name: "faces-gap-down", wait: 1400 },
/* a failed lookup leaves `gapRows[key]` undefined, so the second tap
genuinely asks again rather than merely opening a rail */
{ do: `__ssMock.fail('catalog_artist', '')`, wait: 300 },
{ do: `__click('.gapline', null)`, name: "faces-gap-again", wait: 1600 },
/* a catalog record. The walk has already made 302127 `ready` and
`lookupAlbum` returns early on anything but an error, so the entry is
forgotten first — the smallest honest way to say "ask again". */
{ do: `(function () { delete __ss.catalog.albums[302127]; return __ssMock.fail('catalog_album', 'io: connection reset by peer'); })()`, wait: 300 },
{ do: `__ss.push('catalogAlbum', { id: 302127, artist: 'Daft Punk', title: 'Discovery' })`, name: "faces-album-down", wait: 1500 },
{ do: `__ssMock.fail('catalog_album', '')`, wait: 300 },
{ do: `__click('.imp-foot .btn', 'Try again')`, name: "faces-album-again", wait: 1600 },
{ do: `__ss.back()`, wait: 700 },
/* the artist page has TWO of these and they say different things: a
lookup that FAILED is "didn't answer", one that came back empty is
"had nothing to add". `bestArtistFor` is what has to fail for the
first — failing `catalog_artist` alone never reaches the page at all
and leaves the status at none, which is the other copy. */
{ do: `__ssMock.fail('catalog_search', 'io: failed to lookup address information: no address associated with hostname')`, wait: 250 },
{ do: `__ss.push('artist', { name: 'Hollow Signal' })`, name: "faces-artist-down", wait: 1600 },
{ do: `(function () { __ssMock.fail('catalog_search', ''); __ss.back(); return true; })()`, wait: 700 },
{ do: `__ss.push('artist', { name: 'Hollow Signal' })`, name: "faces-artist-none", wait: 1600 },
{ do: `__ss.back()`, wait: 700 },
/* a playlist that will not read. Id 1 survives the whole walk — it is
the one renamed to "Late night drive II"; the one deleted was New
Playlist. Try again clears `loadErr` and bumps `again`, which is
what re-arms the effect. */
{ do: `__ssMock.fail('playlist_items', 'unable to open database file')`, wait: 250 },
{ do: `__ss.push('playlist', { id: 1 })`, name: "faces-playlist-down", wait: 1400 },
{ do: `__ssMock.fail('playlist_items', '')`, wait: 300 },
{ do: `__click('.imp-foot .btn', 'Try again')`, name: "faces-playlist-again", wait: 1400 },
{ do: `__ss.back()`, wait: 700 },
/* the lyrics face. Sheet.svelte asks with the UN-forced `loadLyrics()`,
which returns at once while `lyr.key` is already this track's — so
the face alone can never ask again, which is exactly why round
thirty-one could not drive this one. The loader, forced. */
{ do: `(function () { __ss.openSheet(); return true; })()`, wait: 900 },
{ do: `document.querySelectorAll('.fbtn')[1].click()`, wait: 900 },
{ do: `(function () { __ssMock.fail('lyrics_get', 'io: failed to lookup address information: no address associated with hostname'); return __ss.loadLyrics(true); })()`, name: "faces-lyrics-down", wait: 1200 },
{ do: `(function () { __ssMock.fail('lyrics_get', ''); return true; })()`, wait: 250 },
{ do: `__ss.loadLyrics(true)`, name: "faces-lyrics-again", wait: 1200 },
{ do: `(function () { document.querySelectorAll('.fbtn')[0].click(); __ss.closeSheet(); return true; })()`, wait: 700 },
/* a transfer that was refused, written straight into the store the way
the shade steps above do — nothing is fetched, and `failed` is in
DONE, so `xfernote.shape()` sees nothing in flight and posts no
notice. The id is far above the backend's own counter, and `list()`
sorts id-descending, so this row is the first one on the screen. */
{ do: `(function () { __ss.transfers.items[990011] = { id: 990011, username: 'walk_peer', path: 'Walk\\\\walk-c.flac', name: 'walk-c.flac', state: 'failed', queue_place: null, local_path: null, error: 'transfer rejected: File not shared.', received: 0, total: 0 }; __ss.push('transfers', {}); return true; })()`, name: "faces-xfer-failed", wait: 1300 },
{ do: `(function () { delete __ss.transfers.items[990011]; __ss.back(); return true; })()`, wait: 800 },
/* the one toast action round thirty-one armed that no step had ever
pressed. `lib.error` is written by `loadLibrary` alone —
`watchLibrary`'s own reload swallows its failure — so You → Rescan
library is the only door in the app that reaches it twice. */
{ do: `document.querySelectorAll('.nav button')[4].click()`, wait: 800 },
{ do: `__ssMock.fail('library_albums', 'unable to open database file')`, wait: 250 },
{ do: `__click('.screen .row', 'Rescan library')`, name: "faces-library-toast", wait: 1500 },
{ do: `__ssMock.fail('library_albums', '')`, wait: 300 },
{ do: `__click('.toast:not(.notice) .na', 'Try again')`, name: "faces-library-again", wait: 1400 },
/* and all of it put away, so the first run below starts exactly where
it would have started without this block at all */
{ do: `(function () { __ssMock.clear(); __ss.player.error = ''; __ss.lib.error = ''; __ss.player.retry = null; __settle(); return __ss.loadHome(true); })()`, wait: 900 },
{ name: "faces-clear", wait: 1200 },
/* ── the first run, on a device that has just been installed ──────
The mock is the whole backend here, so it can answer as one: no
records, no playlists, no account, no all-files permission, and the
@@ -1108,6 +1227,46 @@ const MOCK_EXPECT = {
"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),
/* ── the faces no walk had seen ─────────────────────────────────────
Each of these fails if round thirty-one's fix is undone: stop reading
`slot.error` and Home's line empties; drop the `gapErr` branch and
the Gaps line falls back to "See what's missing" and can be tapped
for ever; put `lyr.failed` back and the face says "No lyrics found"
about a lookup that never answered; take `say()` out of `stateLine`
and the raw string is on the row again. Every one asserts the
sentence a person reads AND that the Rust string is not in it — the
second half is the round-thirty-one rule, and the half that survives
any rewording of the first. */
"faces-home-down": (i) => i.homeSlots > 0 && i.homeErr === i.homeSlots
&& /^Couldn't reach the catalog, so there's nothing new/.test(i.catDown)
&& !/New from your artists/.test(i.secs),
"faces-gap-down": (i) => /Couldn't reach the catalogue — tap to try again/.test(i.gapLine) && !i.gapCards,
"faces-gap-again": (i) => /more released/.test(i.gapLine) && !/Couldn't reach/.test(i.gapLine) && i.gapCards >= 3,
"faces-album-down": (i) => /^The catalog did not answer/.test(i.why) && /The network didn't answer/.test(i.why)
&& !/io:|connection reset/.test(i.why) && i.again && !i.trks,
"faces-album-again": (i) => !i.why && !i.again && i.trks > 0,
/* the pair that proves the distinction is real and not decoration */
"faces-artist-down": (i) => /only what this phone holds is here/.test(i.catNote) && /didn't answer/.test(i.stubText)
&& !/io:|address information/.test(i.catNote + i.stubText),
"faces-artist-none": (i) => !i.catNote && /had nothing to add/.test(i.stubText) && !/didn't answer/.test(i.stubText),
"faces-playlist-down": (i) => /^Couldn't read this playlist/.test(i.why) && /library folder isn't readable/.test(i.why)
&& i.again && !i.plrows && !/database file/.test(i.why),
"faces-playlist-again": (i) => !i.why && !i.again && i.plrows > 0,
"faces-lyrics-down": (i) => i.lyrFailed === "yes" && !i.lyrLines && /^Couldn't look up the words/.test(i.lyrNote)
&& i.lyrTools && !/io:|address information/.test(i.lyrNote),
"faces-lyrics-again": (i) => i.lyrFailed === "no" && i.lyrLines >= 3 && !i.lyrNote,
"faces-xfer-failed": (i) => i.title.startsWith("Transfers")
&& /failed · They refused the transfer \(File not shared\)/.test(i.xferFail)
&& !/transfer rejected/.test(i.xferFail),
"faces-library-toast": (i) => /library folder isn't readable right now/.test(i.toast) && i.retry === "Try again"
&& !/database file/.test(i.toast),
"faces-library-again": (i) => i.title.startsWith("You") && !i.toast && !i.retry,
/* the disarm, proved: no toast, no armed action, no stub, Home's slots
healthy and its rails back — so the first run below is not quietly
running on something this block left standing */
"faces-clear": (i) => !i.toast && !i.why && !i.catDown && !i.retry
&& i.homeSlots > 0 && i.homeErr === 0
&& /New from your artists/.test(i.secs) && /^(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
@@ -1604,6 +1763,17 @@ const PHONE_STEPS = [
{ do: `document.querySelector('.scrim').click()`, wait: 700 },
{ do: `history.back()`, wait: 800 },
{ do: `history.back()`, wait: 900 },
/* the one face of the six that a real build can show without arming
anything: `__ssMock` does not exist here. One already-terminal row
goes into the transfer store — the same thing the shade steps above
do — and comes straight back out. Nothing is downloaded, no room is
joined, no message is sent, nobody is signed in, and neither the
config nor the library is written. A `failed` row is not in flight,
so no notice is posted either. The id is far above the backend's own
counter and `list()` sorts id-descending, so it is the first row on
the screen even if Kyle's own session is carrying real failures. */
{ do: `(function () { __ss.transfers.items[990011] = { id: 990011, username: 'walk_peer', path: 'Walk\\\\walk-c.flac', name: 'walk-c.flac', state: 'failed', queue_place: null, local_path: null, error: 'transfer rejected: File not shared.', received: 0, total: 0 }; __ss.push('transfers', {}); return true; })()`, name: "trouble-xfer", wait: 1400 },
{ do: `(function () { delete __ss.transfers.items[990011]; __ss.back(); return true; })()`, name: "trouble-xfer-gone", wait: 1000 },
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "end", wait: 800 },
];
const PHONE_EXPECT = {
@@ -1737,6 +1907,21 @@ const PHONE_EXPECT = {
"dg-page": (i) => i.dgTabs >= 1 && i.dgRows >= 1 && !i.toast && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
"dg-sheet": (i) => i.half.includes("up") && /By year/.test(i.halfText) && !i.toast,
"dg-az": (i) => i.dgRows >= 1 && /A–Z/.test(i.dgOrder) && !i.toast,
/* the row's own words on the device, and then its absence. The "gone"
clause is written against THIS row's sentence rather than against
`xferFail` being empty: a real failed transfer of Kyle's sitting in
the list is not a reason to fail the step, and the real core
answering faster than a mock would cannot change either reading —
both are settled state, written and removed by the step itself. The
screen underneath is deliberately not named: sibling rounds append
their own phone steps before `end` too, so whichever screen the back
lands on is theirs, not this round's. What IS asserted is that the
back popped at all — a sheet left up would close instead and the bar
would still say Transfers. */
"trouble-xfer": (i) => i.title.startsWith("Transfers")
&& /failed · They refused the transfer \(File not shared\)/.test(i.xferFail)
&& !/transfer rejected/.test(i.xferFail),
"trouble-xfer-gone": (i) => !/^Transfers/.test(i.title) && !/File not shared/.test(i.xferFail),
end: (i) => /^(Good |Still)/.test(i.title),
};