// Round thirty-four: the first run. Every walk and every deploy started
// from Kyle's phone — signed in, folder set, a config full of his
// settings — and nobody had seen what this app looks like on a phone that
// has just installed it. On Android the library root is never missing (it
// falls back to the app's own invisible sandbox), `setup_done` has never
// been written by the phone so EVERY phone reads as unstamped, and signed
// out is answered honestly everywhere and helpfully nowhere. So: one store,
// a two-card sheet the boot gate opens only on a phone that is genuinely
// fresh (three clauses, all required), the note with the row that fixes it
// on the four screens that are empty because of the same two things, a
// preview door in You that writes nothing, and a fresh-install mode in the
// mock so the whole thing is walkable end to end. No password is handled
// anywhere in it — the Soulseek step navigates to the card and stops.
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
let p, s;
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
const save = () => fs.writeFileSync(root + p, s);
const once = (a) => {
const i = s.indexOf(a);
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
return i;
};
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
const NL = String.fromCharCode(10);
/* The shell.css block is an APPEND with no anchor and several inserts
below re-emit their own anchors, so a second run would write twice where
`once` cannot see it coming and only throw further down, leaving half a
tree. Refuse one here, before anything is touched: the store is the file
this round creates and nothing else in the repo makes it. */
if (fs.existsSync(root + 'src/mobile/setup.svelte.js')) {
throw new Error('src/mobile/setup.svelte.js already exists — round 34 has already run against ' + root);
}
// ── the store, and the note that shares it ────────────────────────────
fs.writeFileSync(root + 'src/mobile/setup.svelte.js', ins('ins-r34-setup-store.txt') + NL);
fs.writeFileSync(root + 'src/mobile/SetupNote.svelte', ins('ins-r34-setupnote.txt') + NL);
// ── signed out has to be a FACT, not an absence ───────────────────────
load('src/mobile/slsk.svelte.js');
rep(` /** the saved username, for the sign-in card */
savedUser: "",
hasPassword: false,`,
` /** the saved username, for the sign-in card */
savedUser: "",
hasPassword: false,
/** the credentials have been READ — see loadCredentials */
credsRead: false,`);
rep(` slsk.hasPassword = !!c?.has_password;
} catch { /* fine */ }
}`,
` slsk.hasPassword = !!c?.has_password;
} catch { /* fine */ }
/* read, whatever it said. "Signed out" is a fact the first run has to
be able to state, and before this resolves every phone in the world
merely LOOKS signed out — a note that says so then is a lie with
good timing. */
slsk.credsRead = true;
}`);
save();
// ── the shell: the config at boot, the gate after the two answers, and
// the stamp whichever door closed the sheet ──────────────────────────
load('src/mobile/Mobile.svelte');
rep(` import { loadPrefs } from "./prefs.svelte.js";`,
` import { loadPrefs } from "./prefs.svelte.js";
import { setup, loadSetup, maybeOpenFirstRun, openSetup, finishSetup, setupState } from "./setup.svelte.js";`);
rep(` $effect(() => { syncShade(); });`,
` $effect(() => { syncShade(); });` + NL + NL + ins('ins-r34-mobile-effect.txt'));
rep(` hooks.recentsDirty = refreshRecents;
bindMedia();
loadLibrary();
/* the network: subscribe first, then the saved account signs in on its own */
bindSlsk().then(() => connectStored()).catch(() => {});`,
ins('ins-r34-mobile-boot.txt'));
rep(`wifi: { downloadsAllowed, cellular }, next, prev, toggle`,
`setup, openSetup, setupState, wifi: { downloadsAllowed, cellular }, next, prev, toggle`);
save();
// ── the sheet ─────────────────────────────────────────────────────────
load('src/mobile/HalfSheet.svelte');
rep(` import { downloadsAllowed } from "./network.svelte.js";`,
` import { downloadsAllowed } from "./network.svelte.js";
import { setup, needsFolder, needsAccount, chooseFolder, requestAccess, finishSetup } from "./setup.svelte.js";`);
rep(` {:else if kind === "sleep"}`,
ins('ins-r34-halfsheet.txt') + NL + ` {:else if kind === "sleep"}`);
save();
// ── Home: the stale empty state (its copy predates the picker in You) ──
load('src/mobile/screens/Home.svelte');
rep(` import Release from "../Release.svelte";`,
` import Release from "../Release.svelte";
import SetupNote from "../SetupNote.svelte";`);
rep(`{:else if lib.loaded}
Nothing in the library on this phone yet.
Music the phone can play lives in the app's own Library folder for now; follow artists and Save records from the catalog, and this page fills up.
{/if}`,
`{:else if lib.loaded}
Nothing in the library on this phone yet.
Save records from the catalog and follow the artists you collect, and this page fills up with what they release.
{/if}`);
save();
// ── Library: the albums stub, and the Artists tab, which had no empty
// state at all — a count of zero over an empty each block ────────────
load('src/mobile/screens/Library.svelte');
rep(` import Release from "../Release.svelte";`,
` import Release from "../Release.svelte";
import SetupNote from "../SetupNote.svelte";`);
rep(` Nothing here yetThe library on this phone is empty.
`,
` Nothing here yetThe library on this phone is empty.
`);
rep(`{:else if tab === "artists"}
{arts.length} artists · {lib.follows.length} followed
`,
`{:else if tab === "artists"}
{arts.length} artists · {lib.follows.length} followed
{#if !arts.length}
No artists yetEvery artist with a record in the library on this phone is listed here.
{/if}`);
save();
// ── Search: the flat sentence becomes the row that does it ────────────
load('src/mobile/screens/Search.svelte');
rep(` import Release from "../Release.svelte";`,
` import Release from "../Release.svelte";
import SetupNote from "../SetupNote.svelte";`);
rep(` {#if !online}Sign in on Community to play what the library does not hold.{/if}
`,
`
`);
save();
// ── You: one test for the private folder, and the preview door ────────
load('src/mobile/screens/You.svelte');
rep(` import { settings, setSetting } from "../prefs.svelte.js";`,
` import { settings, setSetting } from "../prefs.svelte.js";
import { setup, openSetup, loadSetup, isPrivateFolder } from "../setup.svelte.js";`);
/* one wrong test is better than two: the regex lives in the store now */
rep(` const privateFolder = $derived(!cfg?.download_dir || /\\/data\\/(user\\/\\d+\\/|data\\/)com\\.soulstream/.test(cfg.download_dir));`,
` const privateFolder = $derived(isPrivateFolder(cfg?.download_dir));`);
/* and the two doors onto the same folder are told about each other: this
screen re-reads when the tour it opens moves the folder, and the tour's
store is told when this screen's own picker does — otherwise the note on
Home goes on offering a folder that has already been picked here. */
rep(` $effect(() => { api.getSettings().then((s) => { cfg = s; }, () => {}); });`,
` $effect(() => { void setup.folder; api.getSettings().then((s) => { cfg = s; }, () => {}); });`);
rep(` await api.setSettings({ downloadDir: r.path });
cfg = await api.getSettings();
await rescan();`,
` await api.setSettings({ downloadDir: r.path });
cfg = await api.getSettings();
void loadSetup();
await rescan();`);
rep(` Soulseek{lib.username || "not signed in"}
`,
` Soulseek{lib.username || "not signed in"}
`
+ NL + ins('ins-r34-you-row.txt'));
save();
// ── the look: almost nothing, because both surfaces are the app's own
// parts. Appended, so the guard above is what refuses a second run.
load('src/mobile/shell.css');
s = s.replace(/\s*$/, NL) + NL + ins('ins-r34-css.txt') + NL;
save();
// ── the mock answers as a fresh install ───────────────────────────────
load('src/lib/mockLibrary.js');
rep(`const enabled = (() => {`, ins('ins-r34-mock-fresh.txt') + NL + NL + `const enabled = (() => {`);
/* The fresh gate sits BELOW round 31's weather gate, not above it. The fresh
block short-circuits nine commands, so anchoring on `if (!enabled)` would
put it first and quietly make those nine unarmable — a fresh phone whose
database will not open is exactly the case a later test will want. */
rep(` const forecast = weather(cmd);
if (forecast) return forecast;`,
` const forecast = weather(cmd);
if (forecast) return forecast;` + NL + ins('ins-r34-mock-empty.txt'));
rep(` if (cmd === "get_credentials") return Promise.resolve({ username: MOCK_USER, has_password: true });`,
` if (cmd === "get_credentials") return Promise.resolve(fresh ? { username: "", has_password: false } : { username: MOCK_USER, has_password: true });`);
rep(` if (cmd === "plugin:storage|all_files_access") return Promise.resolve({ granted: true });`,
` if (cmd === "plugin:storage|all_files_access") return Promise.resolve({ granted: !fresh });`);
/* a provisioned mock is a phone that HAS been set up — say so, rather than
leaving the field absent and the flag false by omission */
rep(` volume_normalization: true,`, ` volume_normalization: true,` + NL + ` setup_done: true,`);
rep(`const cacheReady = new Set(); // paths a preload or a finished stream has fully fetched`,
ins('ins-r34-mock-settings.txt') + NL + `const cacheReady = new Set(); // paths a preload or a finished stream has fully fetched`);
rep(` if (cmd === "player_state") return Promise.resolve({ status: "idle", position: 0 });`,
` if (cmd === "player_state") return Promise.resolve({ status: "idle", position: 0 });` + NL + ins('ins-r34-mock-transport.txt'));
save();
// ── the walks. The two list anchors are the ARRAY ENDS, not a step: a
// sibling round appending its own block must not push mine into the
// middle of the walk, because mine reloads the page.
load('tests/mobile/smoke.mjs');
rep(` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),`,
` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),`
+ NL + ins('ins-r34-probe.txt'));
rep(`];
const MOCK_EXPECT = {`, ins('ins-r34-mock-steps.txt') + NL + `];
const MOCK_EXPECT = {`);
rep(`};
/* the phone walk: a record by name, played for real */`,
ins('ins-r34-mock-expect.txt') + NL + `};
/* the phone walk: a record by name, played for real */`);
rep(` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "end", wait: 800 },`,
ins('ins-r34-phone-steps.txt') + NL + ` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "end", wait: 800 },`);
/* The mock leg reloads for the first time in this round, and the viewport
emulation lives on the DevTools SESSION rather than on the page: the
re-attach after a reload drops it, and every still from there on would
be the Chrome window's own size at 1× with the phone's touch emulation
gone. Keep what was set and put it back. */
rep(` await cdp.send("Emulation.setDeviceMetricsOverride", { width: 411, height: 914, deviceScaleFactor: 2, mobile: true });`,
` await cdp.send("Emulation.setDeviceMetricsOverride", metrics);`);
rep(` if (!PHONE) {
await cdp.send("Page.addScriptToEvaluateOnNewDocument"`,
` /* the emulated phone, kept: a re-attach has to be handed it again */
let metrics = { width: 411, height: 914, deviceScaleFactor: 2, mobile: true };
if (!PHONE) {
await cdp.send("Page.addScriptToEvaluateOnNewDocument"`);
rep(` const r = await cdp.send("Emulation.setDeviceMetricsOverride", { width: s.view[0], height: s.view[1], deviceScaleFactor: 2, mobile: true });`,
` metrics = { width: s.view[0], height: s.view[1], deviceScaleFactor: 2, mobile: true };
const r = await cdp.send("Emulation.setDeviceMetricsOverride", metrics);`);
rep(` try { await reattach(); } catch (e) { problems.push("STEP " + (s.name || "reload") + ": could not re-attach after the reload: " + e.message); }`,
` try {
await reattach();
if (!PHONE) await cdp.send("Emulation.setDeviceMetricsOverride", metrics);
} catch (e) { problems.push("STEP " + (s.name || "reload") + ": could not re-attach after the reload: " + e.message); }`);
rep(` end: (i) => /^(Good |Still)/.test(i.title),`,
ins('ins-r34-phone-expect.txt') + NL + ` end: (i) => /^(Good |Still)/.test(i.title),`);
save();
// ── the doc ───────────────────────────────────────────────────────────
load('ANDROID.md');
rep(`## Settings that reach the config`, ins('ins-r34-doc.txt') + NL + NL + `## Settings that reach the config`);
/* round thirty-one called its weather block the last one in the mock walk,
which was true until this round put the fresh install after it */
rep(`any exception or console error. The block at the end of the mock walk is the
weather:`,
`any exception or console error. Near the end of the mock walk is the
weather:`);
/* the clause after this one ("asserts what each step should show") is round
thirty-two's now — it hangs the launch budget off it — so the anchor stops
at the step list's own last item, which nothing else rewrites. */
rep(`all at 915×411) and back,`,
`all at 915×411) and back → and last, on a reload of its own, a phone that
has just been installed (the first-run sheet and its two cards, its
Soulseek door, the notes on Home, on both Library tabs and on Search, the
preview from You, a folder picked from the tour and the note losing the row
it answered), then back to the provisioned device,`);
rep(`- **Per library folder.** Follows and the play history live in the library
folder, so the phone's own folder starts empty of both; they arrive with a
shared library.`,
`- **Per library folder.** Follows and the play history live in the library
folder, so the phone's own folder starts empty of both; they arrive with a
shared library.
` + ins('ins-r34-doc-notdone.txt'));
save();
console.log('round 34 wired');