diff --git a/docs/validation.md b/docs/validation.md index 42f00ca..4232f7b 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -196,6 +196,9 @@ Wayland rig. It verifies Recording status, non-empty segment files and metadata, closes the only window while recording continues, kills the game process, checks finalisation, reopens the app to load its library from disk, captures screenshots and uses Ctrl+Q during a second recording for clean shutdown. +The script checks required tools before starting and distinguishes pending +status from failed checks. The recovery run also passed with `PATH=/usr/bin:/bin` +without ripgrep installed (2026-10-08); scripts use grep instead. The recording and library screenshots were inspected; storage totals came from real Matroska file sizes. The existing Linux rig also passed all nine scenarios after integration (2026-10-08). Its unwritable-output assertion accepts both diff --git a/scripts/validate/linux-session.sh b/scripts/validate/linux-session.sh index 9c715cf..748e04d 100755 --- a/scripts/validate/linux-session.sh +++ b/scripts/validate/linux-session.sh @@ -1,6 +1,22 @@ #!/usr/bin/env bash # End-to-end desktop/Steam-process/session test in the private Wayland rig. set -euo pipefail +# Check before building or starting the rig; missing tools must not look like +# a status that has simply not appeared yet. +for tool in bash cargo grep python3 ffprobe grim wtype swaymsg dirname mkdir sleep cat seq \ + env dbus-run-session mktemp chmod rm basename head dbus-update-activation-environment \ + pipewire wireplumber pipewire-pulse pactl sway Xwayland ffplay fusermount3; do + if ! command -v "${tool}" > /dev/null; then + echo "FAIL required tool '${tool}' is missing from PATH" >&2 + exit 127 + fi +done +for tool in /usr/lib/xdg-desktop-portal /usr/lib/xdg-desktop-portal-wlr; do + if [[ ! -x "${tool}" ]]; then + echo "FAIL required tool '${tool}' is missing or not executable" >&2 + exit 127 + fi +done repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" out="${1:-${TMPDIR:-/tmp}/clipbin-session-validation}" mkdir -p "${out}" @@ -8,6 +24,28 @@ cargo build --locked -p clipbin-desktop --manifest-path "${repo}/Cargo.toml" export CLIPBIN_SESSION_REPO="${repo}" CLIPBIN_SESSION_OUT="${out}" "${repo}/scripts/rig/headless.sh" bash <<'INNER' set -euo pipefail +# grep returns 1 for no matches; every other error must fail immediately. +wait_log() { + local pattern="$1" log="$2" minimum="$3" attempts="$4" count status attempt + for ((attempt = 0; attempt < attempts; attempt++)); do + if count=$(grep -c "${pattern}" "${log}"); then + if ((count >= minimum)); then return 0; fi + else + status=$? + if [[ ${status} != 1 ]]; then + echo "FAIL could not check '${pattern}' in ${log} (grep exited ${status})" >&2 + exit 1 + fi + fi + if ! kill -0 "${app}" 2>/dev/null; then + echo "FAIL desktop exited while waiting for '${pattern}'; log: ${log}" >&2 + exit 1 + fi + sleep 1 + done + echo "FAIL timed out waiting for ${minimum} '${pattern}' entries; log: ${log}" >&2 + exit 1 +} out="${CLIPBIN_SESSION_OUT}" portal_config="${XDG_CONFIG_HOME}" export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config" @@ -29,29 +67,17 @@ for _ in range(20): argv = open(f'/proc/{sys.argv[1]}/cmdline', 'rb').read().split(b'\0') assert argv[0] == b'reaper' and b'SteamLaunch' in argv and b'AppId=480' in argv, argv PY -for _ in $(seq 30); do - if rg -q 'recorder_status=Recording' "${out}/desktop.log"; then break; fi - sleep 1 -done -rg -q 'recorder_status=Recording' "${out}/desktop.log" +wait_log 'recorder_status=Recording' "${out}/desktop.log" 1 30 swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null sleep 0.5 grim "${out}/recording.png" if [[ "${CLIPBIN_VALIDATE_RECOVERY:-0}" == 1 ]]; then kill "$(cat "${CLIPBIN_RIG_DIR}/xdpw.pid")" - for _ in $(seq 15); do - if rg -q 'recorder_status=Failed' "${out}/desktop.log"; then break; fi - sleep 1 - done - rg -q 'recorder_status=Failed' "${out}/desktop.log" + wait_log 'recorder_status=Failed' "${out}/desktop.log" 1 15 grim "${out}/capture-failed.png" XDG_CONFIG_HOME="${portal_config}" /usr/lib/xdg-desktop-portal-wlr > "${out}/recovered-portal.log" 2>&1 & portal=$! - for _ in $(seq 30); do - if [[ $(rg -c 'recorder_status=Recording' "${out}/desktop.log") -ge 2 ]]; then break; fi - sleep 1 - done - [[ $(rg -c 'recorder_status=Recording' "${out}/desktop.log") -ge 2 ]] + wait_log 'recorder_status=Recording' "${out}/desktop.log" 2 30 python3 - "${XDG_DATA_HOME}" <<'PY' import pathlib,json,sys p=max(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'), key=lambda p:int(p.parent.name)) @@ -70,22 +96,30 @@ kill "${game}" wait "${game}" 2>/dev/null || true finalised=0 for _ in $(seq 20); do - if python3 - "${XDG_DATA_HOME}" 2>/dev/null <<'PY' + status=0 + python3 - "${XDG_DATA_HOME}" <<'PY' || status=$? import pathlib, json, sys files = list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json')) -assert files +if not files: sys.exit(2) path = max(files, key=lambda p: int(p.parent.name)) m = json.loads(path.read_text()) -assert m['app_id'] == 480 and not m['session']['active'] +if m['session']['active']: sys.exit(2) +assert m['app_id'] == 480 assert m['segments'] and all(s['complete'] for s in m['segments']) assert all((path.parent / s['path']).stat().st_size > 0 for s in m['segments']) assert any(s['kind'] == 'Footage' for s in m['session']['spans']) print('PASS session finalised with footage') PY - then finalised=1; break; fi - sleep 1 + case "${status}" in + 0) finalised=1; break ;; + 2) sleep 1 ;; + *) echo "FAIL session finalisation check exited ${status}" >&2; exit 1 ;; + esac done -[[ ${finalised} == 1 ]] +if [[ ${finalised} != 1 ]]; then + echo "FAIL timed out waiting for session finalisation; log: ${out}/desktop.log" >&2 + exit 1 +fi python3 - "${XDG_DATA_HOME}" <<'PY' import pathlib,json,subprocess,sys path=max(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'), key=lambda p:int(p.parent.name)) @@ -129,11 +163,7 @@ sleep 0.5 grim "${out}/library.png" bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' & game=$! -for _ in $(seq 30); do - if rg -q 'recorder_status=Recording' "${out}/reopened.log"; then break; fi - sleep 1 -done -rg -q 'recorder_status=Recording' "${out}/reopened.log" +wait_log 'recorder_status=Recording' "${out}/reopened.log" 1 30 swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null wtype -M ctrl -k q -m ctrl for _ in $(seq 20); do