← 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
A 5x/sweep-3.mdM 5x/tools/sweep.shM scripts/e2e-front-back-display.cjs
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 →