Files
SoulStream/handoff/rounds/patch-round34.cjs
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

271 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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}
<div class="stub">
<b>Nothing in the library on this phone yet.</b>
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.
</div>
{/if}`,
`{:else if lib.loaded}
<div class="stub">
<b>Nothing in the library on this phone yet.</b>
Save records from the catalog and follow the artists you collect, and this page fills up with what they release.
</div>
<SetupNote />
{/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(` <div class="stub"><b>Nothing here yet</b>The library on this phone is empty.</div>`,
` <div class="stub"><b>Nothing here yet</b>The library on this phone is empty.</div>
<SetupNote />`);
rep(`{:else if tab === "artists"}
<div class="sec-note">{arts.length} artists · {lib.follows.length} followed</div>`,
`{:else if tab === "artists"}
<div class="sec-note">{arts.length} artists · {lib.follows.length} followed</div>
{#if !arts.length}
<div class="stub"><b>No artists yet</b>Every artist with a record in the library on this phone is listed here.</div>
<SetupNote />
{/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}
</div>`,
` </div>
<SetupNote />`);
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(` <div class="kv"><span class="k">Soulseek</span><span class="v dim">{lib.username || "not signed in"}</span></div>`,
` <div class="kv"><span class="k">Soulseek</span><span class="v dim">{lib.username || "not signed in"}</span></div>`
+ 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');