#!/usr/bin/env bash # End-to-end desktop/Steam-process/session test in the private Wayland rig. set -euo pipefail repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" out="${1:-${TMPDIR:-/tmp}/clipbin-session-validation}" mkdir -p "${out}" 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 out="${CLIPBIN_SESSION_OUT}" portal_config="${XDG_CONFIG_HOME}" export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config" mkdir -p "${XDG_DATA_HOME}" "${XDG_CONFIG_HOME}" "${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/desktop.log" 2>&1 & app=$! trap 'kill "${app}" "${game:-}" "${portal:-}" 2>/dev/null || true' EXIT sleep 3 swaymsg '[title="clipbin-test-game"] fullscreen disable' > /dev/null || true swaymsg '[app_id="dev.clipbin.Clipbin"] floating enable, resize set 1360 860, move position center, focus' > /dev/null bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' & game=$! python3 - "${game}" <<'PY' import sys,time for _ in range(20): argv = open(f'/proc/{sys.argv[1]}/cmdline', 'rb').read().split(b'\0') if argv[0] == b'reaper': break time.sleep(.1) 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" 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" 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 ]] 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)) m=json.loads(p.read_text()) assert any(s['path'].startswith('run-002/') for s in m['segments']) assert len([s for s in m['session']['spans'] if s['kind']=='Missing']) >= 2 print('PASS capture recovery resumed in the same session with a Missing gap') PY fi # Closing the sole window must leave the process and footage running. swaymsg '[app_id="dev.clipbin.Clipbin"] kill' > /dev/null kill -0 "${app}" sleep 3 kill -0 "${app}" kill "${game}" wait "${game}" 2>/dev/null || true finalised=0 for _ in $(seq 20); do if python3 - "${XDG_DATA_HOME}" 2>/dev/null <<'PY' import pathlib, json, sys files = list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json')) assert files 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'] 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 done [[ ${finalised} == 1 ]] 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)) m=json.loads(path.read_text()) ns=lambda d:d['secs']*1_000_000_000+d['nanos'] runs={} for segment in m['segments']: run=segment['path'].split('/')[0] base,packets=runs.get(run,(ns(segment['start']),0)) assert segment['dropped_frames']==0, 'Dropped frames make count-derived offsets uncertain' expected=base+packets*1_000_000_000//segment['frames_per_second'] assert ns(segment['start'])==expected, (segment['path'],ns(segment['start']),expected) if not segment['available']: assert ns(segment['end'])==ns(segment['start']) runs[run]=(base,packets) continue result=json.loads(subprocess.check_output(['ffprobe','-v','error','-select_streams','v:0','-count_packets','-show_entries','stream=nb_read_packets','-of','json',str(path.parent/segment['path'])])) count=int(result['streams'][0]['nb_read_packets']) assert count>0 runs[run]=(base,packets+count) print('PASS segment offsets exactly match cumulative earlier video packet counts') PY python3 - "${XDG_DATA_HOME}" <<'PY' import pathlib,json,sys files=list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json')) assert files and all(not json.loads(p.read_text())['session']['active'] for p in files) PY for _ in $(seq 10); do if ! kill -0 "${app}" 2>/dev/null; then break; fi sleep 1 done if kill -0 "${app}" 2>/dev/null; then echo 'FAIL closed app kept discovering after game exit' >&2; exit 1; fi wait "${app}" kill "$(cat "${CLIPBIN_RIG_DIR}/game.pid")" # Reopen to prove the library loads from disk, then Quit cleanly with Ctrl+Q. "${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/reopened.log" 2>&1 & app=$! sleep 4 swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null 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" swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null wtype -M ctrl -k q -m ctrl for _ in $(seq 20); do if ! kill -0 "${app}" 2>/dev/null; then break; fi sleep 1 done if kill -0 "${app}" 2>/dev/null; then echo 'FAIL explicit Quit did not exit' >&2; exit 1; fi wait "${app}" python3 - "${XDG_DATA_HOME}" <<'PY' import pathlib,json,sys files=list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json')) assert len(files) >= 2 assert all(not json.loads(p.read_text())['session']['active'] for p in files) PY echo "PASS automatic recording, window close, game exit, library reload and Quit" INNER