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

+30
View File
@@ -0,0 +1,30 @@
// Which backend commands does the phone never call? The remaining feature
// surface, by fact rather than by memory. A command counts as used when its
// api.js wrapper is referenced from src/mobile or the shared modules the
// phone imports, or when it is invoked by name there.
const fs = require('fs');
const path = require('path');
const root = 'F:/Coding/SoulStream/';
const read = (p) => fs.readFileSync(root + p, 'utf8');
const api = read('src/lib/api.js');
const fnByCmd = {};
for (const m of api.matchAll(/export const (\w+) = [\s\S]*?invoke\(\s*"([a-z_:|]+)"/g)) if (!fnByCmd[m[2]]) fnByCmd[m[2]] = m[1];
for (const m of api.matchAll(/export (?:async )?function (\w+)\b[\s\S]*?invoke\(\s*"([a-z_:|]+)"/g)) if (!fnByCmd[m[2]]) fnByCmd[m[2]] = m[1];
const walk = (d) => fs.readdirSync(root + d, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(d + '/' + e.name) : [d + '/' + e.name]);
const phone = [...walk('src/mobile'), 'src/lib/sources.js', 'src/lib/catalogMatch.js', 'src/lib/events.js', 'src/lib/localReleaseType.js', 'src/lib/editions.js']
.filter((p) => fs.existsSync(root + p)).map(read).join('\n');
const cmds = new Set();
for (const f of fs.readdirSync(root + 'src-tauri/src').filter((x) => x.endsWith('.rs'))) {
const s = read('src-tauri/src/' + f);
for (const m of s.matchAll(/#\[tauri::command\][^\n]*\n(?:[^\n]*\n)?\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g)) cmds.add(m[1]);
}
const used = [];
const unused = [];
for (const c of [...cmds].sort()) {
const fn = fnByCmd[c];
const hit = (fn && new RegExp('\\b' + fn + '\\b').test(phone)) || new RegExp('"' + c + '"').test(phone);
(hit ? used : unused).push(c + (fn ? '' : ' (no api wrapper)'));
}
console.log(cmds.size + ' commands: ' + used.length + ' called from the phone, ' + unused.length + ' never');
console.log('--- never called from the phone ---');
console.log(unused.join('\n'));
+255
View File
@@ -0,0 +1,255 @@
ok home
ok hold-sheet
ok cover-refetched
ok search-empty
ok search
ok search-dym
ok search-dym-pick
ok library
ok library-by-format
ok artists
ok tracks
ok album
ok album-playing
ok track-sheet
ok sheet
ok viewer
ok viewer-closed
ok queue
ok queue-row-sheet
ok queue-removed
ok queue-moved-up
ok queue-drag-arm
ok queue-dragged
ok queue-dragged-back
ok lyrics
ok lyrics-sheet
ok lyrics-nudged
ok lyrics-candidates
ok lyrics-chosen
ok lyrics-pasted
ok lyrics-autosync-off
ok lyrics-autosync-on
ok sync-room
ok sync-estimated
ok sync-cleared
ok sync-tapped
ok sync-undone
ok sync-left
ok sync-resumed
ok sync-all
ok sync-saved
ok sync-saved-sheet
ok lyrics-reduced
ok lyrics-original
ok after-back
ok artist
ok library-again
ok community
ok buddies
ok buddy-sheet
ok contact-sheet
ok contact-petname
ok contact-note
ok contact-fav
ok buddy-petname
ok user-lookup
ok taste
ok taste-declared
ok taste-hated
ok taste-dropped
ok taste-peer
ok rooms-filtered
ok rooms-more
ok room
ok room-users
ok contact-cleared
ok cat-results
ok cat-song-sheet
ok cat-record-sheet
ok cat-artist
ok cat-artist-follow
ok cat-artist-tab
ok disco-all
ok disco-albums
ok disco-editions
ok disco-sheet
ok disco-az
ok disco-many-artist
ok disco-many
ok disco-many-all
ok artist-more
ok artist-hero-set
ok artist-more-custom
ok artist-hero-cleared
ok artist-lanes
ok artist-lanes-page
ok cat-artist-related
ok net-playing
ok net-sheet
ok net-arrived
ok np-more
ok np-copied
ok np-saved
ok net-peers
ok net-filter-flac
ok net-filter-free
ok peer-file-sheet
ok peer-sheet
ok peer-screen
ok peer-picture
ok peer-compare
ok peer-gap-open
ok peer-again
ok peer-forgotten
ok peer-sheet-again
ok browse
ok browse-dir
ok browse-play
ok cat-album
ok cat-album-listeners
ok cat-album-sheet
ok cat-album-row
ok cat-album-next
ok cat-album-disc
ok cat-track-sheet
ok cat-track-save
ok cat-album-save
ok transfers
ok shade-note
ok shade-gone
ok gaps
ok gaps-open
ok playlists
ok playlist
ok playlist-playing
ok playlist-stream
ok playlist-filter
ok playlist-row-sheet
ok playlist-removed
ok playlist-undone
ok playlist-moved-up
ok playlist-moved-down
ok playlist-dragged
ok playlist-dragged-back
ok playlist-drag-filtered
ok playlist-drag-held
ok playlist-drag-refused
ok playlist-bulk-cached
ok playlist-bulk-download
ok playlist-unknown-sheet
ok playlist-match
ok playlist-match-results
ok playlist-matched
ok playlist-more
ok playlist-unpinned
ok cover-removed
ok cover-set
ok playlist-rename
ok playlist-renamed
ok playlists-again
ok playlist-new-prompt
ok playlist-new
ok playlist-pick
ok playlist-added
ok playlist-filled
ok playlist-delete-confirm
ok playlist-deleted
ok import-sheet
ok import-paste-prompt
ok import-review
ok import-keep-off
ok import-created
ok import-link-wait
ok import-link-review
ok import-link-created
ok playlists-after-import
ok import-links-wait
ok import-links-review
ok import-folder-review
ok import-folder-created
ok share-import
ok share-cancel
ok import-file-sheet
ok import-file-review
ok import-file-created
ok import-file-cancelled
ok import-file-refused
ok import-file-cleared
ok you
ok you-norm-off
ok you-norm-on
ok you-lossy
ok you-keep
ok you-slots
ok you-words
ok you-artedge
ok you-portraits
ok wifi-on
ok wifi-refused
ok wifi-off
ok you-port-prompt
ok you-port
ok you-port-refused
ok diag
ok diag-back
ok history-confirm
ok history-cleared
ok search-recent
ok search-recent-pick
ok home-again
ok home-more
ok sleep-sheet
ok sleep-set
ok landscape
ok landscape-sheet
ok landscape-lyrics
ok landscape-queue
ok landscape-album
ok landscape-half
ok landscape-artist
ok landscape-insets
ok portrait-again
ok weather-tracks
ok weather-tracks-ok
ok weather-signedout
ok weather-badpass
ok weather-noserver
ok weather-search-out
ok weather-rooms-late
ok weather-rescan
ok weather-clear
ok faces-home-down
ok faces-gap-down
ok faces-gap-again
ok faces-album-down
ok faces-album-again
ok faces-artist-down
ok faces-artist-none
ok faces-playlist-down
ok faces-playlist-again
ok faces-lyrics-down
ok faces-lyrics-again
ok faces-xfer-failed
ok faces-library-toast
ok faces-library-again
ok faces-clear
ok fresh-boot
ok fresh-community
ok fresh-home
ok fresh-albums
ok fresh-artists
ok fresh-search
ok fresh-preview
ok fresh-folder
ok fresh-fixed
ok fresh-off
ok export-open
ok export-sheet
ok export-written
ok export-cancelled
ok export-refused
ok export-clear
ok session-warm
ok session-late
ok session-clear
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Apply and ship a list of rounds, stopping at the first failure.
# bash chain.sh 18 19 20
# Each round: node patch-round<N>.cjs && bash src-tauri/android/ship-fast.sh commit-msg<N>.txt
# (ship.sh = mock walk -> deploy -> phone walk -> commit, each gated.)
set -u
S="C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad"
cd /f/Coding/SoulStream || exit 1
for n in "$@"; do
echo "===== round $n $(date +%H:%M:%S)"
if [ ! -f "$S/patch-round$n.cjs" ]; then echo " no patch for round $n"; exit 1; fi
if [ ! -f "$S/commit-msg$n.txt" ]; then echo " no commit message for round $n"; exit 1; fi
if grep -qi "co-authored\|anthropic\|claude" "$S/commit-msg$n.txt"; then
echo " REFUSING: attribution found in commit-msg$n.txt"; exit 1
fi
# the tree must be clean before a round starts: ship.sh commits with `git add -A`,
# so anything a stray writer left behind would ride along into Kyle's commit.
dirty=$(git status --porcelain)
if [ -n "$dirty" ]; then
echo " REFUSING: the working tree is not clean before round $n"
echo "$dirty" | head -20
exit 1
fi
node "$S/patch-round$n.cjs" || { echo " PATCH $n FAILED"; exit 1; }
bash src-tauri/android/ship-fast.sh "$S/commit-msg$n.txt" || { echo " SHIP $n FAILED"; exit 1; }
echo "== shipped $n $(date +%H:%M:%S)"
done
echo "===== all rounds shipped $(date +%H:%M:%S)"
git log --oneline -8
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Apply and ship a list of rounds, stopping at the first failure.
# bash chain.sh 18 19 20
# Each round: node patch-round<N>.cjs && bash src-tauri/android/ship.sh commit-msg<N>.txt
# (ship.sh = mock walk -> deploy -> phone walk -> commit, each gated.)
set -u
S="C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad"
cd /f/Coding/SoulStream || exit 1
for n in "$@"; do
echo "===== round $n $(date +%H:%M:%S)"
if [ ! -f "$S/patch-round$n.cjs" ]; then echo " no patch for round $n"; exit 1; fi
if [ ! -f "$S/commit-msg$n.txt" ]; then echo " no commit message for round $n"; exit 1; fi
if grep -qi "co-authored\|anthropic\|claude" "$S/commit-msg$n.txt"; then
echo " REFUSING: attribution found in commit-msg$n.txt"; exit 1
fi
# the tree must be clean before a round starts: ship.sh commits with `git add -A`,
# so anything a stray writer left behind would ride along into Kyle's commit.
dirty=$(git status --porcelain)
if [ -n "$dirty" ]; then
echo " REFUSING: the working tree is not clean before round $n"
echo "$dirty" | head -20
exit 1
fi
node "$S/patch-round$n.cjs" || { echo " PATCH $n FAILED"; exit 1; }
bash src-tauri/android/ship.sh "$S/commit-msg$n.txt" || { echo " SHIP $n FAILED"; exit 1; }
echo "== shipped $n $(date +%H:%M:%S)"
done
echo "===== all rounds shipped $(date +%H:%M:%S)"
git log --oneline -8
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Compare a walk log against the baseline, step by step: same verdicts or not.
# bash compare-walks.sh <new-log>
S="C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad"
grep -E "^ ok |^ FAIL " "$1" | awk '{print $1, $2}' > "$S/new-mock-steps.txt"
echo "baseline steps: $(wc -l < "$S/baseline-mock-steps.txt") new steps: $(wc -l < "$S/new-mock-steps.txt")"
if diff -q "$S/baseline-mock-steps.txt" "$S/new-mock-steps.txt" > /dev/null; then
echo "IDENTICAL: every step reached the same verdict"
else
echo "DIFFERENCES:"
diff "$S/baseline-mock-steps.txt" "$S/new-mock-steps.txt" | head -30
fi
grep -m1 "waits:" "$1" || true
grep -m1 "no exceptions\|problem(s)" "$1" || true
+106
View File
@@ -0,0 +1,106 @@
// Adaptive waits for the walk. Every step used to sleep a fixed interval
// sized for its worst case — 414 seconds of them in the mock list and 223 in
// the phone's, over half of a round's twenty-three minutes. This polls a
// fingerprint of the page and the app's own state instead, and goes on once
// both have stopped moving.
//
// Applied on its own, after a chain clears, so it never rides inside a
// round's commit.
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
let p, s;
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
const save = () => fs.writeFileSync(root + p, s);
const once = (a) => {
const i = s.indexOf(a);
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
return i;
};
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
load('tests/mobile/smoke.mjs');
if (s.includes('function settle(')) throw new Error('the walk already waits adaptively — nothing to do');
const A_LOOP = ` for (const s of steps) {`;
const A_SLEEP = ` await sleep(s.wait || 500);`;
once(A_LOOP);
once(A_SLEEP);
rep(A_LOOP, ` /* ── 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) {`);
rep(A_SLEEP, ` await settle(s.wait || 500, s);`);
/* The tally, printed where `saved` and `waited` are provably in scope: the
comment below sits in the same block as the loop, at the same indent. */
rep(` /* ── the restart half of session restore, on the phone only ──────`,
` 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 ──────`);
save();
console.log('the walk waits adaptively now');
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Apply rounds in a given order to a copy of a chosen base snapshot.
# bash preflight-on.sh <base-dir> 31 32 33 34
set -u
S="C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad"
BASE="$1"; shift
P="$S/pf-$$"
rm -rf "$P" && mkdir -p "$P"
cp -r "$BASE"/src "$BASE"/tests "$BASE"/crates "$BASE"/src-tauri "$BASE"/ANDROID.md "$P/" 2>/dev/null
bad=""
for n in "$@"; do
if SS_ROOT="$P/" node "$S/patch-round$n.cjs" > "$P/r$n.log" 2>&1; then
echo " ok $n"
else
bad="$bad $n"
echo " FAIL $n: $(grep -m1 'Error:' "$P/r$n.log" | cut -c1-150)"
fi
done
[ -z "$bad" ] && echo "ORDER OK: $*" || echo "ORDER BROKEN at:$bad"
rm -rf "$P"
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Apply a list of rounds IN ORDER to a throwaway copy of the live tree, to find
# anchor collisions between rounds authored in parallel — before any of them
# costs a twenty-minute ship cycle.
# bash preflight.sh 20 21 22 23 24 25 26
set -u
S="C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad"
P="$S/preflight"
R=/f/Coding/SoulStream
rm -rf "$P" && mkdir -p "$P"
# the areas every patch so far has touched; enough for anchors, cheap to copy
cp -r "$R/src" "$R/tests" "$R/crates" "$R/ANDROID.md" "$P/" 2>/dev/null
# everything in src-tauri except the generated Android project and the build dir,
# which are enormous and which no patch anchors against
mkdir -p "$P/src-tauri"
for e in "$R"/src-tauri/*; do
case "$(basename "$e")" in gen|target) continue ;; esac
cp -r "$e" "$P/src-tauri/" 2>/dev/null
done
ok=""; bad=""
for n in "$@"; do
if SS_ROOT="$P/" node "$S/patch-round$n.cjs" > "$P/round$n.log" 2>&1; then
ok="$ok $n"
echo " ok round $n"
else
bad="$bad $n"
echo " FAIL round $n"
grep -m1 "Error:" "$P/round$n.log" | cut -c1-200
fi
done
echo "--------"
echo "applied cleanly in order:$ok"
[ -n "$bad" ] && echo "needs a fix:$bad"
exit 0
+16
View File
@@ -0,0 +1,16 @@
// Bring the future snapshot's walk in line with the corrected round 17:
// the nav-badge assertion moves off the Transfers screen (where the mock's
// burst has already finished) onto the record save that is actually in flight.
const fs = require('fs');
const f = process.argv[process.argv.length - 1];
let s = fs.readFileSync(f, 'utf8');
const D = String.fromCharCode(92) + 'd';
const a = ' transfers: (i) => i.title.startsWith("Transfers") && i.xfers >= 11 && /^' + D + '+$/.test(i.navBadges),';
const b = ' transfers: (i) => i.title.startsWith("Transfers") && i.xfers >= 11,';
const c = ' "cat-album-save": (i) => /^Saving ' + D + '+ from /.test(i.save),';
const d = ' "cat-album-save": (i) => /^Saving ' + D + '+ from /.test(i.save) && /^' + D + '+$/.test(i.navBadges),';
if (!s.includes(a)) throw new Error('missing transfers anchor');
if (!s.includes(c)) throw new Error('missing cat-album-save anchor');
s = s.replace(a, () => b).replace(c, () => d);
fs.writeFileSync(f, s);
console.log('future snapshot walk realigned');
+23
View File
@@ -0,0 +1,23 @@
// Bring the wave-five snapshot in line with round 37's two corrections:
// the drag probe reads the state behind the module namespace, and the console
// trap ignores Chrome's vibrate intervention (synthetic pointer events).
const fs = require('fs');
const f = process.argv[process.argv.length - 1] + '/tests/mobile/smoke.mjs';
let s = fs.readFileSync(f, 'utf8');
let done = [];
if (s.includes('__ss.pls.current.items')) {
s = s.split('__ss.pls.current.items').join('__ss.pls.pls.current.items');
done.push('probe');
}
const oldLog = ` cdp.on["Log.entryAdded"] = (p) => { if (p.entry.level === "error") problems.push("LOG " + p.entry.text + " " + (p.entry.url || "")); };`;
const newLog = ` cdp.on["Log.entryAdded"] = (p) => {
if (p.entry.level !== "error") return;
if (/navigator\\.vibrate/.test(p.entry.text || "")) return;
problems.push("LOG " + p.entry.text + " " + (p.entry.url || ""));
};`;
if (s.includes(oldLog)) { s = s.replace(oldLog, () => newLog); done.push('console trap'); }
fs.writeFileSync(f, s);
console.log('realigned: ' + (done.join(', ') || 'nothing to do'));
+30
View File
@@ -0,0 +1,30 @@
// Bring the wave-six snapshot in line with round 40's corrected background
// check (a ten-second background window can legitimately cross a track
// boundary, which is playback continuing, not playback stopping).
const fs = require('fs');
const f = process.argv[process.argv.length - 1] + '/tests/mobile/smoke.mjs';
let s = fs.readFileSync(f, 'utf8');
const done = [];
const a1 = ` const posBefore = (await cdp.evaluate(PLAYER))?.pos ?? 0;
cdp.close();`;
const b1 = ` const bgBefore = await cdp.evaluate(PLAYER);
const posBefore = bgBefore?.pos ?? 0;
cdp.close();`;
if (s.includes(a1)) { s = s.replace(a1, () => b1); done.push('posBefore'); }
const a2 = ` const posAfter = (await cdp.evaluate(PLAYER))?.pos ?? 0;
const bgOk = stillPlaying && posAfter >= posBefore + 8;
if (!bgOk) problems.push("EXPECT background " + JSON.stringify({ posBefore, posAfter, stillPlaying }));
console.log((bgOk ? " ok " : " FAIL ") + "background".padEnd(16) + " " + JSON.stringify({ posBefore, posAfter, stillPlaying }));`;
const b2 = ` const bgAfter = await cdp.evaluate(PLAYER);
const posAfter = bgAfter?.pos ?? 0;
const crossed = !!(bgBefore && bgAfter && bgBefore.title !== bgAfter.title);
const bgOk = stillPlaying && (crossed || posAfter >= posBefore + 8);
const bgSeen = { posBefore, posAfter, stillPlaying, crossed, from: bgBefore?.title ?? null, to: bgAfter?.title ?? null };
if (!bgOk) problems.push("EXPECT background " + JSON.stringify(bgSeen));
console.log((bgOk ? " ok " : " FAIL ") + "background".padEnd(16) + " " + JSON.stringify(bgSeen));`;
if (s.includes(a2)) { s = s.replace(a2, () => b2); done.push('bgOk'); }
fs.writeFileSync(f, s);
console.log('realigned: ' + (done.join(', ') || 'nothing to do'));
+40
View File
@@ -0,0 +1,40 @@
// Carry the two harness fixes (a8adce0, 9bfbcfc) into a snapshot: the attach
// retry in cdp.mjs, and the wait-for-the-app block in smoke.mjs. cdp.mjs is
// copied wholesale (no round touches it); smoke.mjs is edited in place,
// because rounds applied to the snapshot own the rest of that file.
const fs = require('fs');
const snap = process.argv[process.argv.length - 1];
const repo = 'F:/Coding/SoulStream';
fs.copyFileSync(repo + '/tests/mobile/cdp.mjs', snap + '/tests/mobile/cdp.mjs');
const f = snap + '/tests/mobile/smoke.mjs';
let s = fs.readFileSync(f, 'utf8');
const anchor = ` console.log("attached to the phone's WebView (pid " + pid + ")");`;
if (s.includes('the app never came up on the phone')) {
console.log('already carries the wait');
} else if (!s.includes(anchor)) {
throw new Error('anchor missing in ' + f);
} else {
const block = anchor + `
/* A walk follows a fresh install, so the app is always cold here: the
shell mounts, the library loads, Soulseek logs in and the share
index is built. Attaching only proves the WebView answers — it says
nothing about the app being up. Starting anyway means the first
steps run against a half-mounted page and get reported as broken
features, which is exactly how an hour went once. Wait for the
handle the walk actually drives. */
const upFrom = Date.now();
let up = false;
while (Date.now() - upFrom < 90_000) {
try {
if (await cdp.evaluate(\`!!(window.__ss && document.querySelector('.app'))\`) === true) { up = true; break; }
} catch { /* a page mid-navigation answers nothing; ask again */ }
await sleep(1000);
}
if (!up) throw new Error("the app never came up on the phone (no __ss and no .app after 90s)");
console.log("the app is up (" + ((Date.now() - upFrom) / 1000).toFixed(1) + "s after attaching)");`;
s = s.replace(anchor, () => block);
fs.writeFileSync(f, s);
console.log('wait inserted');
}
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Put the repo back to HEAD after a failed ship. `git checkout --` restores
# tracked files but leaves behind any NEW file the round created, which then
# trips that round's own second-run guard on the retry.
set -u
cd /f/Coding/SoulStream || exit 1
git checkout -- .
git clean -fd src tests crates src-tauri # no -x: ignored build output stays
git status --porcelain | head
echo "tree: $(git status --porcelain | wc -l) changed (0 = clean)"