Files
mokosh/smoke.sh

297 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
<title>Mokosh Smoke Test</title>
<style>body{font-family:sans-serif;background:#222;color:#eee;padding:40px;line-height:1.5}code{background:#444;padding:2px 6px;border-radius:3px}ol li{margin:6px 0}.flash{animation:flash 1s infinite;background:#0a4;padding:4px 8px;display:inline-block}@keyframes flash{0%,100%{opacity:1}50%{opacity:.4}}</style>
<body>
<h1>🧵 Mokosh Smoke Test</h1>
<p>This tab is the share-screen target. The picker auto-accepts because the title matches <code>--auto-select-desktop-capture-source</code>.</p>
<h2>Steps:</h2>
<ol>
<li><strong class="flash">First time only:</strong> Go to <code>chrome://extensions</code> → toggle <strong>Developer mode</strong> ON → <strong>Load unpacked</strong> → select <code>/home/parf/projects/work/repremium/dist</code>.<br>(Set <code>KEEP_PROFILE=1</code> when re-running this script to skip the reload.)</li>
<li>Click the <strong>AI Call Recorder</strong> toolbar icon (or puzzle-piece menu).</li>
<li>The picker auto-accepts <em>this tab</em>. Confirm Chrome's "Sharing your screen" indicator appears.</li>
<li>Wait <strong>≥ 35 seconds</strong>. Move the mouse around or scroll this page so vp9 has frame deltas.</li>
<li>Click the toolbar icon again → click <strong>Сохранить отчёт об ошибке</strong>.</li>
</ol>
<p>The script in your terminal will detect the download and finish the ffprobe gate automatically.</p>
</body>
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 <before> <after>`, 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 <after> but not in
# <before> — 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}"