65 lines
3.7 KiB
JavaScript
65 lines
3.7 KiB
JavaScript
/* 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');
|
|
}
|