Files
yellowjacket/scripts/seed-sandbox.sh
T
yonluandClaude Opus 5 deb3f3da7e feat(wails): move the e2e harness and headless launch onto v3
make e2e is green on chromium: 92 passed. The harness is rebuilt on
what v3 actually offers, and three of the four things it replaced turn
out to be better than what they replaced.

The headless launch is v3's own server mode. scripts/dev-headless.sh
ran a `-tags dev` binary whose app_dev.go parsed -devserver/-assetdir
out of os.Args; that file went with v2, so the harness had no server at
all. `-tags dev,server` is a first-class mode and needs no display, so
Xvfb is gone from the script and from CI.

The bridge hooks two places, neither of them EventsOn. Inbound is
window._wails.dispatchWailsEvent, wrapped by pre-creating the object
the runtime keeps and putting an accessor on the one property.
Outbound is fetch: v3 routes every runtime call through one POST, so
the bridge sees binding calls and event emits from any module, needs no
walk of an object graph, and cannot miss a call made before it looked.

__yjEvents.call posts to that endpoint by method name, so it depends on
nothing in the app's bundle and works on a page with no init script.
That is what lets seed-sandbox.sh drop playwright-cli entirely — it
drove AddLibrary through a browser only because window.go was v2's one
way in — and with it a global npm install and a second Chromium in CI.

measure.mjs and one spec lose their window.go walks and read the
bridge's log instead; e2e/support/method-ids.mjs derives id -> name
from frontend/bindings/ (phase 6b option 1, so it cannot go stale
silently). Plain .mjs because measure.mjs runs under bare node and one
derivation beats two that can disagree.

Four bugs surfaced, and the migration is how.

The cross-service wiring never ran headless. It hung off
Common.ApplicationStarted, which server mode never emits —
setupCommonEvents is an explicit no-op there — so the queue had no
TrackLoader and playing a track changed the queue and then silently did
nothing. It is a service registered last now (backend/startup.go):
services start in registration order, which is the ordering the wiring
needs, in every mode.

Six specs called SetQueue with 3 of its 4 arguments. v2 accepted that
and filled the gap; v3 answers "expects 4 arguments, got 3".

requested-badge's cleanup read window.go and returned early on
`if (!svc)` — the silent cleanup its own comment was written to
prevent, one migration later. It posts to the runtime endpoint now,
which any page can do.

SearchIndex.Search trusted a startup latch, so rows a spec staged
afterwards were unsearchable and three specs passed only when an
earlier one happened to flip it. shelves.go fixed exactly this and left
hasCatalogRows behind; the search path now uses it as the fallback,
with the latch still the fast path.

Two spec edits are deletions of assertions about v2. harness.spec
checked Object.keys(window.go) and that a bad call *hung*; it now
checks the real runtime is loaded and that the backend rejects with a
TypeError naming the argument. album-actions asserted a tracklist
legend that dcc40b1 deleted on main — that spec has been failing since,
and what replaced it is covered in frontend/test/components.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
2026-08-14 20:58:20 -04:00

196 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Build a seeded YJ_HOME snapshot by RUNNING THE APP.
#
# The point of a seed is to start a harness run *inside* the app rather
# than on the first-run wizard, which intercepts every pointer event
# until a library exists. The wizard's dismissal condition is not a
# config file — it is `GetAllLibrariesWithTrackCounts()` returning a
# non-empty list — so the only honest way to produce that state is to
# call the real AddLibrary binding and let the real scanner finish.
#
# Hand-writing a config.toml and DB rows would be a second description
# of a valid YJ_HOME, free to drift from what the app actually writes.
# That is the failure mode .planning/NOTES.md records for the old
# migration chain, and it is not worth repeating for seeds.
#
# Usage:
# scripts/seed-sandbox.sh [--name NAME] [--port N] [--no-build]
# [--manifest PATH]
#
# --manifest points at a different generated library's manifest, which
# is how the bulk measurement library (`make bulkdata`) gets seeded:
# same script, same discipline, different pile of files. A manifest
# either lists its tracks or states a trackCount; both are accepted,
# because describing 50 000 tracks individually would serve nobody.
#
set -euo pipefail
cd "$(dirname "$0")/.."
REPO_ROOT="$PWD"
RUN_DIR="$REPO_ROOT/.dev"
SEED_DIR="$RUN_DIR/seeds"
LOG_FILE="$RUN_DIR/app.log"
MANIFEST="$REPO_ROOT/test_data/music_library_test.manifest.json"
NAME="default"
PORT=34115
BUILD_ARGS=()
# Replaced below once the manifest says how many tracks are coming: a
# 50 000-track scan is minutes, and a fixed 180 s deadline would abort
# a healthy run rather than an unhealthy one.
SCAN_TIMEOUT=180
while [ $# -gt 0 ]; do
case "$1" in
--name)
NAME="${2:?--name needs a value}"
shift 2
;;
--port)
PORT="${2:?--port needs a number}"
shift 2
;;
--no-build)
BUILD_ARGS+=(--no-build)
shift
;;
--manifest)
MANIFEST="${2:?--manifest needs a path}"
shift 2
;;
*)
echo "seed-sandbox: unknown argument: $1" >&2
exit 2
;;
esac
done
need() {
command -v "$1" >/dev/null 2>&1 || {
echo "seed-sandbox: $1 not found in PATH" >&2
exit 1
}
}
need curl
need jq
if [ ! -f "$MANIFEST" ]; then
echo "seed-sandbox: no manifest at $MANIFEST" >&2
echo " run 'make testdata' (fixtures) or 'make bulkdata' (measurement)" >&2
exit 1
fi
LIBRARY_DIR="$REPO_ROOT/$(jq -r .libraryRoot "$MANIFEST")"
WANT_TRACKS="$(jq '.trackCount // (.tracks | length)' "$MANIFEST")"
SCAN_TIMEOUT=$((180 + WANT_TRACKS / 50))
FIXTURE_HASH="$(jq -r .hash "$MANIFEST")"
cleanup() {
./scripts/dev-stop.sh >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "seed-sandbox: building '$NAME' from $WANT_TRACKS fixture tracks"
# Start on an empty YJ_HOME: seeding must exercise the same first-run
# path a real install takes.
#
# YJ_CORE_INDEX_URL points at a dead address on purpose. A seed must
# not reach for the real explore artifact — that is a minute of network
# per seed, and it makes the result depend on what the artifact server
# happened to be serving that day.
YJ_CORE_INDEX_URL="http://127.0.0.1:1/none.tar.zst" \
./scripts/dev-headless.sh --fresh --port "$PORT" "${BUILD_ARGS[@]}"
YJ_HOME="$(cat "$RUN_DIR/app.home")"
# A binding is called over the runtime's own HTTP endpoint, by method
# name — the same request the bundle makes, minus the browser.
#
# This used to drive a real page through playwright-cli, because v2's
# only way in was `window.go`. v3 answers the same call over HTTP, so a
# browser (and a global npm install of the CLI, in CI) buys nothing
# here: seeding needs the *app* to run and the *real* scanner to finish,
# which it still does. It also fails properly now — v3 answers a bad
# argument with a 422 and a TypeError naming it, where v2 logged
# "error parsing arguments" and never fired the callback.
call() {
local method="$1"
local args="${2:-[]}"
local body
body="$(jq -nc --arg m "yellowjacket/backend/$method" --argjson a "$args" \
'{object: 0, method: 0, args: {"call-id": "seed", methodName: $m, args: $a}}')"
curl -sS --fail-with-body --max-time 30 \
-X POST "http://localhost:$PORT/wails/runtime" \
-H 'Content-Type: application/json' \
-d "$body"
}
echo "seed-sandbox: registering library $LIBRARY_DIR"
if ! call library.Library.AddLibrary \
"$(jq -nc --arg d "$LIBRARY_DIR" '[$d]')" >/dev/null; then
echo "seed-sandbox: AddLibrary failed; app log:" >&2
tail -n 40 "$LOG_FILE" >&2
exit 1
fi
# Wait on the observable outcome — the track count the app itself
# reports — rather than on a fixed sleep or a scan event. This also
# validates the manifest against the real scanner: if the two disagree,
# a fixture is not being ingested and the seed is wrong.
echo "seed-sandbox: waiting for the scan to reach $WANT_TRACKS tracks"
deadline=$((SECONDS + SCAN_TIMEOUT))
got=0
while [ "$SECONDS" -lt "$deadline" ]; do
got="$(call library.Library.GetAllLibrariesWithTrackCounts |
jq '[(. // [])[].trackCount // 0] | add // 0')"
got="${got:-0}"
[ "$got" = "$WANT_TRACKS" ] && break
# A large scan is minutes of silence otherwise, which is
# indistinguishable from a hang.
if [ $((SECONDS % 15)) -eq 0 ]; then
echo " ... $got/$WANT_TRACKS"
fi
sleep 1
done
if [ "$got" != "$WANT_TRACKS" ]; then
echo "seed-sandbox: scan settled at $got/$WANT_TRACKS tracks" >&2
echo " (a fixture the scanner rejects, or a scan still running)" >&2
tail -n 40 "$LOG_FILE" >&2
exit 1
fi
# SIGTERM, so OnBeforeClose / OnShutdown persist window, player and
# queue state. A seed built from a killed app is missing exactly the
# state those hooks write.
./scripts/dev-stop.sh
mkdir -p "$SEED_DIR"
tar -cf "$SEED_DIR/$NAME.tar" -C "$YJ_HOME" .
cat >"$SEED_DIR/$NAME.json" <<EOF
{
"name": "$NAME",
"tracks": $WANT_TRACKS,
"libraryRoot": "$LIBRARY_DIR",
"fixtureHash": "$FIXTURE_HASH",
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
trap - EXIT
echo "seed-sandbox: wrote $SEED_DIR/$NAME.tar ($WANT_TRACKS tracks)"
echo " use it with: make dev-headless SEED=$NAME"