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
+81
-3
@@ -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
|
||||
|
||||
Reference in new issue
Block a user