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.
This commit is contained in:
1 parent
867fd42972
commit
9f0601027c
400 files changed
+26401
-3
No files matched your search
@@ -0,0 +1,270 @@
|
||||
// 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');
|
||||
Reference in new issue
Block a user