Files
SoulStream/handoff/rounds/ins-r50-walk-block.txt
T
KeelrLeek 9f0601027c 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.
2026-09-14 07:57:41 -05:00

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);
}