Skip to content

Commit d9e2c47

Browse files
committed
Add browser microphone end-to-end stress test
1 parent 447f98d commit d9e2c47

1 file changed

Lines changed: 147 additions & 0 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
name: browser-mic-e2e
2+
3+
on:
4+
push:
5+
branches:
6+
- stress/browser-mic-e2e-2026-08-15
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
chromium-fake-microphone:
13+
runs-on: ubuntu-24.04
14+
timeout-minutes: 25
15+
env:
16+
VITE_SOFTWARE_REVISION: 447f98dd511201367d6051704e10607d645735dd
17+
steps:
18+
- uses: actions/checkout@v7
19+
- uses: actions/setup-node@v6
20+
with:
21+
node-version: 22
22+
- run: corepack enable
23+
- run: pnpm install --frozen-lockfile
24+
- run: pnpm build
25+
- name: Generate deterministic fake microphone impact WAV
26+
shell: bash
27+
run: |
28+
python - <<'PY'
29+
import math, random, struct, wave
30+
rate = 48000
31+
seconds = 5.0
32+
onset = 0.85
33+
rng = random.Random(20260815)
34+
modes = [(440.0, 0.55, 1.20), (997.0, 0.31, 0.70), (2413.0, 0.17, 0.38)]
35+
path = '/tmp/everything-rings-fake-impact.wav'
36+
with wave.open(path, 'wb') as out:
37+
out.setnchannels(1)
38+
out.setsampwidth(2)
39+
out.setframerate(rate)
40+
frames = bytearray()
41+
for i in range(round(rate * seconds)):
42+
t = i / rate
43+
value = (rng.random() * 2 - 1) * 0.00025
44+
if t >= onset:
45+
dt = t - onset
46+
value += sum(a * math.exp(-dt / tau) * math.sin(2 * math.pi * f * dt) for f, a, tau in modes)
47+
value = max(-0.92, min(0.92, value))
48+
frames += struct.pack('<h', round(value * 32767))
49+
out.writeframes(frames)
50+
print(path)
51+
PY
52+
- name: Install isolated Playwright Chromium
53+
shell: bash
54+
run: |
55+
set -euo pipefail
56+
mkdir -p /tmp/everything-rings-mic-e2e
57+
cd /tmp/everything-rings-mic-e2e
58+
npm init -y >/dev/null
59+
npm install --save-exact playwright@1.55.0
60+
npx playwright install --with-deps chromium
61+
- name: Exercise microphone to exported evidence end to end
62+
shell: bash
63+
run: |
64+
set -euo pipefail
65+
pnpm --filter @everything-rings/web exec vite preview --host 127.0.0.1 --port 4173 --strictPort >/tmp/everything-rings-mic-e2e-vite.log 2>&1 &
66+
server_pid=$!
67+
trap 'kill "$server_pid" 2>/dev/null || true' EXIT
68+
for _ in $(seq 1 40); do
69+
if curl --fail --silent http://127.0.0.1:4173 >/dev/null; then break; fi
70+
sleep 0.25
71+
done
72+
cat >/tmp/everything-rings-mic-e2e/e2e.mjs <<'NODE'
73+
import fs from 'node:fs';
74+
import { chromium } from 'playwright';
75+
76+
const revision = '447f98dd511201367d6051704e10607d645735dd';
77+
const origin = 'http://127.0.0.1:4173';
78+
const browser = await chromium.launch({
79+
headless: true,
80+
args: [
81+
'--use-fake-ui-for-media-stream',
82+
'--use-fake-device-for-media-stream',
83+
'--use-file-for-fake-audio-capture=/tmp/everything-rings-fake-impact.wav',
84+
'--autoplay-policy=no-user-gesture-required',
85+
],
86+
});
87+
const context = await browser.newContext({ acceptDownloads: true });
88+
await context.grantPermissions(['microphone'], { origin });
89+
const page = await context.newPage();
90+
const pageErrors = [];
91+
page.on('pageerror', (error) => pageErrors.push(String(error)));
92+
await page.goto(`${origin}/?lab=1`, { waitUntil: 'networkidle' });
93+
94+
const body = await page.locator('body').innerText();
95+
if (!body.includes(`software revision ${revision}`)) throw new Error('lab does not expose frozen revision');
96+
97+
await page.getByLabel('specimen ID').fill('fake-mic-positive-control');
98+
await page.getByLabel('object').fill('synthetic modal impact');
99+
await page.getByLabel('material').selectOption('metal');
100+
await page.getByLabel('microphone distance').locator('input').fill('20');
101+
await page.getByLabel('striker').fill('deterministic fake microphone waveform');
102+
await page.getByLabel('strike location').fill('synthetic onset');
103+
await page.getByLabel('support condition').fill('CI positive control');
104+
105+
const arm = page.getByRole('button', { name: 'ARM MICROPHONE' });
106+
if (await arm.isDisabled()) throw new Error('ARM MICROPHONE unexpectedly disabled');
107+
await arm.click();
108+
109+
await page.locator('.status-success').waitFor({ state: 'visible', timeout: 20000 });
110+
const modeRows = page.locator('[aria-label="Estimated acoustic modes"] .mode-row:not(.mode-head)');
111+
const modeCount = await modeRows.count();
112+
if (modeCount < 3) throw new Error(`expected at least 3 modes after microphone analysis, got ${modeCount}`);
113+
const modeText = await modeRows.allInnerTexts();
114+
const frequencies = modeText.map((text) => Number.parseFloat(text));
115+
for (const expected of [440, 997, 2413]) {
116+
if (!frequencies.some((actual) => Number.isFinite(actual) && Math.abs(actual - expected) <= Math.max(5, expected * 0.01))) {
117+
throw new Error(`expected resonance near ${expected} Hz; observed ${JSON.stringify(frequencies)}`);
118+
}
119+
}
120+
121+
const downloadPromise = page.waitForEvent('download');
122+
await page.getByRole('button', { name: 'EXPORT EVIDENCE' }).click();
123+
const download = await downloadPromise;
124+
const exportPath = '/tmp/everything-rings-mic-e2e/evidence.json';
125+
await download.saveAs(exportPath);
126+
const evidence = JSON.parse(fs.readFileSync(exportPath, 'utf8'));
127+
if (evidence.schemaVersion !== 5 || evidence.evidenceContractVersion !== 'validation-evidence-5') {
128+
throw new Error('exported evidence has wrong schema contract');
129+
}
130+
if (evidence.softwareRevision !== revision) throw new Error(`wrong exported revision ${evidence.softwareRevision}`);
131+
if (evidence.attemptCount !== 1 || evidence.attempts?.length !== 1) throw new Error('expected one retained qualified attempt');
132+
if (evidence.attempts[0]?.analysis?.status !== 'success') throw new Error('exported microphone attempt is not analytically successful');
133+
if (evidence.rawMicrophoneSamplesIncluded !== false) throw new Error('raw PCM invariant violated');
134+
if (pageErrors.length > 0) throw new Error(`uncaught page errors: ${pageErrors.join(' | ')}`);
135+
console.log(JSON.stringify({ modeCount, frequencies, quality: evidence.attempts[0].quality }, null, 2));
136+
await browser.close();
137+
NODE
138+
cd /tmp/everything-rings-mic-e2e
139+
node e2e.mjs | tee /tmp/everything-rings-mic-e2e-result.json
140+
- uses: actions/upload-artifact@v4
141+
if: always()
142+
with:
143+
name: browser-mic-e2e-report
144+
path: |
145+
/tmp/everything-rings-mic-e2e-result.json
146+
/tmp/everything-rings-mic-e2e/evidence.json
147+
if-no-files-found: warn

0 commit comments

Comments
 (0)