[object Object]

← back to Dw Photo Capture

5x sweep 3: hung-camera e2e mode (all engines), WebKit stub fix, sweep aborts if server dies

bf95f56ebbf03baebb183a91fbd5e58710cbfde4 · 2026-09-25 11:00:40 -0700 · Steve Abrams

TK-12228

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yv665HsXLibk8kGheHT1P

Files touched

Diff

commit bf95f56ebbf03baebb183a91fbd5e58710cbfde4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 25 11:00:40 2026 -0700

    5x sweep 3: hung-camera e2e mode (all engines), WebKit stub fix, sweep aborts if server dies
    
    TK-12228
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_017yv665HsXLibk8kGheHT1P
---
 5x/sweep-3.md                      | 22 ++++++++++++++++++++++
 5x/tools/sweep.sh                  |  7 ++++++-
 scripts/e2e-front-back-display.cjs |  7 ++++++-
 3 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/5x/sweep-3.md b/5x/sweep-3.md
new file mode 100644
index 0000000..d22af32
--- /dev/null
+++ b/5x/sweep-3.md
@@ -0,0 +1,22 @@
+# /5x sweep 3 — after 4fd7dee (server restarted)
+
+## Verify (5x/tools/sweep.sh 3 → 5x/out/sweep-3/sweep-summary.txt)
+- E2E journey: Chromium 19/19, WebKit 19/19, Firefox 19/19, WebKit warm 19/19 (all LIVE path)
+- NEW hung-camera runs (getUserMedia never settles): Chromium 21/21 SNAP+SNAP, Firefox 21/21 SNAP+SNAP,
+  **WebKit 19/19 but took the LIVE path** — i.e. the hung-camera stub did not apply in WebKit (#1).
+- /3x `/`, `/captures`, `/batch`: M1 M2 M3 B4 PASS.
+- Then **the test server process died mid-sweep** (during /3x /batch clickthrough; background task exit 144,
+  nothing in the server log — the server logs every uncaughtException, so not a JS crash; same silent death as
+  sweep 1). Everything after it (/3x /cam, probe, toolbar, history, missing-side) failed on connection refused →
+  **sweep 3 is invalid as a clean/unclean verdict; not counted toward clean-twice.**
+
+## Caught → diagnosis
+1. Harness: WebKit exposes `navigator.mediaDevices` lazily, so assigning `getUserMedia` on the instance in an
+   init script didn't stick. Stub now patches `MediaDevices.prototype.getUserMedia` too, and a new check asserts a
+   HUNG_CAMERA run really took the timeout→SNAP path (so this can't silently pass again).
+2. Env: test-server killed externally twice (exit 144 = signal, no stack). No self-exit path in server.js
+   (`process.exit` only for missing DWP_TEST_DIR / EADDRINUSE). Not an app defect. sweep.sh now health-checks
+   the server after every stage and ABORTS the sweep ("sweep invalid") instead of recording false failures.
+
+Negative proof of the sweep-2 fix (5x/out/pre3/wk-hung-NEGATIVE-prefix.log): with 15beb44's index.html swapped
+back in, the WebKit hung-camera journey goes RED ("waiting for event filechooser" timeout); with the fix, 21/21.
diff --git a/5x/tools/sweep.sh b/5x/tools/sweep.sh
index 5b62098..c5f0fbe 100755
--- a/5x/tools/sweep.sh
+++ b/5x/tools/sweep.sh
@@ -3,13 +3,18 @@
 N=${1:?sweep number}; R=/Users/macstudio3/Projects/dw-photo-capture; O=$R/5x/out/sweep-$N; mkdir -p $O
 cd $R
 grep -q "STUBBED (--test)" /tmp/dwp-e2e-server.log || { echo "ABORT: server not in --test"; exit 3; }
-S=0; T=0; tally(){ T=$((T+1)); if [ "$1" = 0 ]; then S=$((S+1)); echo "  PASS $2"; else echo "  FAIL $2"; fi; }
+S=0; T=0; alive(){ curl -s -o /dev/null -m 5 http://127.0.0.1:9987/healthz || { echo "ABORT: test server died mid-sweep (before: $1) — sweep invalid"; exit 4; }; }
+tally(){ alive "$2"; T=$((T+1)); if [ "$1" = 0 ]; then S=$((S+1)); echo "  PASS $2"; else echo "  FAIL $2"; fi; }
 for E in chromium webkit firefox; do
   ENGINE=$E TESTROOT=/tmp/dwp-e2e OUT=/tmp/dwp-e2e/shots-sweep$N-$E node scripts/e2e-front-back-display.cjs > $O/e2e-$E.log 2>&1; tally $? "e2e front/back journey [$E] ($(tail -1 $O/e2e-$E.log); $(grep -m1 "capture path" $O/e2e-$E.log))"
 done
 WARM_CAMERA=1 ENGINE=webkit TESTROOT=/tmp/dwp-e2e OUT=/tmp/dwp-e2e/shots-sweep$N-webkit-warm node scripts/e2e-front-back-display.cjs > $O/e2e-webkit-warm.log 2>&1; tally $? "e2e front/back journey [webkit, warm camera → LIVE path] ($(tail -1 $O/e2e-webkit-warm.log))"
+for E in chromium webkit firefox; do
+  HUNG_CAMERA=1 ENGINE=$E TESTROOT=/tmp/dwp-e2e OUT=/tmp/dwp-e2e/shots-sweep$N-$E-hung node scripts/e2e-front-back-display.cjs > $O/e2e-$E-hung.log 2>&1; tally $? "e2e journey [$E, HUNG camera → timeout → SNAP file-input path] ($(tail -1 $O/e2e-$E-hung.log); $(grep -m1 "capture path" $O/e2e-$E-hung.log))"
+done
 for p in / /captures /batch /cam; do n=$(echo $p|tr -d /); n=${n:-root}
   node ~/.claude/skills/3x/run.js --url "http://127.0.0.1:9987$p" --user admin --pass 'DW2024!' --paths "/,/captures,/batch,/cam" --no-open --clickthrough 1 > $O/3x-$n.log 2>&1
+  alive "3x $p"
   echo "  3x $p: $(grep -E '^\s+\[(PASS|FAIL|SKIP)\] (M|B)' $O/3x-$n.log | awk '{print $1,$2}' | tr '\n' ' ') | $(grep 'CLICK-THROUGH' $O/3x-$n.log | sed 's/ *CLICK-THROUGH → //')"
 done
 JSON=$O/probe.json node 5x/tools/console-probe.cjs > $O/probe.log 2>&1; tally $? "console/pageerror/4xx/broken-img probe 3 engines x 4 pages"
diff --git a/scripts/e2e-front-back-display.cjs b/scripts/e2e-front-back-display.cjs
index eec9857..527c4c2 100644
--- a/scripts/e2e-front-back-display.cjs
+++ b/scripts/e2e-front-back-display.cjs
@@ -38,6 +38,10 @@ const allLoaded = l => l.length > 0 && l.every(x => x.w > 0 && x.h > 0);
   console.log('engine:', ENGINE, browser.version());
   const page = await ctx.newPage();
   page.on('pageerror', e => console.log('[pageerror]', e.message));
+  // HUNG_CAMERA=1: getUserMedia never settles (iOS denied-but-not-rejected prompt / camera held by FaceTime) —
+  // deterministically drives the timeout → snap-camera (<input capture> → shotAdd) path in ANY engine.
+  if (process.env.HUNG_CAMERA === '1') await page.addInitScript(() => { const hang = () => new Promise(() => {});   // prototype too: WebKit exposes mediaDevices lazily
+    if (window.MediaDevices) MediaDevices.prototype.getUserMedia = hang; if (navigator.mediaDevices) navigator.mediaDevices.getUserMedia = hang; });
 
   // ── 1. index.html ADD flow: live two-shot camera → FRONT then BACK ──
   await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
@@ -45,7 +49,7 @@ const allLoaded = l => l.length > 0 && l.every(x => x.w > 0 && x.h > 0);
     new MutationObserver(() => window.__toasts.push(t.textContent)).observe(t, { childList: true, characterData: true, subtree: true }); });
   // WARM_CAMERA=1: start+stop the camera once first (a phone whose camera is already awake) so engines with a slow
   // cold mock device (WebKit) also cover the LIVE two-shot path; unset = cold start → exercises the snap fallback.
-  if (process.env.WARM_CAMERA === '1') await page.evaluate(async () => { const st = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 4096 }, height: { ideal: 3072 } } }); st.getTracks().forEach(t => t.stop()); });
+  if (process.env.WARM_CAMERA === '1' && process.env.HUNG_CAMERA !== '1') await page.evaluate(async () => { const st = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 4096 }, height: { ideal: 3072 } } }); st.getTracks().forEach(t => t.stop()); });
   await page.click('#homeScreen [data-act="add"]');
   const opened = await page.waitForSelector('#twoShotCam:not([hidden])', { timeout: 12000 }).then(() => true).catch(() => false);
   // real JPEG files for the native snap-camera fallback (<input type=file capture> → shotAdd)
@@ -90,6 +94,7 @@ const allLoaded = l => l.length > 0 && l.every(x => x.w > 0 && x.h > 0);
       await snapSide('back');
     }
   }
+  if (process.env.HUNG_CAMERA === '1') check('HUNG_CAMERA run actually took the timeout → SNAP path (camera stub effective)', !opened);
   await page.waitForSelector('#frontImg:not([hidden])'); await page.waitForSelector('#backImg:not([hidden])');
   const tiles = await imgs(page, '#frontImg, #backImg');
   check('immediate preview: FRONT + BACK tiles both render', tiles.length === 2 && allLoaded(tiles), tiles);

← 4fd7dee 5x sweep 2: camera-timeout retry tap now opens the snap came  ·  back to Dw Photo Capture  ·  5x sweep 4: history evidence screenshot viewport-only (Firef 0e8aa0e →