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:
KeelrLeek committed 2026-09-14 07:57:41 -05:00
1 parent 867fd42972
commit 9f0601027c
400 files changed
+26401 -3

No files matched your search

+81 -3
View File
@@ -1873,7 +1873,7 @@ const PHONE_STEPS = [
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 500 },
{ do: `(function () { var all = document.querySelectorAll('.grid2 .release'); var c = null; for (var i = 0; i < all.length; i++) { if (!${albumText} || all[i].textContent.indexOf(${albumText}) >= 0) { c = all[i]; break; } } if (!c) throw new Error('no album card'); c.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true })); })()`, wait: 900 },
{ do: `__click('.halfsheet .row', 'Add to playlist')`, name: "playlist-pick", wait: 800 },
{ do: `__click('.halfsheet .row', 'Walk test')`, name: "playlist-added", wait: 1500 },
{ do: `__click('.halfsheet .row', 'Walk test')`, name: "playlist-added", wait: 2600 },
{ do: `__click('.segtabs button', 'Playlists')`, wait: 800 },
{ do: `__click('.plgrid .release', 'Walk test')`, name: "playlist-filled", wait: 1500 },
/* dragged on the real core: the second row to the top and straight
@@ -2334,7 +2334,11 @@ const PHONE_EXPECT = {
playlists: (i) => i.title.startsWith("Your library"),
"playlist-new": (i) => i.screen.includes("tr-push") && i.title.startsWith("Walk test") && i.plrows === 0 && !i.toast,
"playlist-pick": (i) => i.half.includes("up") && /Walk test/.test(i.halfText),
"playlist-added": (i) => /Added .* to Walk test/.test(i.notice) && !i.toast,
/* the notice is transient and the device writes nine rows faster than
the probe can land on it — so either the notice is still up, or the
sheet has closed behind a finished add. playlist-filled proves the
content either way. */
"playlist-added": (i) => (/Added .* to Walk test/.test(i.notice) || !i.half.includes("up")) && !i.toast,
"playlist-filled": (i) => i.title.startsWith("Walk test") && i.plrows >= 1,
/* the drags, tolerant of a real library the walk did not choose: a
one-track record makes a one-row playlist, and a one-row list has
@@ -2671,10 +2675,18 @@ async function main() {
console.log("the app is up (" + ((Date.now() - upFrom) / 1000).toFixed(1) + "s after attaching)");
} else {
if (!CHROME) throw new Error("Chrome not found");
/* ship-fast.sh builds dist-smoke itself so it can start the deploy at
the same time as this walk — two vite builds racing for the same
dependency cache is not worth the four minutes. SS_SKIP_BUILD says
it is already there and current. */
if (process.env.SS_SKIP_BUILD === "1" && fs.existsSync(path.join(ROOT, "dist-smoke", "mobile.html"))) {
console.log("using the dist-smoke/ that was built for us");
} else {
console.log("building dist-smoke/ …");
/* unminified, so a warning's frames name the function */
const b = spawnSync("npx", ["vite", "build", "--outDir", "dist-smoke", "--base", "./", "--minify", "false"], { cwd: ROOT, shell: true, encoding: "utf8" });
if (b.status !== 0) { console.error(b.stdout, b.stderr); throw new Error("vite build failed"); }
}
/* a profile per run: a Chrome left behind by an interrupted run keeps
its crashpad file open, and removing that profile throws EBUSY */
const profile = path.join(os.tmpdir(), "ss-smoke-profile-" + process.pid);
@@ -2734,6 +2746,70 @@ async function main() {
await cdp.send("Page.navigate", { url: pathToFileURL(path.join(ROOT, "dist-smoke", "mobile.html")).href });
await sleep(600);
}
/* ── waiting only as long as it takes ───────────────────────────
Every step used to sleep its declared wait, sized for the worst
case it might ever meet. At 632 steps that was over half of a
round: 414 seconds declared in the mock list, 223 in the phone's.
So: poll something cheap until it stops changing, then go on. The
fingerprint reads the shape of the page AND the app's own moving
parts, because a step can be waiting on work that has not reached
the DOM yet — a resolve in flight, a transfer arriving, a command
that will only show itself in a toast.
Two properties keep this honest:
· It can only ever SHORTEN a wait, never lengthen one. The
declared wait is the ceiling, so a step that passes today still
gets every millisecond it got before.
· There is a floor. A page can be momentarily quiet in the middle
of work — a backend write with nothing visible behind it looks
settled the instant it is asked — so nothing is probed before a
third of its declared wait has passed.
`hold: true` on a step opts out and sleeps the whole wait, for
the handful that are waiting on something nothing here can see. */
const FINGERPRINT = `(function () {
try {
var t = document.querySelector('.toast');
var ss = window.__ss || {};
var pl = ss.player || {}, rs = ss.resolver || {}, nt = ss.net || {}, lb = ss.lib || {};
return [
document.querySelectorAll('*').length,
document.images.length,
(document.querySelector('.screen') || {}).className || '',
(document.querySelector('.sheet') || {}).className || '',
(document.querySelector('.halfsheet') || {}).className || '',
t ? t.textContent.slice(0, 40) : '',
document.title,
pl.status, pl.phase, Math.round(pl.position || 0),
(pl.queue || []).length, pl.qi,
rs.active ? 1 : 0, nt.busy ? 1 : 0, lb.loading ? 1 : 0,
(ss.transfers && ss.transfers.items ? ss.transfers.items.length : 0)
].join('|');
} catch (e) { return 'unreadable'; }
})()`;
/* two identical readings 90ms apart, and nothing has moved */
const STABLE_READINGS = 2;
const POLL_MS = 90;
const FLOOR = 0.34;
let waited = 0, saved = 0; /* what the walk actually spent, and what it did not */
async function settle(want, step) {
waited += want;
if (step && step.hold) { await sleep(want); return; }
const began = Date.now();
await sleep(Math.min(want, Math.max(200, Math.round(want * FLOOR))));
let last = null, stable = 0;
while (Date.now() - began < want) {
let fp;
try { fp = await cdp.evaluate(FINGERPRINT); } catch { break; }
if (fp === last) { if (++stable >= STABLE_READINGS) break; } else { stable = 0; last = fp; }
const left = want - (Date.now() - began);
if (left <= 0) break;
await sleep(Math.min(POLL_MS, left));
}
saved += Math.max(0, want - (Date.now() - began));
}
for (const s of steps) {
/* a phone step can be an adb command — an intent, a key */
let adbOut = "";
@@ -2768,7 +2844,7 @@ async function main() {
await cdp.evaluate(s.do);
} catch (e) { problems.push("STEP " + (s.name || s.do) + ": " + e.message); }
}
await sleep(s.wait || 500);
await settle(s.wait || 500, s);
if (s.do === "location.reload()") {
try {
await reattach();
@@ -2787,6 +2863,8 @@ async function main() {
console.log((ok ? " ok " : " FAIL ") + s.name.padEnd(16) + " " + JSON.stringify(info));
}
console.log("waits: " + Math.round(saved / 1000) + "s of " + Math.round(waited / 1000) + "s declared was not spent");
/* ── the restart half of session restore, on the phone only ──────
A reload kept the audio thread (tested above); a real restart
loses it. The track must come back on the bar, paused, and play