/* global OfflineAudioContext */ // 실제 Chrome의 WAV 디코더로 101개를 검증한다. 재생·UI 흐름은 전체 // Playwright suite가 별도 담당하며 이 인수 검사는 모의 AudioContext를 쓰지 않는다. import assert from 'node:assert/strict'; import { writeFileSync } from 'node:fs'; import { spawn } from 'node:child_process'; import { chromium } from '@playwright/test'; const server = spawn(process.execPath, ['node_modules/vite/bin/vite.js', 'preview', '--host', '127.0.0.1', '--port', '4174', '--strictPort'], { stdio: 'ignore' }); const base = 'http://127.0.0.1:4174'; let browser; try { for (let i = 0; ; i++) { try { if ((await fetch(base)).ok) break; } catch { /* localhost 준비만 기다린다. */ } assert(i < 100, 'preview startup failed'); await new Promise(resolve => setTimeout(resolve, 100)); } browser = await chromium.launch({ executablePath: '/usr/bin/google-chrome', chromiumSandbox: true }); const sandbox = await browser.newPage(); await sandbox.goto('chrome://sandbox'); const sandboxStatus = await sandbox.locator('body').innerText(); console.log('SANDBOX STATUS\n' + sandboxStatus); assert.match(sandboxStatus, /Seccomp-BPF sandbox\s+Yes/); await sandbox.close(); const page = await browser.newPage(); const external = [], errors = []; page.on('request', r => { if (!r.url().startsWith(base + '/')) external.push(r.url()); }); page.on('pageerror', e => errors.push(e.message)); await page.goto(base); assert.equal(await page.title(), '단어를 맞춰라 · Find-Word v0.1.1'); assert.equal(await page.locator('.version').innerText(), 'v0.1.1'); const result = await page.evaluate(async () => { const manifest = await (await fetch('/audio/manifest.json')).json(); const context = new OfflineAudioContext(1, 1, 24000); const rows = []; for (const entry of manifest.files) { const response = await fetch('/audio/' + entry.file); if (!response.ok) throw Error(entry.file + ': HTTP failure'); const bytes = await response.arrayBuffer(); const digest = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), b => b.toString(16).padStart(2, '0')).join(''); if (digest !== entry.sha256 || bytes.byteLength !== entry.bytes) throw Error(entry.file + ': integrity failure'); const buffer = await context.decodeAudioData(bytes); let power = 0, peak = 0; for (const x of buffer.getChannelData(0)) { power += x*x; peak = Math.max(peak, Math.abs(x)); } if (buffer.sampleRate !== 24000 || buffer.numberOfChannels !== 1 || Math.abs(buffer.duration-entry.duration)>1e-9) throw Error(entry.file + ': decode format/duration mismatch'); const rms = Math.sqrt(power/buffer.length); if (rms <= 100/32768 || peak >= 32767/32768) throw Error(entry.file + ': signal failure'); rows.push({ file: entry.file, sha256: digest, bytes: entry.bytes, sampleRate: buffer.sampleRate, channels: buffer.numberOfChannels, duration: buffer.duration, rms, peak }); } return { version: manifest.version, voice: manifest.voice, files: rows }; }); assert.equal(result.version, '0.1.1'); assert.equal(result.voice, 'ko-KR-HyunsuMultilingualNeural'); assert.equal(result.files.length, 101); assert.deepEqual(external, []); assert.deepEqual(errors, []); writeFileSync('.agent/evidence-v0.1.1/browser-audio-integrity.json', JSON.stringify({ chrome: browser.version(), sandboxStatus, external, errors, ...result }, null, 2)+'\n'); console.log('PASS: sandboxed Chrome decoded 101/101 WAVs at 24kHz mono; hashes, bytes, duration, RMS, peak verified; external requests=0, JS errors=0.'); } finally { await browser?.close(); server.kill('SIGTERM'); }