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.
154 lines
11 KiB
Plaintext
154 lines
11 KiB
Plaintext
/* ── 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);
|
|
}
|