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,153 @@
|
||||
/* ── the tree in the car, with the app FORCE-STOPPED ───────
|
||||
LAST in the phone leg, and it kills the app on purpose. The
|
||||
claim round fifty makes is not "the browse tree is correct"
|
||||
but "the browse tree is there when the app is not" — a car
|
||||
that has to be handed a running phone first is no use to
|
||||
anybody — so every check below is taken against a process the
|
||||
SYSTEM built for the browse, with no Activity, no Tauri and no
|
||||
WebView in it.
|
||||
|
||||
What drives it is round forty-nine's prober: a component in
|
||||
its own process that `am start-service` can start cold,
|
||||
connects to our own MediaLibraryService as a framework
|
||||
MediaBrowser and walks the tree. What is ASSERTED is the
|
||||
service's own log of what it answered (see autoNodes()) —
|
||||
which is the one reading a bug in the prober cannot fake.
|
||||
|
||||
Nothing here can say whether AUTO renders any of it. That
|
||||
needs the Desktop Head Unit or a car, and ANDROID.md says so.
|
||||
What this proves is the half that can be proved from here,
|
||||
and it is the half that has always been broken in this port
|
||||
when something was. */
|
||||
{
|
||||
const say = (...a) => { try { return adb(...a).toString(); } catch (e) { return "ERR " + e.message; } };
|
||||
const PROBE = "com.soulstream.app/com.soulstream.mediasession.AutoProbeService";
|
||||
const pidBefore = (say("shell", "pidof", "com.soulstream.app").trim().split(/\s+/)[0]) || "";
|
||||
/* the WebView dies with the process; drop the socket first so
|
||||
the walk's own finally has nothing to trip over */
|
||||
if (cdp) { cdp.close(); cdp = null; }
|
||||
say("shell", "am", "force-stop", "com.soulstream.app");
|
||||
await sleep(3000);
|
||||
say("logcat", "-c");
|
||||
const launched = say("shell", "am", "start-service", "-n", PROBE).trim();
|
||||
/* Polled, never fixed-waited. A cold `dlopen` of a ~100 MB
|
||||
debug .so plus the first SQLite open on a phone is not a
|
||||
number anybody here has measured, and reading a slow
|
||||
answer as a failure has cost this project a round twice.
|
||||
Eight nodes is the shape the assertions want — a root,
|
||||
four tabs and three levels — and sixty seconds is the
|
||||
ceiling, not the wait. */
|
||||
let rows = [];
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await sleep(1000);
|
||||
rows = autoNodes();
|
||||
if (rows.length >= 8 && rows.some((r) => r.node.startsWith("ss:root"))) break;
|
||||
}
|
||||
const seen = rows.map((r) => r.node);
|
||||
|
||||
/* ── the root: within the limit the browser hinted, and all
|
||||
browsable. Google's own rule, and Auto's: only a browsable
|
||||
item may be a root tab, and a root that overflows the
|
||||
limit has its extra tabs quietly dropped or buried. `lim`
|
||||
is what LibraryBridge read out of the root hints, so a
|
||||
hardcoded four would show up here as a limit that never
|
||||
changes rather than as nothing at all. */
|
||||
const root = rows.find((r) => r.node.startsWith("ss:root")) || null;
|
||||
const rootOk = !!root && root.lim >= 1 && root.n >= 1 && root.n <= root.lim
|
||||
&& root.br === root.n && root.pl === 0
|
||||
&& root.uniq === root.n && root.titled === root.n
|
||||
&& root.kinds === "tab";
|
||||
if (!rootOk) problems.push("EXPECT auto-root " + JSON.stringify({ root, launched, seen }));
|
||||
console.log((rootOk ? " ok " : " FAIL ") + "auto-root".padEnd(16) + " "
|
||||
+ JSON.stringify(root ? { n: root.n, lim: root.lim, br: root.br, pl: root.pl, kinds: root.kinds, ms: root.ms } : { root: null, launched }));
|
||||
|
||||
/* ── the four tabs: each loads, each row has a stable id of
|
||||
its own and a title, and every cover the library HAS
|
||||
became a content:// URI (`fail` is the count that did
|
||||
not — a provider root that does not reach the artwork
|
||||
lands there, and nowhere else). `art >= 1` rather than
|
||||
`art === n` on purpose: an album with no art_path
|
||||
genuinely has no cover, and demanding one would be a
|
||||
check that lies about the library. */
|
||||
const TABS = ["ss:tab:recent", "ss:tab:albums", "ss:tab:artists", "ss:tab:playlists"];
|
||||
const tabs = TABS.map((id) => rows.find((r) => r.node === id) || null);
|
||||
const tabOk = (t) => !!t && t.n >= 1 && t.uniq === t.n && t.titled === t.n && t.fail === 0 && t.art >= 1;
|
||||
const tabsOk = tabs.every(tabOk);
|
||||
const tabReport = TABS.map((id, i) => [id.slice(7), tabs[i] ? [tabs[i].n, tabs[i].art, tabs[i].fail, tabs[i].ms] : null]);
|
||||
if (!tabsOk) problems.push("EXPECT auto-tabs " + JSON.stringify({ tabs, seen }));
|
||||
console.log((tabsOk ? " ok " : " FAIL ") + "auto-tabs".padEnd(16) + " " + JSON.stringify(tabReport));
|
||||
|
||||
/* ── three levels deep, which is the brief. An album's
|
||||
children are tracks and every one of them is playable and
|
||||
nothing else; an artist's children are albums and every
|
||||
one of them is browsable; and at least one album was
|
||||
reached UNDER an artist rather than only under Albums,
|
||||
which is the third level and the only clause here that
|
||||
the Albums tab alone cannot satisfy. */
|
||||
const albums = rows.filter((r) => r.node.startsWith("ss:album:"));
|
||||
const artists = rows.filter((r) => r.node.startsWith("ss:artist:"));
|
||||
const lists = rows.filter((r) => r.node.startsWith("ss:playlist:"));
|
||||
const firstArtist = rows.findIndex((r) => r.node.startsWith("ss:artist:"));
|
||||
const deeper = firstArtist >= 0 && rows.slice(firstArtist + 1).some((r) => r.node.startsWith("ss:album:"));
|
||||
const depthOk = albums.length >= 1 && artists.length >= 1 && lists.length >= 1 && deeper
|
||||
&& albums.every((r) => r.n >= 1 && r.kinds === "track" && r.pl === r.n && r.br === 0 && r.uniq === r.n && r.titled === r.n)
|
||||
&& artists.every((r) => r.n >= 1 && r.kinds === "album" && r.br === r.n && r.pl === 0 && r.uniq === r.n)
|
||||
&& lists.every((r) => r.n === 0 || (r.kinds === "plitem" && r.pl === r.n && r.uniq === r.n));
|
||||
const depthSeen = { albums: albums.length, artists: artists.length, playlists: lists.length, deeper, kinds: rows.map((r) => r.node + ":" + r.kinds) };
|
||||
if (!depthOk) problems.push("EXPECT auto-depth " + JSON.stringify(depthSeen));
|
||||
console.log((depthOk ? " ok " : " FAIL ") + "auto-depth".padEnd(16) + " "
|
||||
+ JSON.stringify({ albums: albums.length, artists: artists.length, playlists: lists.length, deeper }));
|
||||
|
||||
/* ── the covers, opened rather than believed. `open` is one
|
||||
real openFileDescriptor against the first URI the service
|
||||
minted: it is what separates "the FileProvider produced a
|
||||
plausible URI" from "the file is there and readable
|
||||
through it", and a files-path root — the one this round
|
||||
nearly shipped — fails it while breaking nothing else.
|
||||
It cannot prove the GRANT: the prober shares our uid and
|
||||
would read the provider with no grant at all. Only a car
|
||||
proves that, and grantUriPermission is the reason the line
|
||||
exists in LibraryBridge at all. */
|
||||
const artOk = rows.length >= 1 && rows.some((r) => r.open === "ok") && rows.every((r) => r.fail === 0);
|
||||
const artSeen = { open: [...new Set(rows.map((r) => r.open))].join("|"), fail: rows.reduce((a, r) => a + Math.max(0, r.fail), 0), art: rows.reduce((a, r) => a + Math.max(0, r.art), 0) };
|
||||
if (!artOk) problems.push("EXPECT auto-art " + JSON.stringify({ artSeen, notes: rows.map((r) => r.node + " " + r.note) }));
|
||||
console.log((artOk ? " ok " : " FAIL ") + "auto-art".padEnd(16) + " " + JSON.stringify(artSeen));
|
||||
|
||||
/* ── and COLD, which is the whole round. A new pid (the app
|
||||
was force-stopped, so the one serving the browse cannot be
|
||||
the one that was playing), no resumed MainActivity, and no
|
||||
WebView devtools socket for it — the three things that
|
||||
would each mean the app quietly came back and answered
|
||||
from the warm path. */
|
||||
const pidAfter = (say("shell", "pidof", "com.soulstream.app").trim().split(/\s+/)[0]) || "";
|
||||
const acts = say("shell", "dumpsys", "activity", "activities");
|
||||
const resumed = acts.split(/\r?\n/).some((l) => /ResumedActivity/.test(l) && /com\.soulstream\.app/.test(l));
|
||||
const socks = say("shell", "cat", "/proc/net/unix");
|
||||
const devtools = !!pidAfter && new RegExp("webview_devtools_remote_" + pidAfter + "\\b").test(socks);
|
||||
const coldOk = /^\d+$/.test(pidAfter) && pidAfter !== pidBefore && !resumed && !devtools && rows.length >= 1;
|
||||
const coldSeen = { pidBefore, pidAfter, resumed, devtools, nodes: rows.length };
|
||||
if (!coldOk) problems.push("EXPECT auto-cold " + JSON.stringify({ coldSeen, launched }));
|
||||
console.log((coldOk ? " ok " : " FAIL ") + "auto-cold".padEnd(16) + " " + JSON.stringify(coldSeen));
|
||||
|
||||
/* ── and how long each node took, as numbers, the way the
|
||||
boot budget is read. Both bounds on purpose: an `ms` of -1
|
||||
is a line that did not carry one, and a one-sided test
|
||||
would sail straight through it. The FIRST node pays the
|
||||
cold dlopen and the first SQLite open and gets a second
|
||||
and a half; everything after it is a warm query and gets
|
||||
400 ms. Every measured value is printed, because the first
|
||||
real run may well move both numbers and a budget guessed
|
||||
at twice is worse than one printed once. */
|
||||
const msAll = rows.map((r) => r.ms);
|
||||
const latOk = rows.length >= 1 && msAll.every((m) => m >= 0)
|
||||
&& msAll[0] < 1500 && msAll.slice(1).every((m) => m < 400);
|
||||
const latSeen = { first: msAll[0] ?? -1, rest: msAll.slice(1), hop: rows.map((r) => r.hop) };
|
||||
if (!latOk) problems.push("EXPECT auto-latency " + JSON.stringify({ latSeen, seen }));
|
||||
console.log((latOk ? " ok " : " FAIL ") + "auto-latency".padEnd(16) + " " + JSON.stringify(latSeen));
|
||||
|
||||
/* put the phone back the way the walk found it: the app
|
||||
open. It restores PAUSED (session restore's own rule), so
|
||||
this starts nothing playing. */
|
||||
say("shell", "am", "start", "-n", "com.soulstream.app/.MainActivity");
|
||||
await sleep(2000);
|
||||
}
|
||||
Reference in new issue
Block a user