fix: version neural audio requests for v0.1.1

This commit is contained in:
oz
2026-10-14 07:31:47 +00:00
parent a92b2ba1e9
commit c92660b514
38 changed files with 3400 additions and 42 deletions
+43
View File
@@ -144,3 +144,46 @@ Codex가 새로 작성한 입력 문서가 아니다. 전체 파일 목록은
- 원자 교체는 Linux와 해당 파일시스템의 지원이 필요하다. SIGKILL/전원 손실 때 임시
숨김 폴더는 남을 수 있고 전원 손실 내구성까지 보장하지 않는다. 일반 실패·Ctrl-C는 정리한다.
- 실제 모바일 Safari/물리적 오디오 장치 청취는 이번 Chrome 검증 범위 밖이다.
## 2026-10-14 추가 — 배포 후 오디오 캐시 회귀 수정
제품 버전 **0.1.1**을 유지하면서 intro와 목표 100개 WAV 모두
`?v=0.1.1`을 붙이도록 수정했다. `src/audio/assets.ts`의
`AUDIO_ASSET_VERSION`과 `audioAssetUrl`을 출제·오디오 엔진이 함께 사용한다.
WAV/manifest 및 package/lock은 변경하지 않았다.
발견 원인은 **음성 내용 교체 후에도 요청 URL이 같았던 것**이다. 사용자 보고의
배포 Nginx `/audio` 7일 캐시에서 기존 사용자는 이전 음성을 계속 받을 수 있었다.
기존 배포 문서의 재검증 가정과 실제 설정이 달랐으며, 브라우저 테스트의
전체 URL `.endsWith('.wav')`와 404 glob도 query string을 놓쳤다.
원격 서버에는 접속하지 않고 소스·로컬 production build에서 재현했다.
앱 구현을 수정하기 전에 unit/browser 계약을 작성하고 실제 RED를 확인했다.
RED 때 앱 소스가 그대로였다는 기록은 [04-red-state.json](evidence-v0.1.1/cache-regression/04-red-state.json)에 있다.
| 계약/검증 | 결과 | 새 evidence |
| --- | --- | --- |
| 목표 100개 버전·intro/target fetch·각각 404 retry·query pathname 판정 | RED 5 failed / 12 passed → GREEN 17 passed | [RED](evidence-v0.1.1/cache-regression/01-unit-red.log), [GREEN](evidence-v0.1.1/cache-regression/05-unit-green.log) |
| 실제 브라우저 요청/정답 mapping·intro/target 404·101개 URL | 구현 전 desktop RED 4 failed → 전체 desktop/mobile GREEN 16 passed (2.6m) | [RED](evidence-v0.1.1/cache-regression/03-browser-red.log), [GREEN](evidence-v0.1.1/cache-regression/09-browser-green.log) |
| `npm run lint` | PASS | [로그](evidence-v0.1.1/cache-regression/06-lint.log) |
| `npm test` | 12 files / 65 passed, skip 0; Python 18개 계약 포함 | [로그](evidence-v0.1.1/cache-regression/07-full-test.log) |
| `npm run build` | PASS, 101개 WAV·notice 검증 | [로그](evidence-v0.1.1/cache-regression/08-build.log) |
| 실제 Chrome sandbox | Namespace·PID·Network·Seccomp-BPF 활성, `--no-sandbox` 없음 | [로그](evidence-v0.1.1/cache-regression/11-sandbox-green.log) |
| query URL 전수 Chrome 해시·bytes·24kHz mono 디코드·duration·RMS·peak | 두 viewport 각각 101/101, 외부 요청 0·JS 예외 0 | [desktop](evidence-v0.1.1/cache-regression/artifacts/desktop-browser-audio-integrity.json), [mobile](evidence-v0.1.1/cache-regression/artifacts/mobile-browser-audio-integrity.json) |
| 과거 evidence·자산·제품 버전 파일 보존 및 HEAD·diff 검사 | 254개 해시 동일, 과거 evidence 149개 포함 | [최종 무결성](evidence-v0.1.1/cache-regression/13-final-integrity.json) |
전체 브라우저 명령은
`env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome DEBUG=pw:browser npm run test:browser`이다.
실제 재생·재청취·10문제 완주·404 복구 후 정답·접근성 검증을 유지했다.
101개 query URL 검사를 `tests/browser/audio.spec.ts`로 정규 suite에 편입했고,
URL 판정 helper의 query 순서·가짜 WAV 접미사도 단위 테스트로 고정했다.
런타임 외부 요청이나 manifest 조회는 추가하지 않았다.
브라우저 스크린샷은 이제 `test-results/`를 사용하며 이번 결과는
[새 artifacts 경로](evidence-v0.1.1/cache-regression/artifacts/)에 복사했다.
과거 evidence와 기존 이 보고서 본문은 보존하고 이번 기록만 추가했다.
보조 sandbox probe의 최초 라벨 판정 오류와 수정 결과도
[원인·RED/GREEN·최종 결과 로그](evidence-v0.1.1/cache-regression/12-cache-regression-result.log)에 기록했다.
README·ARCHITECTURE·DEPLOYMENT에 자산 버전 갱신 규칙과 7일 캐시 예시를 반영했다.
이 변경의 검증 대상은 로컬 build이며 **Git commit/push/tag/merge와 서버 배포는 실행하지 않았다.**
@@ -0,0 +1,259 @@
{
"head": "a92b2ba1e973c65a57e6e34cc52568cd6b208c1e",
"files": {
".agent/evidence/01-content-green.log": "5585acd257b49225218835acd69a1d240c7737d12dba62949f31a5b7c1c9ed2a",
".agent/evidence/01-content-red.log": "0f28fe2ae2c4669a52ba2fc15bfa6285e4922474dc2b2571098b522679948e12",
".agent/evidence/02-game-green.log": "5ca5895f8a09f4e9de7a846bf9ab37fb3715623e8e7654dab095505c097fc0c7",
".agent/evidence/02-game-red.log": "78468ee5c25c1685f3613926f636b6fb4569227533feda3547afc6b8123d6452",
".agent/evidence/03-dsp-green.log": "d3d08683a1426b845a2e987572861b82b3ac978b8c352991950c0f32a473d3de",
".agent/evidence/03-dsp-intermediate.log": "084a52294f718718256eb1516048a672d045a5f7dcb5d11c646092d29efbab58",
".agent/evidence/03-dsp-red.log": "bacf42e5c808d32702b9bcd30523d1fcf17d5ff75b281233d9e84f33b6e42589",
".agent/evidence/04-audio-green.log": "d48c93fa7d32315ed4f719dee19222e7398bc4c4bceb8cf345e0e221a9ac89d8",
".agent/evidence/04-audio-red.log": "58bbf8eae572f83e408dd101eac738a5f6963550e21c8265f42a7eeba18be7a7",
".agent/evidence/05-assets-red.log": "04388acb098f7dabe9a35783b496075ee7c39757d3fde5361c9ac080188ac371",
".agent/evidence/06-ui-05-assets-green.log": "ced2ad738cda673b24308dcb9d99d59e3ae918709eeb32de359d6ab1022133e2",
".agent/evidence/06-ui-intermediate.log": "3295b3ae691dadb8feb7f0aaf604a1aa56cbfae27b5dbdd486cb5645f5edabe9",
".agent/evidence/06-ui-red.log": "1f1ed6bd440af22f5abf59e4be194f969bee1d115d436a155d879d8cffddb747",
".agent/evidence/07-build-green.log": "ec98087d20a68c5b81c6c23406316f1e5f19c6124498e1fc005d04bb78894a57",
".agent/evidence/07-build-red.log": "138802de7bc67c1535668efe7a915424a70d5d7994acb7f366a139e89c1da77f",
".agent/evidence/08-rule-green.log": "91b7ce3fda181fbe51cbe4e6debb61dd44dd55226dcfa81ed73559ba76811f3f",
".agent/evidence/08-rule-red.log": "2cc1854cf6a4b9e8d53a7a36b1dde1fb6c6970b89f841b9e8f404e6e42bb929e",
".agent/evidence/09-ime-green.log": "e5d5d1cfa2b6db724ded97d5f3309a517bcf637d9b2b417e9110dc9da0a48d72",
".agent/evidence/09-ime-red.log": "10e5f9f99609044c768cd338df0d574a06a7442e0debe0e61ca16a6b5873adf3",
".agent/evidence/10-ime-reset-green.log": "e12761b3a2814a79d1448e7452474a086bf371921287718eb72dd9b0f6b8fc82",
".agent/evidence/10-ime-reset-red.log": "f6cb7c1724b8f9c410385e9970051577c03527fb7e719f43a77c967ca08ef18f",
".agent/evidence/11-provenance-green.log": "54dcc417b97f82422c5c3e0ae11caa1f15a60412853ea1dc0152d15031bcd935",
".agent/evidence/11-provenance-red.log": "e69fa7989223e659c16b8030f8668e562ad5b0daa060fbb1c9eff41a37fd56fd",
".agent/evidence/12-ime-replay-green.log": "a93d9f16c7d81ed5e9a7c0b70ffee1e393d2943ddf3149a5bb115c5517327c44",
".agent/evidence/12-ime-replay-red.log": "8cf5dab06d93cc544c21276ee633829f8b137b4b6d7c5fe47aadcad6fc2afb71",
".agent/evidence/13-heading-build-after.log": "19829ac264f97aa023baf7d5e1fd429343272f3bd952e8b6dfb9eec0f594b453",
".agent/evidence/13-heading-build-before.log": "646d197d2ef93c19fec4719e20297de1b22eff54f3e6155ba2125fd3dd372c46",
".agent/evidence/13-heading-green.log": "d59fa8546083e9d371c7a552287f27bf3acd572bd0cce3da8eba860da5c05880",
".agent/evidence/13-heading-red.log": "33acc2838786a87b8ecc170a1ac0a9d7521daf8e5b9aed2195e76e45ea327f76",
".agent/evidence/14-snr-browser-green.log": "fcb82296e9af4b96112fae0bbb77c6ba41230c117021243e85916ae1130a462d",
".agent/evidence/14-snr-browser-red.log": "36298e5bb1f45a69b0ca75e5cbce85ee1455c4485759670268fbbcf55bacd4d4",
".agent/evidence/14-snr-build.log": "1e9ce36a48904a58195f9d1c2d9ca6cf2895fcb8bd0b5ab9b72f76e92c6b2585",
".agent/evidence/14-snr-green.log": "ae74e57cea20841299a8c1ee5eaf60efad34bda697339f45527cdec83578e6df",
".agent/evidence/14-snr-red.log": "b39bdbd8815d0d9cfcedc41c61124afece2011ea6c6c250b0ee0139eae7e9edd",
".agent/evidence/15-volume-browser-green.log": "aeccd5f13eb9db4062cbf2c70eee332d0c39540d580e113d0a79a899b2ef02de",
".agent/evidence/15-volume-browser-red.log": "84954280c1729703f9e9c2680c8933c3b0752b19fb9c3562e7dbf2ea1b4a548c",
".agent/evidence/15-volume-build.log": "2767b794124804b3447f529eef025e2530975fcc506e1cbe8994acf8243e6ba8",
".agent/evidence/15-volume-green.log": "0f9a90923f4fdc477c8f2436653eb2bf0e4b251bcb52e795701309ba14f23c7d",
".agent/evidence/15-volume-red.log": "9beae6305080cd0d869bf3fd2093beb6e2e25e5424223aa7e4ad4db96954f37e",
".agent/evidence/16-copy-green.log": "dacbdbab5d4af2b6e19eb4e0502a70720aac9fb6cf3b9d6dfdc73a5bf7edabfe",
".agent/evidence/16-copy-red.log": "ebe3d7c86938d33d7f1a4818b18c7e94ab8feb18bee099fcb535a93d8083861b",
".agent/evidence/17-spacing-green.log": "8ab85362ceadcdfecf4f7b1e95abfb4237e15018692e14f937be90265c351b11",
".agent/evidence/17-spacing-red.log": "12db8083cd8627a81a52cd32358780a3c2257cb3bd702350b190b39226f6550f",
".agent/evidence/18-literal-green.log": "878a8d788a8cedbf652fe0e819fe482099249a52ae7b5cc3d8826e64b32c6377",
".agent/evidence/18-literal-red.log": "ad23ae7be3d9aab83907c12849012a01f9d1b12e5a174c3acede89817a74d2d2",
".agent/evidence/19-notices-green.log": "efc51aca0738f3cb5264ec3c3ad0d17bb150d685f5fdb5e200bc1eedc0e3c02b",
".agent/evidence/19-notices-red.log": "8f34c583cb8563803828d72fe56622d84bcf59a9541d983d6c0b705225955794",
".agent/evidence/20-audio-generate.log": "f864fc266f7ca50c600457399f198a77949e9ee1601b0d2fc19b64942a2030cb",
".agent/evidence/20-audio-green.log": "edb9b9f0aa1af11835960eee9bb6e8c013341aa528d2857bac0b89ac38457528",
".agent/evidence/20-audio-red.log": "c1cbd5b585e8a70e23b88bfcee0c6479f85fdec0d1e06d13c81c689a140c0ac0",
".agent/evidence/20-mechanics-green.log": "9776c68143f668e35df8b2e19884b25d2369e4800b4c264e635d0c260818c5f1",
".agent/evidence/20-mechanics-red.log": "d25a1c844366e14fe1db9bb7c243fe0adb6857ee5b4548cf2865de96589af647",
".agent/evidence/21-node-green.log": "3c36e9e45b6844ae2c184f943b61195482d9de53ceab06221093c8281d210d73",
".agent/evidence/21-node-red.log": "ee25556037bdbd6b099dbb087a522284e8df00bd573dfcf0e42c734e31e09e84",
".agent/evidence/22-sandbox-green.log": "e8a1a17e49c22dea81d7f85111c744473427466791ddeda75047cd0082e7faba",
".agent/evidence/22-sandbox-red.log": "c8a97270f0f68367d6edb3e5ee10f34bbda1127f8dae2c5639afd482198e7bcd",
".agent/evidence/23-delivery-browser-red.log": "180a61bbacf85db9e52f721b50da683103b719bc350454cc99d4c8954b2ae86e",
".agent/evidence/24-retry-audio-budget.log": "b9241872b4ecd80265df4c6c2be90b31e9de4d0754772655e50b7fb4f05e2d3b",
".agent/evidence/24-retry-final-browser.log": "68029aae89adf86fd60ee56a20a1e2f769cb4f894f27ae23963ca5b5b58c62c4",
".agent/evidence/24-retry-final-build.log": "702c20554872e437137097ca59a80d911188477f018bc48c4b49a770b73d5a84",
".agent/evidence/24-retry-final-lint.log": "8682ca1f55e3ec58f13321cc339c8647ef72c1934d74ea9b980aee717e528819",
".agent/evidence/24-retry-final-test.log": "b71bb1375a039bcc3d7b3c5070da85ece3551f32ae1b8bb3a2abc5ef1edd57b8",
".agent/evidence/24-retry-integrity.log": "a188fe54dce05f6caa868b3d9f2c22a852f6a8cdc462355673316578be623a08",
".agent/evidence/24-retry-targeted-green.log": "4e53f2de988040540f5075b2e4a483ca53b671486c35e864fbc3c2c9c5361c61",
".agent/evidence/audio-reproduce.log": "5f5726f3ed6845882c7a6d6f9a8d898c2b73bf61fa6328e8bbafd25f61ed4268",
".agent/evidence/browser-final.log": "9273c91613160e239027452894b3735f9657ccad97a52412eb889290aebd5adf",
".agent/evidence/browser-first.log": "f3087028d76aae36d61d7a14da488bd809079a9e121a2d8fb01fa034f99d4f05",
".agent/evidence/browser-second.log": "2ff1d16f51b9384cf431d5923ad87d773510fe4b89bdf62f09cbda2efae20e3b",
".agent/evidence/browser-third.log": "1b6c264930da8934e7f1c8da6c998543f35e68b1c3159dad722cfe23a8e5244c",
".agent/evidence/changed-files.txt": "2b609231e5c7becd581f79413a4f7443312e71f0099ac6a51b2c5f0dfdee576f",
".agent/evidence/desktop-game.png": "42702e89264357257db8bf54c2b3743e3212e62cfd3476a8c25bad83fb59ff81",
".agent/evidence/desktop-home.png": "bf2f515fcd483cd5ca9a3b408f484d3556b75b1be0bb70360b7fd5f7c3404993",
".agent/evidence/desktop-results.png": "003df3aeb08ce66618775377d9b2003809d696629f61cf54b7b86e7a0ed63593",
".agent/evidence/final-audit.log": "6d8c5c8f3d7684adb070417bd608d01ae90aa3dc26a65af03ffda4955f38d9a3",
".agent/evidence/final-browser.log": "5855691def462b23667a9d658338a0458a53d984b5b5f7e11b45f9a63092abcd",
".agent/evidence/final-build.log": "565a95a51a10f78bc08f256755afce5ef183440dcdea5e75b3aefde8d7c5b837",
".agent/evidence/final-lint.log": "d2354c241320f11f07890cbf32f99a5d88f1c7f4f236d860e2cd26db739badd8",
".agent/evidence/final-security.log": "c585566624a4596e97327fb1864b1ac2a3189c0794b1e25a9d6ff1abb86046cb",
".agent/evidence/final-test.log": "af03027cb22a090ecd107c10e3fc7e2f9ceb394122b0b73caa7d362310504170",
".agent/evidence/mobile-game.png": "20a3f02f70a28b2faae1b33d0d4d6fea10b77caa426e4bf9faa5ef6d4474a846",
".agent/evidence/mobile-home.png": "7f84bd54183fcdd03fe136b41145146ddfb323041c96a8b9e6b670c9a923033a",
".agent/evidence/mobile-results.png": "ec2938c03ac03da8e33e25f043522b317b60c16be1c29f9dd325a81ae84513a8",
".agent/evidence/neo-before-spacing-browser.log": "e5c214f14bd93ef1bb731f87ecfd3a02d299c9f9e5a788233f6eda5eb8946b4f",
".agent/evidence/neo-desktop-game.png": "f5966688ba46f14fab5125452f935e55f6e6dc4355c876416729866129daea70",
".agent/evidence/neo-desktop-heading.png": "db609f2f54af0e6b1bb96bce9bf839454403d6e2787e6520a2928fad31beb021",
".agent/evidence/neo-desktop-home.png": "826c5da794c4ea3c15bd6c13e81d1c021fbeb8a902fc4908b78f9c457f091c0f",
".agent/evidence/neo-desktop-results.png": "98a68ca050a4e66c17dcc3f1bd5f0e86bfc88c2ebe9a75284766d17e10426632",
".agent/evidence/neo-final-browser.log": "582e7cbb643201e4b197b566b021322448abaae0b383967ad0e816d0c351e96b",
".agent/evidence/neo-final-build.log": "f30138e53b8c12e70e80e2cd5f4bdfcf8d1f6a095216a4f9ade6687bca1f0488",
".agent/evidence/neo-final-lint.log": "d2354c241320f11f07890cbf32f99a5d88f1c7f4f236d860e2cd26db739badd8",
".agent/evidence/neo-final-test.log": "f718e2acda255fcba67b272c7fa625af450714b384bb67a72f09328bf6964f0b",
".agent/evidence/neo-intermediate-build.log": "9d8a1f0586d58196e03e45330b6e8ff5522fdd3c27870aae2fb052199f8cf5cb",
".agent/evidence/neo-intermediate-lint.log": "5e94ea847c3a092f4ff8e97ac5b61ebef805b3c859ad38461f4a437045d1b5b9",
".agent/evidence/neo-mobile-game.png": "1a6733a3293204e4841ec8722a1ffe37033943097c691bcfadbcd907fde024ef",
".agent/evidence/neo-mobile-heading.png": "ae07d0a1cb6f835525c7e0ef31655a543eaa9e10183596c2d409dead6faccf15",
".agent/evidence/neo-mobile-home.png": "9b3ba8b0a9f7b321882429e31ac4a0472fcc13a998dfadfc930c273bc37558d4",
".agent/evidence/neo-mobile-results.png": "d49fdf6ae9566d7a2003db3f252d84ed88ac4d73a0390ce404b47e7bab9c60df",
".agent/evidence/release-desktop-game.png": "7300a06a507f651a587ed3deb36cc5bb728f4a36c30b7b1cf409a3b209ba564c",
".agent/evidence/release-desktop-heading.png": "de454d77e29737e23c573646404489d5682004521deabfa01b439d607faa32ca",
".agent/evidence/release-desktop-home.png": "f28f744e11b66373ee8db6eda2f7bdc384da4f7e763fbb467a4cda30090e869b",
".agent/evidence/release-desktop-results.png": "2ebb4056d0ecdadea8a3469500801065ae158c17cd2db27817e813e53adc71f3",
".agent/evidence/release-docs-final.log": "3529acce5befee536ec7d8e9d22cbbf27182297bb07408ab6e31551aaee76ed3",
".agent/evidence/release-final-audit.log": "6d8c5c8f3d7684adb070417bd608d01ae90aa3dc26a65af03ffda4955f38d9a3",
".agent/evidence/release-final-browser.log": "2a9881cef8c379f41abf5d69dd5ad21881988f78c0579c95e463121dc37f6e09",
".agent/evidence/release-final-build.log": "3fba8356382341db9c562d985ca2229fd0b81969532746b0146bc489d54c321e",
".agent/evidence/release-final-lint.log": "d2354c241320f11f07890cbf32f99a5d88f1c7f4f236d860e2cd26db739badd8",
".agent/evidence/release-final-test.log": "46d5d0fc0180adc00a734b64db4acb4cbaa37d12567d0224836306312213870c",
".agent/evidence/release-format.log": "57658c36cb3161b53cdf934017e4b7a733a273509ace184e0a2a6eaa046c742b",
".agent/evidence/release-integrity.log": "cc77121b43a6b24a358999cf1a3bbf701d467fcee9730095f318303c2e80602b",
".agent/evidence/release-intermediate-lint.log": "ee597e984ebdf11ea247c8cb1551cda446580cbd7a3dc708f85fa78bb72700ad",
".agent/evidence/release-mobile-game.png": "83a918e5ff2142e540cd12b85806b3e69a71bc66df5d241f4b2f5abdaf493f82",
".agent/evidence/release-mobile-heading.png": "d5c92a3b62f749d5f064af12efa5a1a2ad13b177270ec01dd10a549a8437899d",
".agent/evidence/release-mobile-home.png": "c7fc37a247bbadba60de9dff3c8d87930a2e6ebb868f687a4135b3fd035ed1c3",
".agent/evidence/release-mobile-results.png": "587956c9ad5eca054e4bb4b44748a2c637655e407427ba5b4c6ad7bf5a74539a",
".agent/evidence-v0.1.1/00-audio-venv.log": "365f1b3cf48e7f6de9c24d28e4326a245d3a54eb329af4b41a9441ec546dabdc",
".agent/evidence-v0.1.1/00-npm-ci.log": "14a6e676c84fa9b2cc39a251aef0c593cda9903f9e5f2d9581bc4153c9f69e09",
".agent/evidence-v0.1.1/01-version-docs-red.log": "edb26b949d0dc28340989601e781df3582cf784220235d19c16a46aeee97d302",
".agent/evidence-v0.1.1/02-assets-red.log": "ccb8a354e2975c944fb2bde24492fe06a6c1b71add6ce65259d840c66836a1db",
".agent/evidence-v0.1.1/03-generator-red.log": "d78b7c9237c18ba8a87a77a116ea7561ae79af6e88718f9c087c1d8e2c70b1f1",
".agent/evidence-v0.1.1/04-generator-additional-red.log": "9a519598915fbdd98108ee1b364f634905e29a5a01bfdd7ffec22a8b359e9217",
".agent/evidence-v0.1.1/05-generator-green.log": "414464526a325b424e5c5305e3066b79c55527fb67b11a3b9eb3bb33dcecea93",
".agent/evidence-v0.1.1/06-audio-generate.log": "7b605184e89fd033378e6869236553a3d6e040087de23e8ae165f1e304f3b067",
".agent/evidence-v0.1.1/07-format.log": "ce1a102d13a7dfd78130216cea192e3230efa3612e999cb8a6bfd55e8eb6d471",
".agent/evidence-v0.1.1/08-version-docs-green.log": "671c2889a69c83ad24293d8e92d0076255bd00c5100c2a9eb764a570e1344edd",
".agent/evidence-v0.1.1/09-lint.log": "43d5238bc8e59b93cf3a9bc0f444f0165697b5584078973bd92efce056136c1e",
".agent/evidence-v0.1.1/10-audit.log": "6d8c5c8f3d7684adb070417bd608d01ae90aa3dc26a65af03ffda4955f38d9a3",
".agent/evidence-v0.1.1/11-full-test.log": "65721fcf2b03f153274c9cc1da13c6a1f265ab6f084be8b5921a898fbfd6b40b",
".agent/evidence-v0.1.1/12-independent-audio.log": "5ffd6173941d75deb028741cdba549705b652218f418c6d40ceb7f4b216f2d9d",
".agent/evidence-v0.1.1/13-build.log": "42ac41188359699f5f24c7f86de9d7ca6503d714a7be9e277ac2fd01be21614c",
".agent/evidence-v0.1.1/14-browser.log": "657ddc9f60ffa35684110766b2e459842cd1ab10a90b0002256a945f227a16b7",
".agent/evidence-v0.1.1/15-browser-audio.log": "cb16ec7009a88f911632d0bef64692066873d81372e65fea8dac800d59304214",
".agent/evidence-v0.1.1/16-final-lint.log": "43d5238bc8e59b93cf3a9bc0f444f0165697b5584078973bd92efce056136c1e",
".agent/evidence-v0.1.1/17-security-history.json": "5ba66683c3ae29e01f25cfae2ed832e33b07f866bfccd0e1d331a8f102d3d3c7",
".agent/evidence-v0.1.1/18-environment.log": "19a1da4298ba5a51f8dce5e6669205987bb48438e5c98f3ddcbc347ce0f91e46",
".agent/evidence-v0.1.1/19-final-integrity.json": "38f5af8927d95e9dae780decf146eb3d5feb610629202ef9ee67bf223647132f",
".agent/evidence-v0.1.1/audio-integrity.json": "d82b192602b1f054fce5f61479832666f237d7da2698afe84b09186dac90f321",
".agent/evidence-v0.1.1/baseline.json": "085be97acb81dcc83477ebc5f42dd2fd5729d9f5821cbc659df9c00b648ceb34",
".agent/evidence-v0.1.1/browser-audio-integrity.json": "2c0f59ed178285c0d338171328232351aab2b55e22371b3652453afa34849b65",
".agent/evidence-v0.1.1/changed-files.txt": "4877f3d1003372868b5a9f72326ccf0870b22daf35a71b60b67dc761342fa50f",
".agent/evidence-v0.1.1/desktop-game.png": "02c59fa37c4d4f19e5c74e56fc889105b4d280a0a4cf2c14c1838947859a3a8b",
".agent/evidence-v0.1.1/desktop-heading.png": "920d3d998d93f463be9ac6a682c1f7d5d9ac36944b52b715345c865ae46f566d",
".agent/evidence-v0.1.1/desktop-home.png": "9c6f92b67db249b2a1d3e752052678d7ae0af6be426d976d12237d84c105bf76",
".agent/evidence-v0.1.1/desktop-results.png": "3811eae6f5827760499c9c64053a2cb2ecbb1d87ed2ddf11b757eb44e56c64bd",
".agent/evidence-v0.1.1/mobile-game.png": "bb0dec7ec2c99b55caafd729e2f9c9c01875e3b6dd577cc377a76bbfd5c95a7e",
".agent/evidence-v0.1.1/mobile-heading.png": "d61f2d1b01c654a5cfe841e49e244af4e232b5fc9c899e77523b336411e99316",
".agent/evidence-v0.1.1/mobile-home.png": "7c4a4643a5222f45c6492ebf78780a317e26d423bf244fc16ea24a186a76f506",
".agent/evidence-v0.1.1/mobile-results.png": "f2a095ab1d2a3a95a1a275b652fcd31275a5803a926e01b5fcd1e9a9e127cfb9",
".agent/evidence-v0.1.1/verify_audio.py": "7751c03b83dbbd3b222f6d5a68eb1d78dac2bbf82c0fd79345ccca86a15fcb69",
".agent/evidence-v0.1.1/verify_browser.mjs": "53f0f6317274a91017a4f508ef9237e022f7d333ef4243f324720f8d7f85abeb",
"public/audio/correction-apple.wav": "ccd7327a3fb54d3320d049ffdced3bc17595861cee69429c0ee97182d454195b",
"public/audio/correction-furniture.wav": "5c004a7b2f5a4a918c9f689f1a2dcb219a620ed14313f40a9655b36dbf64948d",
"public/audio/correction-hat.wav": "f9f98c91cfbcfcb106eef7d5d3795a9cf3c3aab7e19a60f13d7dffe284d19f86",
"public/audio/correction-market.wav": "b8d25b867497adcd0fe7c8b6a8a8d2e7a52965956738a83e53f58735708773f9",
"public/audio/correction-meat.wav": "48e91b3932d3798e852041a854b3ee08c72127e092c62083a47af6b99adff241",
"public/audio/correction-oil.wav": "f6eff0cdddcad9790351491e60c53cb2a70d222033a583652f2de0b8cd6abcc7",
"public/audio/correction-sea.wav": "9233431d78ab4b144e3c33ad0cf0d49fc0fb91b29514343230ba32987b794bb3",
"public/audio/correction-sound.wav": "5a603bc69979603afddcaeb6edf500a7c540b72405daeb33f5335d746b327da1",
"public/audio/correction-tree.wav": "b8e8bbe6b8b14727efd953fae9d4aed2caeb004055d52f611c6378322847edbe",
"public/audio/correction-umbrella.wav": "92b54f04884ab915783945077efd8f3bd443849a1720f72168b6348cae1f8e85",
"public/audio/first-apple.wav": "7c1dbcb3082055df2b8f087f7e906ec867c92f679de0894c34740c05e61a4bc1",
"public/audio/first-furniture.wav": "29f007b385b7182db1d8aeccad4fc3c260df5e66e14e52a46b0ef0fcd0ff3a0a",
"public/audio/first-hat.wav": "17c8cde90567e7574b0c0702663bd70713054089ee920241b5444d9b7869eef5",
"public/audio/first-market.wav": "1e00b8f70a796cdebb0feb00c605fece7488b06915e6e036f54206f05e201e0c",
"public/audio/first-meat.wav": "f51a37c1722fef581f7d642a4dcae7fcb9953b3c318dce971bf616cd1a81fecc",
"public/audio/first-oil.wav": "f415195e0cfc7ee711d09de7519e88eab62cc8b58d70772c83c3f5c27c267819",
"public/audio/first-sea.wav": "0f0b39314c5f7d7168d4854003c0c284e7bc3438cdc4d1d1c5adab7cb5ed30e5",
"public/audio/first-sound.wav": "46f6adeed928a16af0700f1e8626cf8049f124632a04a5d10604426fb06f9596",
"public/audio/first-tree.wav": "3178230fd5850b6277a2153b438ec5289720e3db7ac929914cc213964f004bd5",
"public/audio/first-umbrella.wav": "878e0bd2e6e4105f175aef51a028f6b141f3a2072be20f783193f3ba897a3ee0",
"public/audio/intro.wav": "90d9f46e6e3f9c8a0c1a781096df54d7809ef4ad38b289508ef7b8c0411465ce",
"public/audio/last-apple.wav": "4cb2a8579fa3a7b91bb36363826ce8ca0d6d86e96e16a479e226a2802edd82f7",
"public/audio/last-furniture.wav": "5ad4f776e35439d408abed0e66567d6b5cdc80fc54d579165472e474f1238776",
"public/audio/last-hat.wav": "bdaf8fbc54df8061e9e58aa8c14b3044a6e863dc54973ee3ee6ae2c5ae1a28f1",
"public/audio/last-market.wav": "7857e87e9dbfe8fe849cc0b18c1d45b60916cb337328099c7a87a42c82e2821e",
"public/audio/last-meat.wav": "10f8ad4259ed8945c9da831e80b3902303c09caef96d400298599168fb1efed4",
"public/audio/last-oil.wav": "b5ce34950b8b31b1baf6ce55410797f0d8aa5478a670d6eace87cf76d134d6d3",
"public/audio/last-sea.wav": "f755452e1da44674aa1df7d12edffa988091b89a394e7eb6d4a00eb721f07cbf",
"public/audio/last-sound.wav": "ec9c49802aebeb6a8b898a1c0d8ad1daa127d2d64c905c197ed49a8e0c4eb64a",
"public/audio/last-tree.wav": "589e56d940a3c9e80ec02ae0b2dd93463fdc85ef019a753fcd0fded0752055f9",
"public/audio/last-umbrella.wav": "c801293a34013a8d93d3289a2cc87cd3eed83f97ba4647f5060efa00b117a9ad",
"public/audio/luck-apple.wav": "7c8f413f4b7e164f090e508ba090448f1376c6012649bbb6a917d45851313883",
"public/audio/luck-furniture.wav": "52cb92cdb8ee92b6f581c9d68dbc346f180c470564809ba57185429c9eb84127",
"public/audio/luck-hat.wav": "4ff43dd042f4b5451e9fdeb3edd59f1e389a7d1dd488094587a3a9fc7dfffa44",
"public/audio/luck-market.wav": "356af25332bf2000ed349671f3ff39767929e1f4599af622c021f907d11fb7bf",
"public/audio/luck-meat.wav": "d36dfc395a5c53fbfa4abe9980db7bb59adfdac59e40318c4aca365365e3f2ab",
"public/audio/luck-oil.wav": "3fa1e662a3af8f2726b17b2baa5bec2934406e6595ebc243c8380dd16e190977",
"public/audio/luck-sea.wav": "47aa31d757c9fb49c9b53a1c052d90a5ced4be57549d649850d9b48f8864045b",
"public/audio/luck-sound.wav": "34e4d9758d2ed448646fda364610e34779c84794403d9051ab3d8c1d33e2c80c",
"public/audio/luck-tree.wav": "122f496834fb16d6da47f801e50e46e272e4e934af959467d1db4e490f79be7a",
"public/audio/luck-umbrella.wav": "8d3e8e8bc97e5fe3f3362184c4abb12f01a882a1f1174d2644f0f70aac677c0a",
"public/audio/manifest.json": "ec4c6f3c86ba5083989a2da51150476a37b0a2d3152f202d1c82ed10a7aaa34b",
"public/audio/message-apple.wav": "d85e6f47d871369f00de2274139e5ad6b4b5686b5e20e248df0ed6b9a320d942",
"public/audio/message-furniture.wav": "26c331b7e12311236f4bb48f88918dfbf53cccd2f50b55741ea6ff4473e71dd8",
"public/audio/message-hat.wav": "5a713a8522b231bb8276805c014b385b9150b79f58ff2618446607e2b442495e",
"public/audio/message-market.wav": "9242d4cd6e0030e6b073e4d2360f84aa8a454d1749711cdb2d64c14f28a7ea20",
"public/audio/message-meat.wav": "ce19735df079b54c171e775b6b365385a80160b9033442abab95b2b9c8505e1a",
"public/audio/message-oil.wav": "a2a71a7e5e07c81871a25c6cb7540c39aa0b32fc44f78cda66b5f34ace98d805",
"public/audio/message-sea.wav": "28a461fbe03bffe2560030dea8b60715f7c0fe262dcf85b5b7f7daf483836382",
"public/audio/message-sound.wav": "1e682147dae8026ba78dc8f256c7f24acb3a6743bfd88a0fcc86096930eef078",
"public/audio/message-tree.wav": "5d684aa710a63d36ed8dcc2a8bfbdd5c71b0b1c63dad2a324adecb76591e85b8",
"public/audio/message-umbrella.wav": "41d5866dae66600df4694a8312b531571d44cece6dba0e423e7c43d09d184819",
"public/audio/middle-apple.wav": "8832f1b793ab3eedbe663875ea8dbeabb122c329bd5879437c71230b0418c3c8",
"public/audio/middle-furniture.wav": "b45b1afc8d9b534ff3e0a2a2ecb99efc1f4e40d41f89f55c4ed8ba94ad6d76d4",
"public/audio/middle-hat.wav": "b01ef9762ac84e3eb757243bc9286c62f7d45444a821ae35ac36a8b095e1598a",
"public/audio/middle-market.wav": "c855b449e0d4ace74681bfe68e5e7f0b063568827ae872017234dbe8ae119e4c",
"public/audio/middle-meat.wav": "d416107cc05e7c5b92ca27100a10a8a9689de913346179945d5542de4a6bac0c",
"public/audio/middle-oil.wav": "7a6b122b11c07da86c95fb78e2db2a70681b5219c63d6441fe56da1979ac1dc3",
"public/audio/middle-sea.wav": "b6dbe0af143ea775f05bd59a3d6af348bcc770ab378ac91ea75a56e06c326edf",
"public/audio/middle-sound.wav": "4d00aa6114327f450a00dacbc7e7b572ebf1de4ce9dc2fe4b5505766cca71ce9",
"public/audio/middle-tree.wav": "341888c75275c13eb31058f53a1d4276be0daa0a1b5a3a45522036928b83b0c1",
"public/audio/middle-umbrella.wav": "399ea2f681f755918f1117c8baba42739ceca64b307b999342f457a9235050b6",
"public/audio/mission-apple.wav": "3ef42e276ff3500c3e197d149f29f79d6a0c1d5516ed1d5a2c57210333fded9c",
"public/audio/mission-furniture.wav": "af82e03b686635c3356e706fe5b05c1231aeaaeab33f275aa5b8714988c65d43",
"public/audio/mission-hat.wav": "308278ec2bd27d4224976a55632d1f7fa1edabb0792126d9d858a699c287aee4",
"public/audio/mission-market.wav": "cefa0e95649ea6bd1df4c8dfccd2b74b4f4c028714420ce1087b61db2f2a8d47",
"public/audio/mission-meat.wav": "94f8a4f9e116a787e5ed052661d6de9ecf3ad8629db476661dd59ad03014b0c9",
"public/audio/mission-oil.wav": "5fb63e04520c312aa6b7b48b70c2f7c140b5ab480e5c6992edbc2ba85b9a06e7",
"public/audio/mission-sea.wav": "8838d6a1ee8a790427dc5aa761e70b3d8872af62086efeed743297ff7f8b8a06",
"public/audio/mission-sound.wav": "659eb0e2b06e6b165b46744769743e032e2282519f8486c57bc45e0f7d07a069",
"public/audio/mission-tree.wav": "00d8e77ec0371971d632724d91f8cf8d947bfb83c582a513b6421be4c457a1da",
"public/audio/mission-umbrella.wav": "4b4c858a25659540f9dbc524664496fc6de61ab8c1627f3cfee8c9c744e11d5b",
"public/audio/password-apple.wav": "b6f5de3fe571fe3a7fdb722fe49250bcfeedef49475271e0ec76a7630907edd5",
"public/audio/password-furniture.wav": "fb2b631dd4e12120d08a84cce6450f9cd9cd68105d13d7001d808e7deaa1df4a",
"public/audio/password-hat.wav": "e91bfc97df90a1675b6cd856880ac15c54de32cfc3b23603bdd1c2c5ac3ce6b8",
"public/audio/password-market.wav": "02e325fb753ef7a3ee585a03d2ca2aa3f1fbc58523b062222ae1136eeed7a95c",
"public/audio/password-meat.wav": "b009caadd56350d6a641ff17f0973e2b2724c183e1f45ae0bf3c483e237fa99e",
"public/audio/password-oil.wav": "b472957b27ff98ea7b8894fa1ff12653f7e9f66f77870faa9e52a1c1bf116c41",
"public/audio/password-sea.wav": "03f45b8cb5b3dc72faf5c6e3d0e2a10cea6b60301ea400d0a4453596d2f0b337",
"public/audio/password-sound.wav": "044aafff08a19e39f21e58b963644715a987cd155896998769ef8e9ce6449628",
"public/audio/password-tree.wav": "67da8147296e35134ec254bc82b58317630061b2c64d2d63c4be197ce8692d50",
"public/audio/password-umbrella.wav": "ab3ca6398c879971dc793eb25b472fcc0db72d76e386f11ef4d32c639f37c69d",
"public/audio/remember-apple.wav": "1bf2aef9a9f030af1a8fd7dee8f44aa420a397b3ba99b575feeb3dc7a63ec191",
"public/audio/remember-furniture.wav": "442bdcb956f120c270e1f321feae0415c897de0183e468980fcc119b898b2a67",
"public/audio/remember-hat.wav": "31a6a4270bf368cda419094d87a303ac3cd5c22539786cb165f8044b0b8e9d6e",
"public/audio/remember-market.wav": "d89792da51c83ac49d27e55aa6d822a338899a5ee487b34b552748201e1522f6",
"public/audio/remember-meat.wav": "9183ef90b64f5ceaf3bb0178c689231ff3e9981d6ce889124a15f2a6e2b7a344",
"public/audio/remember-oil.wav": "3ca98682ff06d1d6b8af02c0fd5b2124f42266ac989884223eff4a9bbde1287a",
"public/audio/remember-sea.wav": "5c91a36496e274d2be9c6fa8d9b506cd04f2a1b7ceb05ed1b7bb03fe7fca99d9",
"public/audio/remember-sound.wav": "98387ccc7aab566f6487d4c250b2128082cc527e013a6f7b7bc4d0266ac295df",
"public/audio/remember-tree.wav": "bc8e54f5407e8866bf1ca1ddfd4f6c63d85fee29bd9e430c7dd33e87974fc5b8",
"public/audio/remember-umbrella.wav": "c8988e604ba05d52597e561389541e8f9cda86e052279f1862bbf8399ead314e",
"public/audio/repeat-apple.wav": "cb7da4e9a1288cac64d168b241f27d7d2bc2dfecace58744f9bbf2792dfad04e",
"public/audio/repeat-furniture.wav": "bf84dc6cff1f227924a3a62158d2fc18769fb0ca40b91418fa19e534dbf92f00",
"public/audio/repeat-hat.wav": "6e089a42fd7e65e82eec7186d369c926fb7fdac794365b2bd7658822fd92437c",
"public/audio/repeat-market.wav": "68a5b899115a66ab225d610ec47b4de56cc8793657362bc38f533c06f741e4cc",
"public/audio/repeat-meat.wav": "6599d2fac73ea616a793151b4ce6f51af673988d630bac6219a77e78ae6cb34e",
"public/audio/repeat-oil.wav": "55b2c7d6aa8bbce04edf8661882c9781da81c57f56217f99b80457a3bb895da0",
"public/audio/repeat-sea.wav": "db2f235bee26ac804b5362e8e013b8f40c8da24fd3a34e0fb3b43c6d264feb36",
"public/audio/repeat-sound.wav": "e9d636ff7f4777f38b1ff15bebdcec6fbde95b4b4b05625a7415b4cfa0025b59",
"public/audio/repeat-tree.wav": "8735c3d9672561ca237fa87317beec2878492eb9063d6048d5b214423561acc1",
"public/audio/repeat-umbrella.wav": "5e59ef39113c3d5e106862356d715f4cef2862ef8ee02654a97e532675c1ac6d",
".agent/RESULT.md": "eef6d73a67a8a7e90cd7b4747debc5976cc51571bc268328634413f698d0e311",
"package.json": "41ba1d6c1e9f6350cf3bbc627b921839d67f7f9a20809d082f0c112ce7dbf631",
"package-lock.json": "9c64fc6a305196b84e39ac52405210b926c1431055724d82b41eafd21d542e5c"
}
}
@@ -0,0 +1,235 @@
UTC: 2026-10-14T07:20:59.789143+00:00
COMMAND: npm test -- tests/audio.test.ts tests/game.test.ts tests/audio-requests.test.ts
> find-word@0.1.1 test
> vitest run tests/audio.test.ts tests/game.test.ts tests/audio-requests.test.ts
RUN v5.0.4 /home/hermes/worktrees/find-word-v0.1.1
❯ tests/audio-requests.test.ts (1 test | 1 failed) 28ms
× WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다 26ms
❯ tests/game.test.ts (8 tests | 1 failed) 92ms
❯ 출제와 정답 판정 (5)
× 목표 WAV 100개 모두 같은 명시적 cache-busting 버전과 정답 mapping을 유지한다 50ms
❯ tests/audio.test.ts (8 tests | 3 failed) 357ms
❯ 오디오 재생 수명과 믹서 (8)
× intro와 목표 실제 fetch URL에 같은 명시적 cache-busting 버전을 붙인다 101ms
× intro 404 후 intro·목표를 동일한 versioned URL로 다시 요청한다 47ms
× target 404 후 intro·목표를 동일한 versioned URL로 다시 요청한다 47ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 5 ⎯⎯⎯⎯⎯⎯⎯
FAIL tests/audio-requests.test.ts > WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다
AssertionError: /audio/intro.wav?v=0.1.1: expected false to be true // Object.is equality
- Expected
+ Received
- true
+ false
❯ tests/audio-requests.test.ts:12:74
10| "/audio/luck-apple.wav",
11| ]) {
12| expect.soft(isAudioRequest(new URL(path, "http://localhost")), pat…
| ^
13| }
14| for (const path of [
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/11]⎯
FAIL tests/audio-requests.test.ts > WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다
AssertionError: /audio/luck-apple.wav?v=0.1.1: expected false to be true // Object.is equality
- Expected
+ Received
- true
+ false
❯ tests/audio-requests.test.ts:12:74
10| "/audio/luck-apple.wav",
11| ]) {
12| expect.soft(isAudioRequest(new URL(path, "http://localhost")), pat…
| ^
13| }
14| for (const path of [
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/11]⎯
FAIL tests/audio-requests.test.ts > WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다
AssertionError: /audio/luck-apple.wav?retry=1&v=0.1.1: expected false to be true // Object.is equality
- Expected
+ Received
- true
+ false
❯ tests/audio-requests.test.ts:12:74
10| "/audio/luck-apple.wav",
11| ]) {
12| expect.soft(isAudioRequest(new URL(path, "http://localhost")), pat…
| ^
13| }
14| for (const path of [
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/11]⎯
FAIL tests/audio-requests.test.ts > WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다
AssertionError: /audio/luck-apple.wav?v=0.1.1&retry=1: expected false to be true // Object.is equality
- Expected
+ Received
- true
+ false
❯ tests/audio-requests.test.ts:12:74
10| "/audio/luck-apple.wav",
11| ]) {
12| expect.soft(isAudioRequest(new URL(path, "http://localhost")), pat…
| ^
13| }
14| for (const path of [
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/11]⎯
FAIL tests/audio-requests.test.ts > WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다
AssertionError: /audio/manifest.json?file=intro.wav: expected true to be false // Object.is equality
- Expected
+ Received
- false
+ true
❯ tests/audio-requests.test.ts:20:74
18| "/audio/intro.wav.txt?v=0.1.1",
19| ]) {
20| expect.soft(isAudioRequest(new URL(path, "http://localhost")), pat…
| ^
21| }
22| });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/11]⎯
FAIL tests/audio-requests.test.ts > WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다
AssertionError: /other/intro.wav: expected true to be false // Object.is equality
- Expected
+ Received
- false
+ true
❯ tests/audio-requests.test.ts:20:74
18| "/audio/intro.wav.txt?v=0.1.1",
19| ]) {
20| expect.soft(isAudioRequest(new URL(path, "http://localhost")), pat…
| ^
21| }
22| });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/11]⎯
FAIL tests/audio-requests.test.ts > WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다
AssertionError: /audio-backup/intro.wav: expected true to be false // Object.is equality
- Expected
+ Received
- false
+ true
❯ tests/audio-requests.test.ts:20:74
18| "/audio/intro.wav.txt?v=0.1.1",
19| ]) {
20| expect.soft(isAudioRequest(new URL(path, "http://localhost")), pat…
| ^
21| }
22| });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/11]⎯
FAIL tests/audio.test.ts > 오디오 재생 수명과 믹서 > intro와 목표 실제 fetch URL에 같은 명시적 cache-busting 버전을 붙인다
AssertionError: expected [ '/audio/intro.wav', …(1) ] to deeply equal [ '/audio/intro.wav?v=0.1.1', …(1) ]
- Expected
+ Received
[
- "/audio/intro.wav?v=0.1.1",
- "/audio/correction-market.wav?v=0.1.1",
+ "/audio/intro.wav",
+ "/audio/correction-market.wav",
]
❯ tests/audio.test.ts:97:54
95| f.sources.at(-1)!.onended?.();
96| await done;
97| expect(f.fetcher.mock.calls.map(([url]) => url)).toEqual([
| ^
98| "/audio/intro.wav?v=0.1.1",
99| `/audio/${question.id}.wav?v=0.1.1`,
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[8/11]⎯
FAIL tests/audio.test.ts > 오디오 재생 수명과 믹서 > intro 404 후 intro·목표를 동일한 versioned URL로 다시 요청한다
FAIL tests/audio.test.ts > 오디오 재생 수명과 믹서 > target 404 후 intro·목표를 동일한 versioned URL로 다시 요청한다
AssertionError: expected [ '/audio/intro.wav', …(3) ] to deeply equal [ '/audio/intro.wav?v=0.1.1', …(3) ]
- Expected
+ Received
[
- "/audio/intro.wav?v=0.1.1",
- "/audio/correction-market.wav?v=0.1.1",
- "/audio/intro.wav?v=0.1.1",
- "/audio/correction-market.wav?v=0.1.1",
+ "/audio/intro.wav",
+ "/audio/correction-market.wav",
+ "/audio/intro.wav",
+ "/audio/correction-market.wav",
]
❯ tests/audio.test.ts:119:54
117| f.sources.at(-1)!.onended?.();
118| await done;
119| expect(f.fetcher.mock.calls.map(([url]) => url)).toEqual([
| ^
120| "/audio/intro.wav?v=0.1.1",
121| `/audio/${question.id}.wav?v=0.1.1`,
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[9/11]⎯
FAIL tests/game.test.ts > 출제와 정답 판정 > 목표 WAV 100개 모두 같은 명시적 cache-busting 버전과 정답 mapping을 유지한다
AssertionError: expected Set{ null } to deeply equal Set{ '0.1.1' }
- Expected
+ Received
Set {
- "0.1.1",
+ null,
}
❯ tests/game.test.ts:30:22
28| versions.add(url.searchParams.get("v"));
29| }
30| expect(versions).toEqual(new Set(["0.1.1"]));
| ^
31| });
32| it("실제 보기에 있는 경쟁 단어를 문장에 넣고 청취 연산별 순서를 유지한다", () => {
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[10/11]⎯
Test Files 3 failed (3)
Tests 5 failed | 12 passed (17)
Start at 07:21:00
Duration 1.91s (environment 66%, setup 13%, tests 11%, transform 6%, worker 2%, import 2%)
EXIT_CODE: 1
@@ -0,0 +1,19 @@
UTC: 2026-10-14T07:21:00.984991+00:00
COMMAND: npm run build
> find-word@0.1.1 build
> tsc --noEmit && vite build && node scripts/check-build.mjs
vite v8.3.4 building client environment for production...
transforming...
✓ 20 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.89 kB │ gzip: 0.63 kB
dist/assets/index-KT6VDfPn.css 15.25 kB │ gzip: 4.39 kB
dist/assets/index-CorsjeLi.js 245.34 kB │ gzip: 77.26 kB
✓ built in 177ms
빌드 음성 자산 101개 및 런타임 notice 전문 확인
EXIT_CODE: 0
@@ -0,0 +1,148 @@
UTC: 2026-10-14T07:21:27.811844+00:00
COMMAND: env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome npm run test:browser -- tests/browser/game.spec.ts tests/browser/audio.spec.ts --project=desktop
> find-word@0.1.1 test:browser
> playwright test tests/browser/game.spec.ts tests/browser/audio.spec.ts --project=desktop
[WebServer] (node:796454) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)
Running 4 tests using 2 workers
(node:796474) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
(Use `node --trace-warnings ...` to show where the warning was created)
(node:796475) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
(Use `node --trace-warnings ...` to show where the warning was created)
✘ 1 [desktop] › tests/browser/audio.spec.ts:7:1 › intro·목표 101개 versioned WAV를 실제 Chrome에서 해시·디코드 검증한다 (1.8s)
(node:796732) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
(Use `node --trace-warnings ...` to show where the warning was created)
✘ 2 [desktop] › tests/browser/game.spec.ts:8:1 › 실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음 (3.0s)
✘ 3 [desktop] › tests/browser/game.spec.ts:124:3 › intro WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다 (805ms)
(node:796861) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
(Use `node --trace-warnings ...` to show where the warning was created)
✘ 4 [desktop] › tests/browser/game.spec.ts:124:3 › target WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다 (666ms)
1) [desktop] › tests/browser/audio.spec.ts:7:1 › intro·목표 101개 versioned WAV를 실제 Chrome에서 해시·디코드 검증한다
Error: expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 1
Set {
- "0.1.1",
+ null,
}
30 | expect(targetUrls).toHaveLength(100);
31 | const urls = [introUrl, ...targetUrls.map((url) => new URL(url, page.url()).href)];
> 32 | expect(new Set(urls.map((url) => new URL(url).searchParams.get("v")))).toEqual(new Set(["0.1.1"]));
| ^
33 | expect(urls.every((url) => new URL(url).origin === new URL(page.url()).origin)).toBe(true);
34 | const manifest = JSON.parse(readFileSync("public/audio/manifest.json", "utf8")) as {
35 | version: string;
at /home/hermes/worktrees/find-word-v0.1.1/tests/browser/audio.spec.ts:32:74
Error Context: test-results/audio-intro·목표-101개-versioned-WAV를-실제-Chrome에서-해시·디코드-검증한다-desktop/error-context.md
attachment #2: trace (application/zip) ─────────────────────────────────────────────────────────
test-results/audio-intro·목표-101개-versioned-WAV를-실제-Chrome에서-해시·디코드-검증한다-desktop/trace.zip
Usage:
npx playwright show-trace test-results/audio-intro·목표-101개-versioned-WAV를-실제-Chrome에서-해시·디코드-검증한다-desktop/trace.zip
────────────────────────────────────────────────────────────────────────────────────────────────
2) [desktop] › tests/browser/game.spec.ts:8:1 › 실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음 ───────────────
Error: expect(received).toEqual(expected) // deep equality
- Expected - 2
+ Received + 2
Array [
- "0.1.1",
- "0.1.1",
+ null,
+ null,
]
42 | await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
43 | await expect.poll(() => audioUrls.length).toBe(2);
> 44 | expect(audioUrls.map((url) => url.searchParams.get("v"))).toEqual(["0.1.1", "0.1.1"]);
| ^
45 | await expect(page.getByRole("button", { name: /듣는 중/ })).toBeDisabled();
46 | await expect(page.locator(".sound-wave.active i").first()).toHaveCSS(
47 | "animation-name",
at /home/hermes/worktrees/find-word-v0.1.1/tests/browser/game.spec.ts:44:61
Error Context: test-results/game-실제-오디오로-혼합-모드-완주·레이아웃·접근성·외부-요청-없음-desktop/error-context.md
attachment #2: trace (application/zip) ─────────────────────────────────────────────────────────
test-results/game-실제-오디오로-혼합-모드-완주·레이아웃·접근성·외부-요청-없음-desktop/trace.zip
Usage:
npx playwright show-trace test-results/game-실제-오디오로-혼합-모드-완주·레이아웃·접근성·외부-요청-없음-desktop/trace.zip
────────────────────────────────────────────────────────────────────────────────────────────────
3) [desktop] › tests/browser/game.spec.ts:124:3 › intro WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다
Error: expect(received).toBe(expected) // Object.is equality
Expected: "0.1.1"
Received: null
138 | await expect(page.getByRole("alert")).toContainText("소리를 재생하지 못했어요");
139 | expect(missing).toHaveLength(1);
> 140 | expect(new URL(missing[0]!).searchParams.get("v")).toBe("0.1.1");
| ^
141 | expect(audioUrls).toHaveLength(2);
142 | await page.unroute(matchesFailure);
143 | await page.getByRole("button", { name: "다시 시도" }).click();
at /home/hermes/worktrees/find-word-v0.1.1/tests/browser/game.spec.ts:140:56
Error Context: test-results/game-intro-WAV-query-URL의-404를-표시하고-같은-URL로-재시도·정답-mapping한다-desktop/error-context.md
attachment #2: trace (application/zip) ─────────────────────────────────────────────────────────
test-results/game-intro-WAV-query-URL의-404를-표시하고-같은-URL로-재시도·정답-mapping한다-desktop/trace.zip
Usage:
npx playwright show-trace test-results/game-intro-WAV-query-URL의-404를-표시하고-같은-URL로-재시도·정답-mapping한다-desktop/trace.zip
────────────────────────────────────────────────────────────────────────────────────────────────
4) [desktop] › tests/browser/game.spec.ts:124:3 › target WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다
Error: expect(received).toBe(expected) // Object.is equality
Expected: "0.1.1"
Received: null
138 | await expect(page.getByRole("alert")).toContainText("소리를 재생하지 못했어요");
139 | expect(missing).toHaveLength(1);
> 140 | expect(new URL(missing[0]!).searchParams.get("v")).toBe("0.1.1");
| ^
141 | expect(audioUrls).toHaveLength(2);
142 | await page.unroute(matchesFailure);
143 | await page.getByRole("button", { name: "다시 시도" }).click();
at /home/hermes/worktrees/find-word-v0.1.1/tests/browser/game.spec.ts:140:56
Error Context: test-results/game-target-WAV-query-URL의-8e3ab-하고-같은-URL로-재시도·정답-mapping한다-desktop/error-context.md
attachment #2: trace (application/zip) ─────────────────────────────────────────────────────────
test-results/game-target-WAV-query-URL의-8e3ab-하고-같은-URL로-재시도·정답-mapping한다-desktop/trace.zip
Usage:
npx playwright show-trace test-results/game-target-WAV-query-URL의-8e3ab-하고-같은-URL로-재시도·정답-mapping한다-desktop/trace.zip
────────────────────────────────────────────────────────────────────────────────────────────────
4 failed
[desktop] › tests/browser/audio.spec.ts:7:1 › intro·목표 101개 versioned WAV를 실제 Chrome에서 해시·디코드 검증한다
[desktop] › tests/browser/game.spec.ts:8:1 › 실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음 ────────────────
[desktop] › tests/browser/game.spec.ts:124:3 › intro WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다
[desktop] › tests/browser/game.spec.ts:124:3 › target WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다
EXIT_CODE: 1
@@ -0,0 +1,22 @@
{
"utc": "2026-10-14T07:22:25.563434+00:00",
"productionSourceDiff": "",
"unitRed": {
"failed": 5,
"passed": 12,
"exitCode": 1
},
"browserRed": {
"failed": 4,
"exitCode": 1
},
"sources": {
"src/App.tsx": "1ebde5bd2aa8038e5e003e4b9c84071bd9571dc2deff334b848720761d43df19",
"src/audio/dsp.ts": "e51176acc381b7e9d1fc19d7f3fdeb87f6b653dad13a332470104e957e973ecc",
"src/audio/engine.ts": "2d2272c8690d1b791fb51e111e1408e1a9688f8b6abb49f7ccf7bef5e8091543",
"src/content.ts": "f7363aaa6dd99f56e733843c569e96018a9c570708bea25857af59548f45e2c5",
"src/game.ts": "7aac9bccb6c322288d22966ee672da17d6745205260c8e87e009a3704ea8bd89",
"src/main.tsx": "eeab7184c76db4d03f45af69697f19affaaf318de056e36c8782000807d5e88e",
"src/styles.css": "09a32cc57163e1f517075525d98ffcf783b18e872135163f7572ac60ace25b9f"
}
}
@@ -0,0 +1,17 @@
UTC: 2026-10-14T07:22:34.296917+00:00
COMMAND: npm test -- tests/audio.test.ts tests/game.test.ts tests/audio-requests.test.ts
> find-word@0.1.1 test
> vitest run tests/audio.test.ts tests/game.test.ts tests/audio-requests.test.ts
RUN v5.0.4 /home/hermes/worktrees/find-word-v0.1.1
Test Files 3 passed (3)
Tests 17 passed (17)
Start at 07:22:34
Duration 1.57s (environment 72%, setup 13%, tests 7%, transform 6%, worker 1%, import 1%)
EXIT_CODE: 0
@@ -0,0 +1,8 @@
UTC: 2026-10-14T07:23:10.118081+00:00
COMMAND: npm run lint
> find-word@0.1.1 lint
> eslint . && tsc --noEmit
EXIT_CODE: 0
@@ -0,0 +1,20 @@
UTC: 2026-10-14T07:23:10.132588+00:00
COMMAND: npm test
> find-word@0.1.1 test
> vitest run
RUN v5.0.4 /home/hermes/worktrees/find-word-v0.1.1
Test Files 12 passed (12)
Tests 65 passed (65)
Start at 07:23:10
Duration 14.73s (tests 71%, environment 20%, setup 7%, import 1%)
Isolate 12 workers spawned · ~883ms startup each (spawn + environment, per file)
at least ~2.65s faster with isolate: false — reuses workers across files instead of one per file
EXIT_CODE: 0
@@ -0,0 +1,19 @@
UTC: 2026-10-14T07:23:10.144409+00:00
COMMAND: npm run build
> find-word@0.1.1 build
> tsc --noEmit && vite build && node scripts/check-build.mjs
vite v8.3.4 building client environment for production...
transforming...
✓ 21 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.89 kB │ gzip: 0.63 kB
dist/assets/index-KT6VDfPn.css 15.25 kB │ gzip: 4.39 kB
dist/assets/index-CNSv_hb8.js 245.39 kB │ gzip: 77.28 kB
✓ built in 654ms
빌드 음성 자산 101개 및 런타임 notice 전문 확인
EXIT_CODE: 0
@@ -0,0 +1,84 @@
UTC: 2026-10-14T07:23:35.351749+00:00
COMMAND: env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome DEBUG=pw:browser npm run test:browser
> find-word@0.1.1 test:browser
> playwright test
[WebServer] (node:797635) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)
Running 16 tests using 2 workers
(node:797656) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
(Use `node --trace-warnings ...` to show where the warning was created)
(node:797657) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
(Use `node --trace-warnings ...` to show where the warning was created)
pw:browser <launching> /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-VLndm1 --remote-debugging-pipe --no-startup-window +0ms
pw:browser <launched> pid=797676 +6ms
pw:browser <launching> /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-E3SrLp --remote-debugging-pipe --no-startup-window +0ms
pw:browser <launched> pid=797720 +11ms
pw:browser [pid=797676][err] [797729:797729:1014/072337.395281:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +151ms
pw:browser [pid=797720][err] [797806:797806:1014/072337.559739:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +228ms
pw:browser [pid=797676][err] [797822:7:1014/072337.636786:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:287] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +236ms
pw:browser [pid=797676][err] [797884:797884:1014/072338.237452:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +600ms
pw:browser [pid=797720][err] [797908:797908:1014/072339.957896:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +2s
✓ 1 [desktop] › tests/browser/audio.spec.ts:7:1 › intro·목표 101개 versioned WAV를 실제 Chrome에서 해시·디코드 검증한다 (3.5s)
pw:browser [pid=797720][err] [797720:797786:1014/072343.442681:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: QUOTA_EXCEEDED +3s
pw:browser [pid=797676][err] [797676:797709:1014/072344.514117:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +6s
✓ 3 [desktop] › tests/browser/game.spec.ts:130:3 › intro WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다 (15.7s)
✓ 4 [desktop] › tests/browser/game.spec.ts:130:3 › target WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다 (13.7s)
✓ 5 [desktop] › tests/browser/release.spec.ts:6:1 › 릴리스는 사용자 크레딧과 런타임 MIT notice 전문을 제공한다 (312ms)
✓ 6 [desktop] › tests/browser/ui.spec.ts:4:1 › 전체 음량은 설정 흐름 안에 들어가며 키보드로 조절된다 (1.2s)
✓ 7 [desktop] › tests/browser/ui.spec.ts:52:1 › 난이도 설명은 모바일에서도 보인다 (852ms)
pw:browser [pid=797676][err] [797676:797709:1014/072414.236741:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +30s
✓ 8 [desktop] › tests/browser/ui.spec.ts:68:1 › 한국어 제목은 단어 중간에서 줄바꿈하지 않는다 (16.7s)
pw:browser [pid=797676] <gracefully close start> +15s
pw:browser [pid=797676] <process did exit: exitCode=0, signal=null> +63ms
pw:browser [pid=797676] starting temporary directories cleanup +1ms
pw:browser [pid=797676] finished temporary directories cleanup +8ms
pw:browser [pid=797676] <gracefully close end> +0ms
(node:798432) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
(Use `node --trace-warnings ...` to show where the warning was created)
pw:browser <launching> /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-98ggzR --remote-debugging-pipe --no-startup-window +0ms
pw:browser <launched> pid=798444 +6ms
pw:browser [pid=798444][err] [798487:798487:1014/072430.332265:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +129ms
pw:browser [pid=798444][err] [798525:7:1014/072430.474467:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:287] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +141ms
pw:browser [pid=798444][err] [798546:798546:1014/072430.822117:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +347ms
✓ 9 [mobile] › tests/browser/audio.spec.ts:7:1 › intro·목표 101개 versioned WAV를 실제 Chrome에서 해시·디코드 검증한다 (2.6s)
pw:browser [pid=798444][err] [798444:798472:1014/072437.625010:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: QUOTA_EXCEEDED +7s
✓ 2 [desktop] › tests/browser/game.spec.ts:8:1 › 실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음 (1.7m)
pw:browser [pid=797720] <gracefully close start> +2m
pw:browser [pid=797720] <process did exit: exitCode=0, signal=null> +58ms
pw:browser [pid=797720] starting temporary directories cleanup +0ms
pw:browser [pid=797720] finished temporary directories cleanup +9ms
pw:browser [pid=797720] <gracefully close end> +0ms
(node:798715) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
(Use `node --trace-warnings ...` to show where the warning was created)
pw:browser <launching> /usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-n2iw8E --remote-debugging-pipe --no-startup-window +0ms
pw:browser <launched> pid=798727 +6ms
pw:browser [pid=798727][err] [798770:798770:1014/072519.143522:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +114ms
pw:browser [pid=798727][err] [798809:7:1014/072519.291872:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:287] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +146ms
pw:browser [pid=798727][err] [798831:798831:1014/072519.654215:ERROR:base/memory/shared_memory_switch.cc:289] Failed global descriptor lookup: 7 +362ms
pw:browser [pid=798727][err] [798727:798755:1014/072523.099951:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +3s
✓ 11 [mobile] › tests/browser/game.spec.ts:130:3 › intro WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다 (17.1s)
pw:browser [pid=798727][err] [798727:798755:1014/072550.990604:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +28s
✓ 12 [mobile] › tests/browser/game.spec.ts:130:3 › target WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다 (15.9s)
✓ 13 [mobile] › tests/browser/release.spec.ts:6:1 › 릴리스는 사용자 크레딧과 런타임 MIT notice 전문을 제공한다 (308ms)
✓ 14 [mobile] › tests/browser/ui.spec.ts:4:1 › 전체 음량은 설정 흐름 안에 들어가며 키보드로 조절된다 (976ms)
✓ 15 [mobile] › tests/browser/ui.spec.ts:52:1 › 난이도 설명은 모바일에서도 보인다 (772ms)
✓ 16 [mobile] › tests/browser/ui.spec.ts:68:1 › 한국어 제목은 단어 중간에서 줄바꿈하지 않는다 (16.8s)
pw:browser [pid=798727] <gracefully close start> +20s
pw:browser [pid=798727] <process did exit: exitCode=0, signal=null> +45ms
pw:browser [pid=798727] starting temporary directories cleanup +0ms
pw:browser [pid=798727] finished temporary directories cleanup +8ms
pw:browser [pid=798727] <gracefully close end> +0ms
✓ 10 [mobile] › tests/browser/game.spec.ts:8:1 › 실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부 요청 없음 (1.7m)
pw:browser [pid=798444] <gracefully close start> +2m
pw:browser [pid=798444] <process did exit: exitCode=0, signal=null> +57ms
pw:browser [pid=798444] starting temporary directories cleanup +0ms
pw:browser [pid=798444] finished temporary directories cleanup +10ms
pw:browser [pid=798444] <gracefully close end> +0ms
16 passed (2.6m)
EXIT_CODE: 0
@@ -0,0 +1,40 @@
UTC: 2026-10-14T07:23:54.730130+00:00
COMMAND: env -u PLAYWRIGHT_NO_SANDBOX node .cache/verify-cache-sandbox.mjs
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /Namespace sandbox\s+Yes/. Input:
'Sandbox Status\n' +
'Layer 1 Sandbox\tNamespace\n' +
'PID namespaces\tYes\n' +
'Network namespaces\tYes\n' +
'Seccomp-BPF sandbox\tYes\n' +
'Seccomp-BPF sandbox supports TSYNC\tYes\n' +
'Ptrace Protection with Yama LSM (Broker)\tYes\n' +
'Ptrace Protection with Yama LSM (Non-broker)\tNo\n' +
'\n' +
'You are adequately sandboxed.'
at file:///home/hermes/worktrees/find-word-v0.1.1/.cache/verify-cache-sandbox.mjs:8:10 {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'Sandbox Status\n' +
'Layer 1 Sandbox\tNamespace\n' +
'PID namespaces\tYes\n' +
'Network namespaces\tYes\n' +
'Seccomp-BPF sandbox\tYes\n' +
'Seccomp-BPF sandbox supports TSYNC\tYes\n' +
'Ptrace Protection with Yama LSM (Broker)\tYes\n' +
'Ptrace Protection with Yama LSM (Non-broker)\tNo\n' +
'\n' +
'You are adequately sandboxed.',
expected: /Namespace sandbox\s+Yes/,
operator: 'match',
diff: 'simple'
}
Node.js v26.7.0
EXIT_CODE: 1
@@ -0,0 +1,10 @@
UTC: 2026-10-14T07:24:19.461259+00:00
COMMAND: env -u PLAYWRIGHT_NO_SANDBOX node .cache/verify-cache-sandbox.mjs
{
"node": "v26.7.0",
"chrome": "146.0.7680.80",
"sandboxStatus": "Sandbox Status\nLayer 1 Sandbox\tNamespace\nPID namespaces\tYes\nNetwork namespaces\tYes\nSeccomp-BPF sandbox\tYes\nSeccomp-BPF sandbox supports TSYNC\tYes\nPtrace Protection with Yama LSM (Broker)\tYes\nPtrace Protection with Yama LSM (Non-broker)\tNo\n\nYou are adequately sandboxed.",
"commandLine": "/usr/bin/google-chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,BlockOriginHeaderModificationOnRedirect,Translate,AutoDeElevate,OptimizationHints,NetworkTimeServiceQuerying,AimEnabled,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --user-data-dir=/tmp/playwright_chromiumdev_profile-1fbWzM --remote-debugging-pipe --no-startup-window --noerrdialogs --ozone-platform=headless --ozone-override-screen-size=800,600 --use-angle=swiftshader-webgl --flag-switches-begin --flag-switches-end --enable-unsafe-swiftshader"
}
EXIT_CODE: 0
@@ -0,0 +1,55 @@
2026-10-14 UTC — 배포 후 오디오 캐시 회귀 수정
발견 원인
- v0.1 → v0.1.1에서 intro 1개·목표 100개 WAV 내용이 바뀌었지만 파일명은 동일하다.
- src/game.ts와 src/audio/engine.ts가 버전 없는 /audio/*.wav URL을 요청했다.
- 사용자 보고에 따르면 배포 Nginx는 /audio를 7일 캐시한다. 기존 URL의 캐시가
유효한 사용자는 이전 음성을 계속 받는다. 이 작업은 원격 서버에 접속하지 않았다.
- 배포 문서는 expires -1을 가정했고, 브라우저 요청 관찰의 URL.endsWith('.wav')와
404 route의 **/audio/*.wav glob도 query string을 처리하지 못했다.
실제 RED → GREEN 순서
1. 앱 소스를 그대로 두고 unit/browser 계약 테스트를 먼저 작성했다.
기존 endsWith 판정을 공용 테스트 helper로 추출했으며 RED까지 동작은 그대로다.
2. 01-unit-red.log: 5 failed / 12 passed / exit 1.
목표 100개 버전, intro·target fetch, intro/target 각각 404 retry,
query 순서와 무관한 pathname 판정 및 가짜 .wav query 배제를 검출했다.
3. 02-build-before-fix.log: 수정 전 앱 production build 성공.
4. 03-browser-red.log: sandboxed Chrome desktop 4 failed / exit 1.
101개 URL, 실제 앱 요청/정답 mapping, intro/target 404 계약이 모두
v=null 대신 v=0.1.1을 기대하는 assertion에서 실패했다.
5. 04-red-state.json: 위 RED 완료 시점의 production source diff가 비어 있음을 기록.
6. 그 뒤 src/audio/assets.ts 한 곳에 AUDIO_ASSET_VERSION="0.1.1"과
audioAssetUrl을 정의하고 intro·target에 함께 적용했다.
테스트 요청 관찰·404 route·정답 mapping·101개 manifest 대응은 pathname 기준이다.
7. 05-unit-green.log: 같은 대상 17 passed / exit 0.
8. 09-browser-green.log: desktop/mobile 전체 16 passed (2.6m) / exit 0.
최종 검증
- npm run lint: PASS (06-lint.log, ESLint + TypeScript).
- npm test: PASS, 12 files / 65 tests, skip 0 (07-full-test.log).
Python 생성기 18개 계약도 기존 npm test runner를 통해 통과했다.
- npm run build: PASS, WAV 101개 hash 및 runtime notice 전문 (08-build.log).
- env -u PLAYWRIGHT_NO_SANDBOX CHROME_PATH=/usr/bin/google-chrome DEBUG=pw:browser
npm run test:browser: PASS, desktop/mobile 전체 16개, skip 0.
- 두 viewport 모두 실제 음성으로 10문제 완주·재청취·정답 mapping·intro/target
각각 404 복구·키보드·레이아웃·axe를 통과했다.
- 두 viewport 각각 101/101 query URL 요청·SHA-256·bytes·24kHz mono decode·
duration·RMS·peak 통과. 외부 페이지 요청 0, 페이지 JS 예외 0.
- Node 26.7.0 / npm 11.19.0 / system Chrome 146.0.7680.80.
- 11-sandbox-green.log: Layer 1 Namespace, PID/Network namespaces,
Seccomp-BPF/TSYNC 활성. 실제 실행 인자 --no-sandbox 없음.
10-sandbox.log는 보조 검사에서 Chrome 상태 라벨을 'Namespace sandbox Yes'로
잘못 가정한 실패 기록이다. 실제 표시 'Layer 1 Sandbox Namespace'와
PID/Network Yes를 검사하도록 보조 probe를 고친 뒤 11번에서 통과했다.
앱·Playwright sandbox 설정은 바꾸지 않았다.
기록 및 범위
- 제품 0.1.1 유지. WAV/manifest 재생성 및 package/lock 변경 없음.
- 런타임 버전 조회·외부 TTS/API 호출 없음. 저장 WAV의 상대 URL query만 추가했다.
- 스크린샷은 test-results/에 저장 후 이번 artifacts/로 복사했다.
과거 verify_browser.mjs, 로그, 스크린샷은 수정하거나 다시 실행해 덮어쓰지 않았다.
- 00-baseline.json의 기존 evidence 149개, 오디오 102개, 기존 RESULT.md,
package/lock 포함 총 254개 파일은 최종 13-final-integrity.json에서 해시 대조한다.
- .agent/RESULT-v0.1.1.md에는 이번 결과를 뒤에 추가한다.
- Git commit/push/tag/merge 및 서버 배포 실행 없음. 로컬 production preview만 사용했다.
@@ -0,0 +1,50 @@
{
"utc": "2026-10-14T07:27:57.445449+00:00",
"status": "PASS",
"productVersion": "0.1.1",
"preservedFiles": 254,
"preservedHistoricalEvidenceFiles": 149,
"preservedAudioFiles": 102,
"changedHistoricalFiles": [],
"headBefore": "a92b2ba1e973c65a57e6e34cc52568cd6b208c1e",
"headAfter": "a92b2ba1e973c65a57e6e34cc52568cd6b208c1e",
"previousResultContentPreservedAsPrefix": true,
"gitDiffCheck": "PASS",
"unit": {
"files": 12,
"tests": 65,
"skipped": 0
},
"browser": {
"tests": 16,
"skipped": 0,
"launchesWithoutNoSandbox": 4,
"projects": {
"desktop": {
"wavFiles": 101,
"cacheVersion": "0.1.1",
"externalRequests": 0,
"pageErrors": 0
},
"mobile": {
"wavFiles": 101,
"cacheVersion": "0.1.1",
"externalRequests": 0,
"pageErrors": 0
}
}
},
"sourceSha256": {
"src/audio/assets.ts": "beb8da92ab1ae6dae6bc3b4db37afefc72a07bbf089fe4ea5948043a9eead79a",
"src/audio/engine.ts": "971bbe451c984cb4209e9f0338c3b9b422035e9b1339484effb5d3c2a7e6352f",
"src/game.ts": "3d0fd88ebeebcd4f25696168dd07a669bf3161d620091a67bb81882aa87232aa",
"tests/audio.test.ts": "58ebce8a48b83dae6f45d92175b37ff1b5e654ea067d0b2658bc879162b0d488",
"tests/game.test.ts": "bcb03d0c2d84177664b5320d53fb5bf2b3865dbbdc3bb281caf47199a57620e1",
"tests/audio-requests.test.ts": "9496b5515982dfb2eb24d3e4bf99da11eac28812164eaa557443fdf716bd9dd3",
"tests/browser/audio-requests.ts": "3dff162cbc5a9b735fb8f911ed83ca04d646ba011511c4f922faf712f8094295",
"tests/browser/audio.spec.ts": "f26ea575d9e5471b5746471e6704a0694e1d0a4cde34f8c5faf66ff2d9df9d16",
"tests/browser/game.spec.ts": "64306590d39c90cc8fe208ff758b63ba889ca1581ee081c520dfd43e97d7df1e",
"tests/browser/ui.spec.ts": "a342a49f83a2ef7f1b78145be08a457463baf53a8fdfeac28649bcd7bc14bbbf"
},
"gitCommitPushTagMergeDeploymentPerformed": false
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+7 -1
View File
@@ -79,7 +79,9 @@ Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격
브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요.
실제 음성 재생이 끝나기를 기다려 약 2–3분이 걸립니다. 로컬 4173 포트를
테스트 중에만 열고 종료합니다. v0.1.1 스크린샷은 `.agent/evidence-v0.1.1/`에 남습니다.
테스트 중에만 열고 종료합니다. 스크린샷과 101개 WAV의 query URL·해시·디코드
결과는 `test-results/`에 저장합니다. 인수 기록은 실행별 새 evidence 경로에
복사하여 `.agent/evidence-v0.1.1/`의 과거 결과를 보존합니다.
## 음성과 문서
@@ -89,6 +91,10 @@ rate `-6%`, pitch `-4Hz`이며 FFmpeg로 **24kHz mono PCM16**으로 변환합니
생성 시 네트워크가 필요하며 앱은 저장된 WAV만 재생합니다. 환경음은 브라우저에서
고정 seed로 합성합니다. 생성 음성의 독점 권리를 주장하지 않습니다.
intro와 목표 WAV는 모두 `/audio/파일명.wav?v=0.1.1`로 요청합니다.
`src/audio/assets.ts`의 자산 버전 한 곳을 사용하며, 같은 파일명으로 음성을
교체할 때 이 값을 갱신해 기존 7일 캐시와 구분합니다. 제품 버전은 0.1.1입니다.
Linux에서 Python 3.11 이상과 FFmpeg를 준비한 뒤 다음 명령을 사용합니다.
클라이언트 버전을 고정해도 원격 모델 변경으로 동일 바이트 재생성은 보장하지 않습니다.
+5
View File
@@ -81,6 +81,11 @@ Promise를 무시한다. 질문과 보기는 재생 완료 후 DOM에 생성되
RMS를 0.03으로 정규화한다. `gain = 10^(dB/20)`으로 목표 gain을 지정한다.
시간별·주파수별 청감이나 물리적 dB SPL을 보장하는 수치는 아니다.
`src/audio/assets.ts`는 음성 자산 버전과 URL 생성을 한 곳에서 관리한다.
intro와 목표 100개 모두 `?v=0.1.1`을 붙여 동일 파일명에 남은 이전 캐시를
피한다. 파일을 다시 생성하면 제품 버전 유지 여부와 관계없이 자산 버전을
갱신한다. 버전 확인을 위한 외부 요청이나 런타임 manifest 요청은 없다.
```text
배경 BufferSource → 배경 Gain (1, 시작/끝 fade) ─┐
목표 BufferSource → 음성 Gain (10^(dB/20)) ─────┼→ master Gain → destination
+9 -6
View File
@@ -48,11 +48,11 @@ server {
expires 1y;
}
# 음성 파일명은 재생성해도 같으므로 재검증한다. 누락 시 index.html 대신
# 404를 반환해야 앱이 명확한 음성 로드 오류와 재시도를 제공할 수 있다.
# 앱은 intro·목표 모두 같은 자산 버전 query를 붙여 이전 캐시와 구분한다.
# $uri는 query를 제외한 실제 WAV 경로다. 누락 시 index.html 대신 404로 응답한다.
location /audio/ {
try_files $uri =404;
expires -1;
expires 7d;
}
# SPA의 직접 진입·새로고침을 index.html로 받되 HTML은 재검증한다.
@@ -67,10 +67,13 @@ server {
1. 서버에 적용하기 전 `nginx -t`, 기존 서비스 충돌, 파일 읽기 권한을 확인한다.
2. TLS/HTTPS와 DNS를 운영자가 확인한다. 실제 도메인에서 시작 후 음성이 들리는지 확인한다.
3. `audio/intro.wav`와 목표 WAV가 정상 응답하고 존재하지 않는 오디오는 404인지 확인한다.
3. `audio/intro.wav?v=0.1.1`와 목표 WAV의 같은 버전 query가 정상 응답하고
존재하지 않는 오디오는 query가 있어도 404인지 확인한다.
4. 모바일 Safari/Chrome, 데스크톱에서 음량·중단·다시 듣기·10문제 완주를 직접 확인한다.
5. 원자적으로 릴리스 디렉터리를 전환하고 이전 디렉터리를 보관해 되돌릴 수 있게 한다.
오프라인 캐시를 제공하는 PWA는 아니다. 정적 자산의 최초 로드에는 해당 서버
연결이 필요하다. 페이지·오디오 캐시가 서로 다른 버전으로 섞이지 않게 HTML과
음성의 재검증 정책을 유지한다.
연결이 필요하다. HTML은 재검증하고, 오디오는 URL의 자산 버전으로 구분한다.
같은 파일명으로 WAV를 교체할 때 `src/audio/assets.ts`의 `AUDIO_ASSET_VERSION`도
갱신한다. 제품 버전을 유지하는 음성 수정에서도 새 자산 버전이 필요하다.
이 query는 로컬 정적 파일 요청에만 사용하며 런타임 외부 요청을 추가하지 않는다.
+7
View File
@@ -0,0 +1,7 @@
// WAV를 같은 파일명으로 교체하면 장기 캐시가 남는다. 음성 내용이 바뀔 때
// 이 자산 버전도 갱신한다. 제품 버전과 별개로 관리하며 intro·목표에 함께 쓴다.
export const AUDIO_ASSET_VERSION = "0.1.1";
export function audioAssetUrl(file: string): string {
return `/audio/${file}?v=${AUDIO_ASSET_VERSION}`;
}
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Question } from "../game";
import { audioAssetUrl } from "./assets";
import { dbToGain, normalizeRms, synthesizeBackground } from "./dsp";
// 인터페이스를 주입하면 화면 테스트가 실제 스피커를 켜지 않아도 재생 완료와
@@ -111,7 +112,7 @@ export class AudioEngine implements AudioPort {
return buffer;
};
const [introBuffer, voiceBuffer] = await Promise.all([
intro ? load("/audio/intro.wav") : Promise.resolve(null),
intro ? load(audioAssetUrl("intro.wav")) : Promise.resolve(null),
load(question.audioUrl),
]);
// fetch를 무시하는 환경에서도 이 토큰 검사로 취소된 세션의 부활을 막는다.
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Environment, Pattern, WordEntry } from "./content";
import { audioAssetUrl } from "./audio/assets";
import {
ENVIRONMENTS,
LEVELS,
@@ -78,7 +79,7 @@ export function createQuestions(
environment:
ENVIRONMENTS.find((e) => e.id === environment) ??
ENVIRONMENTS[index % ENVIRONMENTS.length]!,
audioUrl: `/audio/${pattern.id}-${word.id}.wav`,
audioUrl: audioAssetUrl(`${pattern.id}-${word.id}.wav`),
};
});
}
+26
View File
@@ -0,0 +1,26 @@
import { expect, it } from "vitest";
import { isAudioRequest } from "./browser/audio-requests";
it("WAV 요청 판정은 query의 버전·순서와 무관하게 pathname을 사용한다", () => {
for (const path of [
"/audio/intro.wav?v=0.1.1",
"/audio/luck-apple.wav?v=0.1.1",
"/audio/luck-apple.wav?retry=1&v=0.1.1",
"/audio/luck-apple.wav?v=0.1.1&retry=1",
"/audio/luck-apple.wav",
]) {
expect
.soft(isAudioRequest(new URL(path, "http://localhost")), path)
.toBe(true);
}
for (const path of [
"/audio/manifest.json?file=intro.wav",
"/other/intro.wav",
"/audio-backup/intro.wav",
"/audio/intro.wav.txt?v=0.1.1",
]) {
expect
.soft(isAudioRequest(new URL(path, "http://localhost")), path)
.toBe(false);
}
});
+53 -4
View File
@@ -88,6 +88,54 @@ const flush = async () => {
};
describe("오디오 재생 수명과 믹서", () => {
it("intro와 목표 실제 fetch URL에 같은 명시적 cache-busting 버전을 붙인다", async () => {
const f = fixture();
const done = f.engine.play(question, true, vi.fn());
await flush();
f.sources.at(-1)!.onended?.();
await done;
expect(f.fetcher.mock.calls.map(([url]) => url)).toEqual([
"/audio/intro.wav?v=0.1.1",
`/audio/${question.id}.wav?v=0.1.1`,
]);
f.engine.dispose();
});
it.each(["intro", "target"])(
"%s 404 후 intro·목표를 동일한 versioned URL로 다시 요청한다",
async (failed) => {
const f = fixture();
let pendingFailure = true;
f.fetcher.mockImplementation(async (input) => {
const pathname = new URL(String(input), "http://localhost").pathname;
if (
pendingFailure &&
pathname ===
`/audio/${failed === "intro" ? "intro" : question.id}.wav`
) {
pendingFailure = false;
return { ok: false, status: 404 } as Response;
}
return {
ok: true,
arrayBuffer: async () => new ArrayBuffer(8),
} as Response;
});
await expect(f.engine.play(question, true, vi.fn())).rejects.toThrow(
"음성 파일",
);
const done = f.engine.play(question, true, vi.fn());
await flush();
f.sources.at(-1)!.onended?.();
await done;
expect(f.fetcher.mock.calls.map(([url]) => url)).toEqual([
"/audio/intro.wav?v=0.1.1",
`/audio/${question.id}.wav?v=0.1.1`,
"/audio/intro.wav?v=0.1.1",
`/audio/${question.id}.wav?v=0.1.1`,
]);
f.engine.dispose();
},
);
it("생성 시 무음이며 gesture의 play에서만 장치를 연다", async () => {
const f = fixture();
expect(f.factory).not.toHaveBeenCalled();
@@ -98,10 +146,11 @@ describe("오디오 재생 수명과 믹서", () => {
await flush();
expect(started).toHaveBeenCalledOnce();
expect(f.sources).toHaveLength(3);
expect(f.fetcher.mock.calls.map((call) => call[0])).toEqual([
"/audio/intro.wav",
question.audioUrl,
]);
expect(
f.fetcher.mock.calls.map(
([url]) => new URL(String(url), "http://localhost").pathname,
),
).toEqual(["/audio/intro.wav", `/audio/${question.id}.wav`]);
// master, 배경, 목표, 안내의 gain을 분리한다. 안내는 점차 소음에 묻힌다.
expect(f.gains[1]!.gain.setValueAtTime).toHaveBeenCalledWith(
1,
+5
View File
@@ -0,0 +1,5 @@
// 전체 URL의 접미사나 glob은 query가 붙으면 WAV를 놓친다. 경로만 판정하여
// 요청 관찰·정답 mapping·404 route·101개 전수 검사가 같은 기준을 사용한다.
export function isAudioRequest(url: URL): boolean {
return /^\/audio\/[^/]+\.wav$/.test(url.pathname);
}
+125
View File
@@ -0,0 +1,125 @@
import { readFileSync, writeFileSync } from "node:fs";
import { expect, test } from "@playwright/test";
import { createQuestions } from "../../src/game";
import { isAudioRequest } from "./audio-requests";
// 과거 evidence의 검증 스크립트는 보존하고, query URL 전수 검사를 정규 suite로 옮긴다.
test("intro·목표 101개 versioned WAV를 실제 Chrome에서 해시·디코드 검증한다", async ({
page,
browser,
}, info) => {
const external: string[] = [];
const errors: string[] = [];
const requests: string[] = [];
page.on("request", (request) => {
const url = new URL(request.url());
if (url.origin !== "http://127.0.0.1:4173") external.push(url.href);
if (isAudioRequest(url)) requests.push(url.href);
});
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/");
await expect(page).toHaveTitle("단어를 맞춰라 · Find-Word v0.1.1");
await expect(page.locator(".version")).toHaveText("v0.1.1");
// intro는 테스트에서 URL을 재조립하지 않고 배포 앱의 실제 요청을 관찰한다.
const introRequest = page.waitForRequest(
(request) => new URL(request.url()).pathname === "/audio/intro.wav",
);
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
const introUrl = (await introRequest).url();
await page.getByRole("button", { name: "처음으로" }).click();
const targetUrls = [
...new Set(
Array.from({ length: 200 }, (_, seed) =>
createQuestions(seed, "mixed").map((question) => question.audioUrl),
).flat(),
),
];
expect(targetUrls).toHaveLength(100);
const urls = [
introUrl,
...targetUrls.map((url) => new URL(url, page.url()).href),
];
expect(
new Set(urls.map((url) => new URL(url).searchParams.get("v"))),
).toEqual(new Set(["0.1.1"]));
expect(
urls.every((url) => new URL(url).origin === new URL(page.url()).origin),
).toBe(true);
const manifest = JSON.parse(
readFileSync("public/audio/manifest.json", "utf8"),
) as {
version: string;
files: { file: string; sha256: string; bytes: number; duration: number }[];
};
expect(manifest.version).toBe("0.1.1");
expect(urls.map((url) => new URL(url).pathname).sort()).toEqual(
manifest.files.map((entry) => `/audio/${entry.file}`).sort(),
);
requests.length = 0;
const rows = await page.evaluate(
async ({ urls, entries }) => {
const context = new OfflineAudioContext(1, 1, 24000);
const rows = [];
for (const url of urls) {
// query를 파일명으로 취급하지 않는다. 원문 파일과 응답 해시를 독립 대조한다.
const entry = entries.find(
(entry) => `/audio/${entry.file}` === new URL(url).pathname,
)!;
const response = await fetch(url);
if (!response.ok) throw Error(`${url}: HTTP ${response.status}`);
const bytes = await response.arrayBuffer();
const sha256 = Array.from(
new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)),
(b) => b.toString(16).padStart(2, "0"),
).join("");
if (sha256 !== entry.sha256 || bytes.byteLength !== entry.bytes)
throw Error(`${url}: integrity mismatch`);
const buffer = await context.decodeAudioData(bytes);
if (
buffer.sampleRate !== 24000 ||
buffer.numberOfChannels !== 1 ||
Math.abs(buffer.duration - entry.duration) > 1e-9
)
throw Error(`${url}: decode format/duration mismatch`);
let power = 0,
peak = 0;
for (const sample of buffer.getChannelData(0)) {
power += sample * sample;
peak = Math.max(peak, Math.abs(sample));
}
const rms = Math.sqrt(power / buffer.length);
if (rms <= 100 / 32768 || peak >= 32767 / 32768)
throw Error(`${url}: signal failure`);
rows.push({
url,
sha256,
bytes: entry.bytes,
sampleRate: buffer.sampleRate,
channels: buffer.numberOfChannels,
duration: buffer.duration,
rms,
peak,
});
}
return rows;
},
{ urls, entries: manifest.files },
);
expect(rows).toHaveLength(101);
expect([...requests].sort()).toEqual([...urls].sort());
expect(external).toEqual([]);
expect(errors).toEqual([]);
const path = info.outputPath("browser-audio-integrity.json");
writeFileSync(
path,
JSON.stringify(
{ chrome: browser.version(), external, errors, files: rows },
null,
2,
) + "\n",
);
await info.attach("101 versioned WAV integrity", {
path,
contentType: "application/json",
});
});
+74 -27
View File
@@ -1,6 +1,7 @@
import { expect, test } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
import { WORDS } from "../../src/content";
import { isAudioRequest } from "./audio-requests";
// 실제 WAV를 디코드하고 Web Audio의 ended를 기다린다. 모의 엔진을 사용한
// 컴포넌트 검증과 달리 배포 산출물·자동 재생 정책·레이아웃을 함께 확인한다.
@@ -10,11 +11,14 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부
const external: string[] = [];
const errors: string[] = [];
let audioPath = "";
const audioUrls: URL[] = [];
page.on("request", (request) => {
if (!request.url().startsWith("http://127.0.0.1:4173"))
external.push(request.url());
if (request.url().endsWith(".wav") && !request.url().endsWith("intro.wav"))
audioPath = new URL(request.url()).pathname;
const url = new URL(request.url());
if (url.origin !== "http://127.0.0.1:4173") external.push(request.url());
if (isAudioRequest(url)) {
audioUrls.push(url);
if (url.pathname !== "/audio/intro.wav") audioPath = url.pathname;
}
});
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/");
@@ -29,12 +33,17 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부
).toBe(true);
await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({
path: `.agent/evidence-v0.1.1/${info.project.name}-home.png`,
path: info.outputPath("home.png"),
fullPage: true,
});
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
await page.emulateMedia({ reducedMotion: "reduce" });
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
await expect.poll(() => audioUrls.length).toBe(2);
expect(audioUrls.map((url) => url.searchParams.get("v"))).toEqual([
"0.1.1",
"0.1.1",
]);
await expect(page.getByRole("button", { name: /듣는 중/ })).toBeDisabled();
await expect(page.locator(".sound-wave.active i").first()).toHaveCSS(
"animation-name",
@@ -67,7 +76,7 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부
).toBe(true);
await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({
path: `.agent/evidence-v0.1.1/${info.project.name}-game.png`,
path: info.outputPath("game.png"),
fullPage: true,
});
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
@@ -99,7 +108,7 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부
).toBe(true);
await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({
path: `.agent/evidence-v0.1.1/${info.project.name}-results.png`,
path: info.outputPath("results.png"),
fullPage: true,
});
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
@@ -107,28 +116,66 @@ test("실제 오디오로 혼합 모드 완주·레이아웃·접근성·외부
await expect(
page.getByRole("button", { name: "게임 시작 · 소리 듣기" }),
).toBeVisible();
expect(new Set(audioUrls.map((url) => url.searchParams.get("v")))).toEqual(
new Set(["0.1.1"]),
);
expect(
audioUrls.filter((url) => url.pathname !== "/audio/intro.wav"),
).toHaveLength(11);
expect(external).toEqual([]);
expect(errors).toEqual([]);
});
test("오디오 HTTP 실패를 화면에 표시하고 재시도한다", async ({ page }) => {
await page.route("**/audio/*.wav", (route) =>
route.fulfill({ status: 404, body: "missing" }),
);
await page.goto("/");
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
await expect(page.getByRole("alert")).toContainText(
"소리를 재생하지 못했어요",
);
await page.unroute("**/audio/*.wav");
await page.getByRole("button", { name: "다시 시도" }).click();
// 재시도는 안내부터 재생한다. v0.1.1 최대 7.056 + 11.256 + 시작·전환 0.4초에
// 로드·디코드·병렬 브라우저 스케줄링 여유를 더해 이 assertion만 30초로 제한한다.
await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({
timeout: 30_000,
for (const failed of ["intro", "target"]) {
test(`${failed} WAV query URL의 404를 표시하고 같은 URL로 재시도·정답 mapping한다`, async ({
page,
}) => {
const audioUrls: string[] = [];
const missing: string[] = [];
page.on("request", (request) => {
if (isAudioRequest(new URL(request.url()))) audioUrls.push(request.url());
});
const matchesFailure = (url: URL) =>
isAudioRequest(url) &&
(failed === "intro"
? url.pathname === "/audio/intro.wav"
: url.pathname !== "/audio/intro.wav");
await page.route(matchesFailure, (route) => {
missing.push(route.request().url());
return route.fulfill({ status: 404, body: "missing" });
});
await page.goto("/");
await page.getByRole("button", { name: "게임 시작 · 소리 듣기" }).click();
await expect(page.getByRole("alert")).toContainText(
"소리를 재생하지 못했어요",
);
expect(missing).toHaveLength(1);
expect(new URL(missing[0]!).searchParams.get("v")).toBe("0.1.1");
expect(audioUrls).toHaveLength(2);
await page.unroute(matchesFailure);
await page.getByRole("button", { name: "다시 시도" }).click();
// 안내 7.056 + 최대 목표 11.256초와 로드·디코드 여유를 포함한다.
await expect(page.getByRole("group", { name: "답 선택" })).toBeVisible({
timeout: 30_000,
});
expect(audioUrls).toHaveLength(4);
expect(audioUrls.slice(2).sort()).toEqual(audioUrls.slice(0, 2).sort());
expect(
new Set(audioUrls.map((url) => new URL(url).searchParams.get("v"))),
).toEqual(new Set(["0.1.1"]));
const targetPath = audioUrls
.map((url) => new URL(url).pathname)
.find((path) => path !== "/audio/intro.wav")!;
const answer = WORDS.find((word) =>
targetPath.endsWith(`-${word.id}.wav`),
)!.answer;
await page.getByRole("button", { name: new RegExp(answer) }).click();
await expect(
page.getByRole("heading", { name: "정답이에요!" }),
).toBeVisible();
await page.getByRole("button", { name: "처음으로" }).click();
await expect(
page.getByRole("button", { name: "게임 시작 · 소리 듣기" }),
).toBeFocused();
});
await page.getByRole("button", { name: "처음으로" }).click();
await expect(
page.getByRole("button", { name: "게임 시작 · 소리 듣기" }),
).toBeFocused();
});
}
+1 -1
View File
@@ -95,7 +95,7 @@ test("한국어 제목은 단어 중간에서 줄바꿈하지 않는다", async
// 이미지 안으로 들어오는 Chrome 캡처 아티팩트를 방지한다. 초점은 유지한다.
await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({
path: `.agent/evidence-v0.1.1/${info.project.name}-heading.png`,
path: info.outputPath("heading.png"),
fullPage: true,
});
const gameHeadings = await wordBreaks();
+21 -1
View File
@@ -11,6 +11,24 @@ import {
// 고정 seed로 재현 가능성을 확보하되 seed를 바꾸면 10문제를 넘는 조합이
// 실제로 선택되는지 본다. 오디오 URL도 같은 문장 조합을 가리켜야 한다.
describe("출제와 정답 판정", () => {
it("목표 WAV 100개 모두 같은 명시적 cache-busting 버전과 정답 mapping을 유지한다", () => {
const questions = new Map(
Array.from({ length: 200 }, (_, seed) => createQuestions(seed, "mixed"))
.flat()
.map((q) => [q.id, q]),
);
expect(questions.size).toBe(100);
const versions = new Set<string | null>();
for (const q of questions.values()) {
const url = new URL(q.audioUrl, "http://localhost");
expect(url.origin).toBe("http://localhost");
expect(url.pathname).toBe(`/audio/${q.pattern.id}-${q.word.id}.wav`);
expect(q.id).toBe(`${q.pattern.id}-${q.word.id}`);
expect(q.options).toContain(q.word.answer);
versions.add(url.searchParams.get("v"));
}
expect(versions).toEqual(new Set(["0.1.1"]));
});
it("실제 보기에 있는 경쟁 단어를 문장에 넣고 청취 연산별 순서를 유지한다", () => {
for (let seed = 0; seed < 20; seed++) {
for (const q of createQuestions(seed, "choice")) {
@@ -43,7 +61,9 @@ describe("출제와 정답 판정", () => {
expect(new Set(q.options).size).toBe(4);
expect(q.options.filter((o) => o === q.word.answer)).toHaveLength(1);
expect(q.sentence).not.toContain("{word}");
expect(q.audioUrl).toBe(`/audio/${q.pattern.id}-${q.word.id}.wav`);
expect(new URL(q.audioUrl, "http://localhost").pathname).toBe(
`/audio/${q.pattern.id}-${q.word.id}.wav`,
);
});
expect(createQuestions(42, "mixed")).toEqual(questions);
expect(