fix: version neural audio requests for v0.1.1
@@ -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
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 62 KiB |
@@ -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를 준비한 뒤 다음 명령을 사용합니다.
|
||||
클라이언트 버전을 고정해도 원격 모델 변경으로 동일 바이트 재생성은 보장하지 않습니다.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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는 로컬 정적 파일 요청에만 사용하며 런타임 외부 요청을 추가하지 않는다.
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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를 무시하는 환경에서도 이 토큰 검사로 취소된 세션의 부활을 막는다.
|
||||
|
||||
@@ -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`),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// 전체 URL의 접미사나 glob은 query가 붙으면 WAV를 놓친다. 경로만 판정하여
|
||||
// 요청 관찰·정답 mapping·404 route·101개 전수 검사가 같은 기준을 사용한다.
|
||||
export function isAudioRequest(url: URL): boolean {
|
||||
return /^\/audio\/[^/]+\.wav$/.test(url.pathname);
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||