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.
271 lines
16 KiB
JavaScript
271 lines
16 KiB
JavaScript
// 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');
|