Files
mokosh/smoke.sh
Mark 06dee246c9 feat(01-09): GREEN — toolbar onClicked + badge state machine + onStartup notification + SAVE-only popup
Plan 01-09 Task 4 GREEN — flips all 13 Task 3 RED tests to GREEN:

src/background/index.ts:
  • Badge palette + notification id prefix constants (SCREAMING_SNAKE).
  • setBadgeState(state) helper: 3-state machine REC/OFF/ERROR with
    deterministic setBadgeText + setBadgeBackgroundColor + setTitle.
    Each chrome call wrapped in try/catch (defense in depth).
  • setIdleMode / setRecordingMode / setErrorMode helpers — drive the
    setPopup dance: '' in OFF (so onClicked fires), html path in REC/
    ERROR (so popup opens for SAVE).
  • startVideoCapture wires setRecordingMode on success, setErrorMode
    in catch.
  • chrome.action.onClicked.addListener — direct toolbar-to-picker flow
    (no popup needed for start). isRecording guard prevents double-start.
  • chrome.runtime.onStartup.addListener — fires once per browser
    session; creates mokosh-startup- notification inviting recording.
  • chrome.notifications.onClicked.addListener — T-1-09-01 spoofing
    mitigation via 'mokosh-' prefix gate; clears notification + invokes
    startVideoCapture (notification click is a valid activation gesture).
  • RECORDING_ERROR onMessage branch — setErrorMode + creates a
    mokosh-recovery- notification inviting the operator to restart.
  • initialize() calls setIdleMode at SW boot — ensures fresh OFF state
    on every (re-)spawn including Chrome's idle-eviction respawn.
  • All new listener registrations wrapped in try/catch so unit-test
    chrome stubs that don't define action/notifications/onStartup don't
    crash SW load (preserves the 5 pre-existing request-id-protocol +
    1 port-lifecycle-continuous tests as GREEN).

src/popup/index.ts:
  • Removed checkPermissions + requestPermissions functions entirely
    (no more REQUEST_PERMISSIONS round-trip on popup open).
  • popupState defaults isRecording=true, hasPermissions=true under
    SAVE-only charter — the popup ONLY opens when recording is active
    (REC/ERROR setPopup html path), so SAVE button is always enabled.
  • init() calls updateUI() directly (no async permission probe).
  • Empty-state copy updated: 'Откройте запись через иконку расширения'
    (Open recording via the extension icon — points operator back to
    the toolbar for starting a new session).
  • saveArchive() simplified: no permission re-check.

manifest.json:
  • Added 'notifications' to permissions array (preserves all existing).
  • default_popup retained — popup still opens in REC/ERROR modes.

smoke.sh (W-04 5-sub-step update):
  • SHARE_TARGET='Entire screen' (was 'Mokosh Smoke Test').
  • Added 14-line locale-fallback comment block citing Chromium
    generated_resources.grd as authoritative source + 4 known locale
    strings + KEEP_PROFILE=1 fallback path.
  • <title> changed to 'Mokosh Smoke Test — monitor mode' to keep tab
    title distinct from the screen-source string.
  • <ol> instruction updated: picker auto-accepts entire screen, not
    the tab. Body intro paragraph also updated.
  • T+/wall timer overlay (commit 923aaca) preserved — no behavioral
    change to polling/Downloads-snapshot/ffprobe-gate logic.

Tests: 13/13 new GREEN; full suite 18 files / 81 tests / all GREEN.
tsc --noEmit exit 0. npm run build exit 0; dist/manifest.json has
'notifications' permission. Tier-1 SW-bundle-import gate (Layer 1 + 2)
remains GREEN.
2026-05-17 15:46:25 +02:00

351 lines
17 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"
# Plan 01-09 D-15-display-surface: SHARE_TARGET must match the OS-locale-
# specific name Chrome's picker uses for entire-screen selection. Known
# strings (Chrome stable, observed at plan-write time):
# English: "Entire screen"
# Russian: "Весь экран"
# German: "Gesamter Bildschirm"
# French: "Ecran entier"
# Authoritative source: Chromium's grit resource definitions in
# chrome/app/generated_resources.grd (per-locale .xtb translations under
# chrome/app/resources/). The IDS_DESKTOP_MEDIA_PICKER_SOURCE_TYPE_SCREEN
# identifier lives in the .grd, NOT in the .cc — per-locale translations
# drift across Chrome major versions, so the only reliable check is to
# run the picker once and inspect the source label in Chrome's UI.
# If --auto-select-desktop-capture-source="${SHARE_TARGET}" fails to
# auto-accept on the operator's Chrome locale, the operator picks the
# screen manually one time; KEEP_PROFILE=1 on subsequent runs carries
# the picker's last-pick memory across re-runs, sidestepping the
# auto-select string altogether.
SHARE_TARGET="Entire screen"
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 — monitor mode</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}}
/* Diagnostic timer overlay — high-contrast monospace pinned top-right so it appears in every
recorded frame. T+ counts seconds since page load (matches the moment Mokosh starts recording);
wall is HH:MM:SS local time for cross-referencing with SW console logs. The visible value
in the last frame of the saved video minus the value at save-click time = the operator-visible
"stale gap" the D-13 architecture leaks. */
#mokosh-timer{position:fixed;top:12px;right:12px;background:#000;color:#0f0;padding:10px 16px;
border:2px solid #0f0;border-radius:6px;font-family:'Courier New',monospace;font-size:28px;
font-weight:bold;line-height:1.15;text-align:right;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,.6)}
#mokosh-timer .lbl{font-size:11px;color:#0a0;letter-spacing:.05em}
#mokosh-timer .val{font-variant-numeric:tabular-nums}
</style>
<body>
<div id="mokosh-timer">
<div><span class="lbl">T+</span> <span class="val" id="t-elapsed">0.0s</span></div>
<div><span class="lbl">wall</span> <span class="val" id="t-wall">--:--:--</span></div>
</div>
<h1>🧵 Mokosh Smoke Test</h1>
<p>Plan 01-09: this tab is <em>informational</em>; the picker shares the whole monitor (D-15-display-surface charter). The picker auto-accepts the entire-screen source matching <code>--auto-select-desktop-capture-source="Entire screen"</code> (locale-specific — see SHARE_TARGET comment block in smoke.sh).</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>the entire screen</em> (not this tab — the new Plan 01-09 D-15-display-surface charter constrains to monitor mode). Confirm Chrome's "Sharing your screen" indicator appears.</li>
<li>Wait <strong>≥ 35 seconds</strong>. <em>Note the timer value in the corner</em>. Move the mouse around or scroll this page so vp9 has frame deltas.</li>
<li>Click the toolbar icon again → click <strong>Сохранить отчёт об ошибке</strong>. <em>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.</em></li>
</ol>
<p>The script in your terminal will detect the download and finish the ffprobe gate automatically.</p>
<script>
(function(){
var t0 = performance.now();
var el = document.getElementById('t-elapsed');
var wallEl = document.getElementById('t-wall');
function pad(n){return String(n).padStart(2,'0');}
function tick(){
var dt = (performance.now() - t0) / 1000;
el.textContent = dt.toFixed(1) + 's';
var now = new Date();
wallEl.textContent = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds());
}
tick();
setInterval(tick, 100);
})();
</script>
</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}"