#!/usr/bin/env bash # Mokosh Phase 1 — D-12 ffprobe acceptance gate smoke test. # # Architecture: # - Launches Chrome with a DEDICATED `/tmp/mokosh-smoke-profile` user-data-dir # so we don't interfere with your daily Chrome session. # - Opens a data: URL with title "Mokosh Smoke Test" as the share target. # - Passes `--auto-select-desktop-capture-source="Mokosh Smoke Test"` so the # getDisplayMedia picker auto-accepts that tab — no manual pick. # - Logs Chrome stderr/stdout to /tmp/mokosh-chrome.log. # - Polls ~/Downloads for the new session_report_*.zip. # - Runs ffprobe gate, stages fixture, opens WebM in Chrome for visual check. # # Chrome 148+ removed `--load-extension`, so the extension load is the ONE # manual step that has to happen each time the smoke profile is fresh. (Once # loaded, it persists in the profile until the dir is wiped — see KEEP_PROFILE.) # # What YOU do (~3 clicks total): # 1. Run this script. # 2. In the Chrome window that opens, address-bar → chrome://extensions # → toggle "Developer mode" ON → "Load unpacked" → select # /home/parf/projects/work/repremium/dist (only needed if profile is fresh). # 3. Click the extension icon (puzzle-piece menu if not pinned). # The screen-share picker auto-accepts the "Mokosh Smoke Test" tab. # 4. Wait ~35 seconds (or move mouse / scroll the smoke tab for frame deltas). # 5. Click the icon again → click "Сохранить отчёт об ошибке". # This script handles everything after that. # # Env knobs: # CHROME_BIN — Chrome binary (default: /usr/bin/google-chrome-stable) # KEEP_PROFILE=1 — don't wipe the smoke profile, so extension stays loaded # across runs (default: wipe, fresh-load each run) # POLL_TIMEOUT — max seconds to wait for the download (default: 900 = 15m) set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIST_DIR="${REPO_DIR}/dist" PROFILE_DIR="/tmp/mokosh-smoke-profile" DOWNLOADS_DIR="${HOME}/Downloads" FIXTURE_DEST="${REPO_DIR}/tests/fixtures/last_30sec.webm" WEBM_TMP="/tmp/mokosh-last_30sec.webm" CHROME_LOG="/tmp/mokosh-chrome.log" SHARE_TARGET="Mokosh Smoke Test" CHROME_BIN="${CHROME_BIN:-/usr/bin/google-chrome-stable}" KEEP_PROFILE="${KEEP_PROFILE:-0}" POLL_TIMEOUT="${POLL_TIMEOUT:-900}" red() { printf '\033[31m%s\033[0m\n' "$*"; } green() { printf '\033[32m%s\033[0m\n' "$*"; } yellow() { printf '\033[33m%s\033[0m\n' "$*"; } blue() { printf '\033[34m%s\033[0m\n' "$*"; } echo blue "==> Mokosh Phase 1 smoke test (D-12 ffprobe gate)" echo # --- pre-flight --- [[ -d "${DIST_DIR}" ]] || { red "FAIL: ${DIST_DIR} missing — run \`npm run build\` first"; exit 1; } [[ -x "${CHROME_BIN}" ]] || { red "FAIL: ${CHROME_BIN} not found (set CHROME_BIN=...)"; exit 1; } command -v ffprobe >/dev/null || { red "FAIL: ffprobe not installed"; exit 1; } command -v unzip >/dev/null || { red "FAIL: unzip not installed"; exit 1; } # WR-04 fix: python3 is REQUIRED for URL encoding of the smoke-tab data: URL. # Previously a fallback `|| printf '%s' "${SMOKE_HTML}"` would emit raw HTML # into the data URL on python3-missing systems — Chrome silently failed to # parse those URLs (containing literal `<`, `>`, spaces, quotes) and the # operator saw a blank tab with no diagnostic. Better to fail loud early. command -v python3 >/dev/null || { red "FAIL: python3 not installed (needed for URL encoding the smoke tab data URL)"; exit 1; } grep -q '"desktopCapture"' "${DIST_DIR}/manifest.json" || { red "FAIL: dist/manifest.json missing desktopCapture"; exit 1; } ! grep -q '"tabCapture"' "${DIST_DIR}/manifest.json" || { red "FAIL: dist/manifest.json still has tabCapture"; exit 1; } green "✓ pre-flight checks passed" echo " chrome: ${CHROME_BIN} ($("${CHROME_BIN}" --version 2>/dev/null || echo 'unknown'))" echo " dist: ${DIST_DIR}" echo " profile: ${PROFILE_DIR} (KEEP_PROFILE=${KEEP_PROFILE})" echo " log: ${CHROME_LOG}" echo " downloads: ${DOWNLOADS_DIR}" echo # --- snapshot Downloads --- # WR-05 fix: snapshot the FULL list of pre-existing zips, not just the # count. The count-only approach falsely succeeded when an unrelated # session_report appeared in Downloads (operator running the extension # in another window, etc.) — the script would then ffprobe the WRONG # file. comm -13 against the post-recording list yields the genuinely # new file by identity. The mtime sort below still picks the latest if # multiple new zips appear (unlikely but defensive). BEFORE_ZIPS=$(find "${DOWNLOADS_DIR}" -maxdepth 1 -name 'session_report_*.zip' 2>/dev/null | sort) BEFORE_COUNT=$(printf '%s\n' "${BEFORE_ZIPS}" | grep -c . || true) echo " existing session_report_*.zip in Downloads: ${BEFORE_COUNT}" echo # --- profile prep --- PROFILE_HAS_EXTENSION=0 if [[ "${KEEP_PROFILE}" != "1" ]]; then rm -rf -- "${PROFILE_DIR}" fi mkdir -p -- "${PROFILE_DIR}" # Detect if a previous run already loaded the extension into this profile if [[ -d "${PROFILE_DIR}/Default/Extensions" ]] && \ find "${PROFILE_DIR}/Default/Extensions" -maxdepth 3 -name 'manifest.json' 2>/dev/null | head -1 | xargs -r grep -q 'AI Call Recorder' 2>/dev/null; then PROFILE_HAS_EXTENSION=1 green "✓ extension already loaded in profile from previous KEEP_PROFILE=1 run" fi # --- compose the smoke tab data URL --- read -r -d '' SMOKE_HTML <<'EOF' || true Mokosh Smoke Test
T+ 0.0s
wall --:--:--

🧵 Mokosh Smoke Test

This tab is the share-screen target. The picker auto-accepts because the title matches --auto-select-desktop-capture-source.

Steps:

  1. First time only: Go to chrome://extensions → toggle Developer mode ON → Load unpacked → select /home/parf/projects/work/repremium/dist.
    (Set KEEP_PROFILE=1 when re-running this script to skip the reload.)
  2. Click the AI Call Recorder toolbar icon (or puzzle-piece menu).
  3. The picker auto-accepts this tab. Confirm Chrome's "Sharing your screen" indicator appears.
  4. Wait ≥ 35 seconds. Note the timer value in the corner. Move the mouse around or scroll this page so vp9 has frame deltas.
  5. Click the toolbar icon again → click Сохранить отчёт об ошибке. Note T+ and wall at the moment you click — compare to the LAST visible timer values in the saved video. Gap = operator-visible "stale" window.

The script in your terminal will detect the download and finish the ffprobe gate automatically.

EOF # WR-04 fix: python3 is required (asserted in pre-flight). NO fallback — # the previous `|| printf '%s' "${SMOKE_HTML}"` would emit unencoded HTML # into the data URL, causing Chrome to fail to parse the URL silently and # the operator to see a blank tab. Fail loudly if URL encoding fails. SMOKE_DATA_URL="data:text/html,$(printf '%s' "${SMOKE_HTML}" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read(), safe=""))')" # --- launch Chrome --- blue "==> launching Chrome with smoke profile + auto-accept picker..." "${CHROME_BIN}" \ --user-data-dir="${PROFILE_DIR}" \ --auto-select-desktop-capture-source="${SHARE_TARGET}" \ --no-first-run \ --no-default-browser-check \ --new-window \ "${SMOKE_DATA_URL}" \ > "${CHROME_LOG}" 2>&1 & CHROME_PID=$! echo " Chrome PID: ${CHROME_PID}" echo " Tail with: tail -f ${CHROME_LOG}" sleep 4 if ! ps -p "${CHROME_PID}" >/dev/null 2>&1; then # Chrome may have exec'd into a singleton; check if any chrome procs are running with our profile dir if ! pgrep -f "${PROFILE_DIR}" >/dev/null 2>&1; then red "FAIL: Chrome exited within 4 seconds" echo " Last 30 lines of ${CHROME_LOG}:" tail -30 "${CHROME_LOG}" 2>/dev/null || true exit 3 fi fi green "✓ Chrome running" echo # --- prompt --- if [[ ${PROFILE_HAS_EXTENSION} -eq 1 ]]; then yellow "==> Extension is already loaded. Just click the icon → wait 35s → click save." else yellow "==> In the Chrome window:" echo " 1. chrome://extensions → Developer mode ON → Load unpacked → ${DIST_DIR}" echo " 2. Confirm 'AI Call Recorder' appears with no red error." echo " 3. Click the extension icon." echo " 4. WAIT >= 35 seconds." echo " 5. Click the icon again → 'Сохранить отчёт об ошибке'." fi echo blue "==> waiting for a new session_report_*.zip to appear..." echo " (Ctrl+C aborts. Auto-detects, ffprobes, stages fixture, opens WebM.)" echo # --- poll Downloads --- # WR-05 fix: detect by IDENTITY via `comm -13 `, not by # count comparison. The count approach false-positives when ANY # session_report appears (e.g. the operator's daily extension in another # window). `comm -13` returns lines present in but not in # — the genuine new file(s). We still apply mtime sort + head -1 # to pick the latest if multiple new zips materialize (e.g., overlapping # operator activity), but the candidate set is now restricted to actually-new # files. NEW_ARCHIVE="" WAITED=0 while [[ ${WAITED} -lt ${POLL_TIMEOUT} ]]; do AFTER_ZIPS=$(find "${DOWNLOADS_DIR}" -maxdepth 1 -name 'session_report_*.zip' 2>/dev/null | sort) # comm requires sorted streams; both inputs above are pre-sorted. # `-13` keeps only lines unique to file2 (AFTER), suppressing common # lines and lines unique to BEFORE. NEW_ZIPS=$(comm -13 <(printf '%s\n' "${BEFORE_ZIPS}") <(printf '%s\n' "${AFTER_ZIPS}") | grep -v '^$' || true) if [[ -n "${NEW_ZIPS}" ]]; then sleep 2 # let the download settle # Re-snapshot after settle, recompute identity diff, pick latest by mtime AFTER_ZIPS=$(find "${DOWNLOADS_DIR}" -maxdepth 1 -name 'session_report_*.zip' 2>/dev/null | sort) NEW_ZIPS=$(comm -13 <(printf '%s\n' "${BEFORE_ZIPS}") <(printf '%s\n' "${AFTER_ZIPS}") | grep -v '^$' || true) if [[ -n "${NEW_ZIPS}" ]]; then # Pick the latest among the genuinely-new zips by mtime. Quoting note: # NEW_ZIPS is a newline-separated list of full paths from `find`; we # iterate via `while read` to preserve paths with embedded spaces. LATEST="" LATEST_MTIME=0 while IFS= read -r zip_path; do [[ -z "${zip_path}" ]] && continue mtime=$(stat -c %Y -- "${zip_path}" 2>/dev/null || echo 0) if [[ "${mtime}" -gt "${LATEST_MTIME}" ]]; then LATEST="${zip_path}" LATEST_MTIME="${mtime}" fi done <<<"${NEW_ZIPS}" if [[ -n "${LATEST}" ]]; then NEW_ARCHIVE="${LATEST}" break fi fi fi sleep 1 WAITED=$((WAITED + 1)) if [[ $((WAITED % 30)) -eq 0 ]]; then after_count=$(printf '%s\n' "${AFTER_ZIPS}" | grep -c . || true) yellow " ...still waiting (${WAITED}s elapsed, count=${after_count}/baseline=${BEFORE_COUNT})" fi done if [[ -z "${NEW_ARCHIVE}" ]]; then red "FAIL: no new archive appeared in ${POLL_TIMEOUT}s" echo " Chrome log tail:" tail -30 "${CHROME_LOG}" 2>/dev/null || true echo " Chrome still running — close manually or: kill ${CHROME_PID}" exit 4 fi green "✓ archive detected: ${NEW_ARCHIVE}" echo # --- extract + ffprobe gate --- # Bash-style sweep: pass `--` to terminate options on file-taking commands # that accept user-controlled paths (Google shell style guide §"Special # considerations" — defensive against filenames starting with `-`). unzip -p -- "${NEW_ARCHIVE}" video/last_30sec.webm > "${WEBM_TMP}" SIZE_BYTES=$(stat -c %s -- "${WEBM_TMP}") SIZE_HUMAN=$(ls -lh -- "${WEBM_TMP}" | awk '{print $5}') echo " WebM size: ${SIZE_HUMAN} (${SIZE_BYTES} bytes)" if [[ ${SIZE_BYTES} -lt 100000 ]]; then yellow "⚠ WebM is smaller than 100 KB — buffer may not have rotated; capture longer" fi echo blue "==> D-12 ACCEPTANCE GATE — ffprobe -v error" echo "---" # The `&& GATE=0 || GATE=$?` chain is correct: under `set -e`, ffprobe's # non-zero exit doesn't terminate the script because it's followed by `||`. # When ffprobe succeeds, `GATE=0` (an assignment returning 0) is executed # and the `||` branch is skipped. When ffprobe fails, the `&&` chain is # bypassed and `GATE=$?` captures ffprobe's exit. The earlier review note # WR-04 confirmed this is NOT broken. ffprobe -v error -f matroska -i "${WEBM_TMP}" && GATE=0 || GATE=$? echo "---" echo "ffprobe exit: ${GATE}" if [[ ${GATE} -eq 0 ]]; then green "✓ D-12 ACCEPTANCE GATE PASSED" else red "✗ D-12 ACCEPTANCE GATE FAILED — D-13 fallback (restart-segments) required" yellow "==> diagnostic for D-13 escalation:" ffprobe -v error -show_packets -i "${WEBM_TMP}" 2>&1 | head -50 || true fi echo blue "==> stream / format dump (paste this back to the orchestrator):" echo "---" ffprobe -v error -show_format -show_streams "${WEBM_TMP}" 2>&1 | head -30 || true echo "---" echo # --- stage fixture --- if [[ ${GATE} -eq 0 ]]; then mkdir -p -- "$(dirname -- "${FIXTURE_DEST}")" cp -- "${WEBM_TMP}" "${FIXTURE_DEST}" green "✓ fixture staged: ${FIXTURE_DEST} ($(ls -lh -- "${FIXTURE_DEST}" | awk '{print $5}'))" fi # --- open the WebM for visual check --- echo blue "==> opening the WebM for visual playback (SPEC §10 #7)..." "${CHROME_BIN}" --user-data-dir="${PROFILE_DIR}" --new-window "file://${WEBM_TMP}" >/dev/null 2>&1 & echo green "==> smoke complete." echo " Chrome (smoke profile) PID ${CHROME_PID} still running." echo " Kill when done: kill ${CHROME_PID}" echo " To keep the extension loaded across runs: KEEP_PROFILE=1 ./smoke.sh" echo if [[ ${GATE} -eq 0 ]]; then green "==> NEXT: reply 'approved' to the orchestrator with the stream/format dump." else red "==> NEXT: reply 'ffprobe-failed' with the show_packets diagnostic above." fi exit "${GATE}"