Files
2026-10-14 10:01:46 +00:00

2348 lines
78 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
$ npx vitest run tests/App.test.tsx tests/neural-release.test.ts
RUN v5.0.4 /home/hermes/worktrees/find-word-v0.2.3
❯ tests/neural-release.test.ts (4 tests | 1 failed) 54ms
× README는 현재 리뷰 증거 경로와 정확한 라운드 보너스를 안내한다 44ms
❯ tests/App.test.tsx (23 tests | 6 failed) 6299ms
× 오디오 started=false 중 종료는 즉시 정지하고 결과를 거쳐 홈으로 간다 227ms
× 오디오 started=true 중 종료는 즉시 정지하고 결과를 거쳐 홈으로 간다 208ms
× 5단계 완료는 6단계 대신 같은 단계의 새 세 문항을 시작한다 601ms
× 세 문항 라운드에서 가능한 연속 정답 보너스 상한을 설명한다 100ms
× 현재 문항 진행률은 1/3→2/3→3/3이고 1단계 만점은 330점이다 221ms
× 무응답 종료는 정답률 대신 미응답 상태와 단계별 미진행을 보여 준다 215ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 7 ⎯⎯⎯⎯⎯⎯⎯
FAIL tests/App.test.tsx > 오디오 started=false 중 종료는 즉시 정지하고 결과를 거쳐 홈으로 간다
FAIL tests/App.test.tsx > 오디오 started=true 중 종료는 즉시 정지하고 결과를 거쳐 홈으로 간다
TestingLibraryElementError: Unable to find an accessible element with the role "heading" and name "문제 풀이를 중간에 종료했어요."
Here are the accessible roles:
link:
Name "본문으로 건너뛰기":
<a
class="skip-link"
href="#main"
/>
Name "환경음 출처·라이선스":
<a
href="/AMBIENCE_NOTICES.md"
/>
--------------------------------------------------
banner:
Name "":
<header
class="header wrap"
/>
--------------------------------------------------
heading:
Name "단어를 맞춰라":
<h1 />
Name "문제 풀이를 종료했어요.":
<h2
tabindex="-1"
/>
--------------------------------------------------
main:
Name "":
<main
class="wrap"
id="main"
tabindex="-1"
/>
--------------------------------------------------
paragraph:
Name "":
<p
class="muted"
/>
Name "":
<p />
Name "":
<p
class="muted"
/>
Name "":
<p />
Name "":
<p />
--------------------------------------------------
strong:
Name "":
<strong />
Name "":
<strong />
Name "":
<strong />
--------------------------------------------------
table:
Name "난이도별 결과":
<table
aria-label="난이도별 결과"
/>
--------------------------------------------------
rowgroup:
Name "":
<thead />
Name "":
<tbody />
--------------------------------------------------
row:
Name "난이도 SNR 맞힌 문제":
<tr />
Name "1단계 SNR +6 dB 0 / 0":
<tr />
Name "2단계 SNR +3 dB 0 / 0":
<tr />
Name "3단계 SNR 0 dB 0 / 0":
<tr />
Name "4단계 SNR -3 dB 0 / 0":
<tr />
Name "5단계 SNR -6 dB 0 / 0":
<tr />
--------------------------------------------------
columnheader:
Name "난이도":
<th
scope="col"
/>
Name "SNR":
<th
scope="col"
/>
Name "맞힌 문제":
<th
scope="col"
/>
--------------------------------------------------
rowheader:
Name "1단계":
<th
scope="row"
/>
Name "2단계":
<th
scope="row"
/>
Name "3단계":
<th
scope="row"
/>
Name "4단계":
<th
scope="row"
/>
Name "5단계":
<th
scope="row"
/>
--------------------------------------------------
cell:
Name "SNR +6 dB":
<td />
Name "0 / 0":
<td />
Name "SNR +3 dB":
<td />
Name "0 / 0":
<td />
Name "SNR 0 dB":
<td />
Name "0 / 0":
<td />
Name "SNR -3 dB":
<td />
Name "0 / 0":
<td />
Name "SNR -6 dB":
<td />
Name "0 / 0":
<td />
--------------------------------------------------
button:
Name "처음 화면으로":
<button
class="secondary"
/>
--------------------------------------------------
slider:
Name "전체 음량":
<input
id="volume"
max="100"
min="0"
type="range"
value="35"
/>
--------------------------------------------------
status:
Name "":
<output
for="volume"
/>
--------------------------------------------------
contentinfo:
Name "":
<footer
class="footer wrap"
/>
--------------------------------------------------
Ignored nodes: comments, script, style
<body>
<div>
<a
class="skip-link"
href="#main"
>
본문으로 건너뛰기
</a>
<header
class="header wrap"
>
<div
class="brand"
>
<span
aria-hidden="true"
class="brand-mark"
>
<i />
<i />
<i />
<i />
</span>
<h1>
단어를 맞춰라
</h1>
<span
class="version"
>
v0.2.3
</span>
</div>
<div
class="header-actions"
>
<span
class="little-dot"
/>
소리에 집중하는 시간
</div>
</header>
<main
class="wrap"
id="main"
tabindex="-1"
>
<section
class="results-panel panel"
>
<span
class="eyebrow"
>
지금까지의 소리 탐험
</span>
<div
class="result-mascot"
>
<div
aria-hidden="true"
class="mascot small"
>
<span
class="ear left"
/>
<span
class="ear right"
/>
<span
class="eye left"
/>
<span
class="eye right"
/>
<span
class="smile"
/>
<b>
S
</b>
</div>
</div>
<h2
tabindex="-1"
>
문제 풀이를 종료했어요.
</h2>
<p
class="muted"
>
작은 소리에 집중한 당신, 다음 탐험에서도 만나요.
</p>
<p>
응답 
0
문항 · 정답 
0
문항
</p>
<div
class="stats"
>
<div>
<strong>
0
</strong>
<span>
총 점수
</span>
</div>
<div>
<strong>
0
%
</strong>
<span>
정답률 · 
0
/
0
</span>
</div>
<div>
<strong>
0
<small>
회
</small>
</strong>
<span>
최고 연속 정답
</span>
</div>
</div>
<table
aria-label="난이도별 결과"
>
<thead>
<tr>
<th
scope="col"
>
난이도
</th>
<th
scope="col"
>
SNR
</th>
<th
scope="col"
>
맞힌 문제
</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>
1
단계
</th>
<td>
SNR +6 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
2
단계
</th>
<td>
SNR +3 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
3
단계
</th>
<td>
SNR 0 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
4
단계
</th>
<td>
SNR -3 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
5
단계
</th>
<td>
SNR -6 dB
</td>
<td>
0
 / 
0
</td>
</tr>
</tbody>
</table>
<p
class="muted"
>
단계 × 100점 + 연속 정답 보너스 (라운드 내 최대
 
20
점)
</p>
<button
class="secondary"
>
처음 화면으로
</button>
</section>
<div
class="volume-row"
>
<label
for="volume"
>
<span
aria-hidden="true"
>
◖))
</span>
 전체 음량
</label>
<input
id="volume"
max="100"
min="0"
type="range"
value="35"
/>
<output
for="volume"
>
35
%
</output>
</div>
</main>
<footer
class="footer wrap"
>
<p>
Designed by OZ at Heat-T Medical Inc.
</p>
<p>
청각적 주의·식별 게임 · 의료적 진단이나 치료를 제공하지 않습니다.
</p>
<span>
개인정보 수집 없이, 오직 플레이만.
</span>
<a
href="/AMBIENCE_NOTICES.md"
>
환경음 출처·라이선스
</a>
</footer>
</div>
</body>
❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19
❯ node_modules/@testing-library/dom/dist/query-helpers.js:76:38
❯ node_modules/@testing-library/dom/dist/query-helpers.js:52:17
❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19
❯ tests/App.test.tsx:415:14
413| expect(f.audio.stop).toHaveBeenCalledOnce();
414| expect(
415| screen.getByRole("heading", { name: "문제 풀이를 중간에 종료했어요." }),
| ^
416| ).toHaveFocus();
417| expect(screen.getByText("응답 0문항 · 정답 0문항")).toBeInTheDocument();
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/8]⎯
FAIL tests/App.test.tsx > 5단계 완료는 6단계 대신 같은 단계의 새 세 문항을 시작한다
Error: expect(element).toHaveAttribute("value", "1") // element.getAttribute("value") === "1"
Expected the element to have attribute:
value="1"
Received:
value="0"
❯ tests/App.test.tsx:477:43
475| );
476| expect(screen.getByRole("progressbar")).toHaveAttribute("max", "3");
477| expect(screen.getByRole("progressbar")).toHaveAttribute("value", "1"…
| ^
478| await f.finish();
479| });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/8]⎯
FAIL tests/App.test.tsx > 세 문항 라운드에서 가능한 연속 정답 보너스 상한을 설명한다
TestingLibraryElementError: Unable to find an element with the text: 정답마다 단계 × 100점 + 연속 정답 보너스 (문항별 최대 20점, 라운드 합계 최대 30점). This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<a
class="skip-link"
href="#main"
>
본문으로 건너뛰기
</a>
<header
class="header wrap"
>
<div
class="brand"
>
<span
aria-hidden="true"
class="brand-mark"
>
<i />
<i />
<i />
<i />
</span>
<h1>
단어를 맞춰라
</h1>
<span
class="version"
>
v0.2.3
</span>
</div>
<div
class="header-actions"
>
<span
class="little-dot"
/>
소리에 집중하는 시간
</div>
</header>
<main
class="wrap"
id="main"
tabindex="-1"
>
<section
class="results-panel panel"
>
<span
class="eyebrow"
>
지금까지의 소리 탐험
</span>
<div
class="result-mascot"
>
<div
aria-hidden="true"
class="mascot small"
>
<span
class="ear left"
/>
<span
class="ear right"
/>
<span
class="eye left"
/>
<span
class="eye right"
/>
<span
class="smile"
/>
<b>
S
</b>
</div>
</div>
<h2
tabindex="-1"
>
문제 풀이를 종료했어요.
</h2>
<p
class="muted"
>
작은 소리에 집중한 당신, 다음 탐험에서도 만나요.
</p>
<p>
응답 
0
문항 · 정답 
0
문항
</p>
<div
class="stats"
>
<div>
<strong>
0
</strong>
<span>
총 점수
</span>
</div>
<div>
<strong>
0
%
</strong>
<span>
정답률 · 
0
/
0
</span>
</div>
<div>
<strong>
0
<small>
회
</small>
</strong>
<span>
최고 연속 정답
</span>
</div>
</div>
<table
aria-label="난이도별 결과"
>
<thead>
<tr>
<th
scope="col"
>
난이도
</th>
<th
scope="col"
>
SNR
</th>
<th
scope="col"
>
맞힌 문제
</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>
1
단계
</th>
<td>
SNR +6 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
2
단계
</th>
<td>
SNR +3 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
3
단계
</th>
<td>
SNR 0 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
4
단계
</th>
<td>
SNR -3 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
5
단계
</th>
<td>
SNR -6 dB
</td>
<td>
0
 / 
0
</td>
</tr>
</tbody>
</table>
<p
class="muted"
>
단계 × 100점 + 연속 정답 보너스 (라운드 내 최대
 
20
점)
</p>
<button
class="secondary"
>
처음 화면으로
</button>
</section>
<div
class="volume-row"
>
<label
for="volume"
>
<span
aria-hidden="true"
>
◖))
</span>
 전체 음량
</label>
<input
id="volume"
max="100"
min="0"
type="range"
value="35"
/>
<output
for="volume"
>
35
%
</output>
</div>
</main>
<footer
class="footer wrap"
>
<p>
Designed by OZ at Heat-T Medical Inc.
</p>
<p>
청각적 주의·식별 게임 · 의료적 진단이나 치료를 제공하지 않습니다.
</p>
<span>
개인정보 수집 없이, 오직 플레이만.
</span>
<a
href="/AMBIENCE_NOTICES.md"
>
환경음 출처·라이선스
</a>
</footer>
</div>
</body>
❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19
❯ node_modules/@testing-library/dom/dist/query-helpers.js:76:38
❯ node_modules/@testing-library/dom/dist/query-helpers.js:52:17
❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19
❯ tests/App.test.tsx:507:12
505| await f.user.click(screen.getByRole("button", { name: "문제 풀이 종료" }));
506| expect(
507| screen.getByText("정답마다 단계 × 100점 + 연속 정답 보너스 (문항별 최대 20점, 라운드 합계 최…
| ^
508| ).toBeInTheDocument();
509| await f.finish();
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/8]⎯
FAIL tests/App.test.tsx > 현재 문항 진행률은 1/3→2/3→3/3이고 1단계 만점은 330점이다
TestingLibraryElementError: Unable to find an accessible element with the role "progressbar" and name "현재 문항 진행률"
Here are the accessible roles:
link:
Name "본문으로 건너뛰기":
<a
class="skip-link"
href="#main"
/>
Name "환경음 출처·라이선스":
<a
href="/AMBIENCE_NOTICES.md"
/>
--------------------------------------------------
banner:
Name "":
<header
class="header wrap"
/>
--------------------------------------------------
heading:
Name "단어를 맞춰라":
<h1 />
Name "카페":
<h2 />
Name "소리 사이에 집중해 보세요.":
<h2
tabindex="-1"
/>
--------------------------------------------------
button:
Name "문제 풀이 종료":
<button
class="text-button"
/>
Name "듣는 중…":
<button
class="secondary"
disabled=""
/>
Name "듣기 중단":
<button
class="text-button"
/>
--------------------------------------------------
main:
Name "":
<main
class="wrap"
id="main"
tabindex="-1"
/>
--------------------------------------------------
region:
Name "단어 찾기 게임":
<section
aria-label="단어 찾기 게임"
class="game-layout"
/>
--------------------------------------------------
progressbar:
Name "전체 진행률":
<progress
aria-label="전체 진행률"
max="3"
value="0"
/>
--------------------------------------------------
complementary:
Name "":
<aside
class="environment-panel"
/>
--------------------------------------------------
paragraph:
Name "":
<p
class="eyebrow"
/>
Name "":
<p />
Name "":
<p
class="db-note"
/>
Name "":
<p
id="snr-help"
/>
Name "":
<p />
Name "":
<p />
Name "":
<p />
--------------------------------------------------
slider:
Name "목표 음성 SNR":
<input
aria-describedby="snr-help"
id="snr"
max="12"
min="-12"
step="1"
type="range"
value="6"
/>
Name "전체 음량":
<input
id="volume"
max="100"
min="0"
type="range"
value="35"
/>
--------------------------------------------------
status:
Name "":
<output
for="snr"
/>
Name "":
<output
for="volume"
/>
Name "":
<p
role="status"
/>
--------------------------------------------------
contentinfo:
Name "":
<footer
class="footer wrap"
/>
--------------------------------------------------
Ignored nodes: comments, script, style
<body>
<div>
<a
class="skip-link"
href="#main"
>
본문으로 건너뛰기
</a>
<header
class="header wrap"
>
<div
class="brand"
>
<span
aria-hidden="true"
class="brand-mark"
>
<i />
<i />
<i />
<i />
</span>
<h1>
단어를 맞춰라
</h1>
<span
class="version"
>
v0.2.3
</span>
</div>
<div
class="header-actions"
>
<button
class="text-button"
>
문제 풀이 종료
</button>
<span
class="little-dot"
/>
소리에 집중하는 시간
</div>
</header>
<main
class="wrap"
id="main"
tabindex="-1"
>
<section
aria-label="단어 찾기 게임"
class="game-layout"
>
<div
class="game-top"
>
<span
class="eyebrow"
>
나의 소리 탐험
</span>
<span>
문제 
<b>
01
</b>
 /
 
3
</span>
</div>
<progress
aria-label="전체 진행률"
max="3"
value="0"
/>
<div
class="game-columns"
>
<aside
class="environment-panel"
>
<p
class="eyebrow"
>
지금 듣고 있는 공간
</p>
<h2>
카페
</h2>
<p>
부드러운 웅성임과 잔의 울림
</p>
<div
aria-hidden="true"
class="mascot small"
>
<span
class="ear left"
/>
<span
class="ear right"
/>
<span
class="eye left"
/>
<span
class="eye right"
/>
<span
class="smile"
/>
<b>
S
</b>
</div>
<div
aria-hidden="true"
class="sound-wave active"
>
<i
style="height: 10px; animation-delay: 0s;"
/>
<i
style="height: 33.799955450859244px; animation-delay: 0.05s;"
/>
<i
style="height: 16.132986448643948px; animation-delay: 0.1s;"
/>
<i
style="height: 32.219552375865575px; animation-delay: 0.15000000000000002s;"
/>
<i
style="height: 21.858720427326595px; animation-delay: 0.2s;"
/>
<i
style="height: 29.163690702963766px; animation-delay: 0.25s;"
/>
<i
style="height: 26.796992502245015px; animation-delay: 0.30000000000000004s;"
/>
<i
style="height: 24.835290693688798px; animation-delay: 0.35000000000000003s;"
/>
<i
style="height: 30.619883556555898px; animation-delay: 0.4s;"
/>
<i
style="height: 35.39139668035928px; animation-delay: 0.45s;"
/>
<i
style="height: 71.52943948029164px; animation-delay: 0.5s;"
/>
<i
style="height: 19.53593765210872px; animation-delay: 0.55s;"
/>
<i
style="height: 73.98674560913082px; animation-delay: 0.6000000000000001s;"
/>
<i
style="height: 16.952742107140878px; animation-delay: 0.65s;"
/>
<i
style="height: 72.19510052760725px; animation-delay: 0.7000000000000001s;"
/>
<i
style="height: 32.979734657418774px; animation-delay: 0.75s;"
/>
<i
style="height: 66.27347594564635px; animation-delay: 0.8s;"
/>
<i
style="height: 47.480789758355044px; animation-delay: 0.8500000000000001s;"
/>
<i
style="height: 56.615089141222136px; animation-delay: 0.9s;"
/>
<i
style="height: 59.492984932238116px; animation-delay: 0.9500000000000001s;"
/>
<i
style="height: 43.861291911681526px; animation-delay: 1s;"
/>
<i
style="height: 68.21866699734562px; animation-delay: 1.05s;"
/>
<i
style="height: 28.858982496016484px; animation-delay: 1.1s;"
/>
<i
style="height: 73.07841912260051px; animation-delay: 1.1500000000000001s;"
/>
<i
style="height: 12.60436847039135px; animation-delay: 1.2000000000000002s;"
/>
<i
style="height: 73.74953619965402px; animation-delay: 1.25s;"
/>
<i
style="height: 23.8231850360459px; animation-delay: 1.3s;"
/>
<i
style="height: 32.57029511072814px; animation-delay: 1.35s;"
/>
<i
style="height: 20.99981090780238px; animation-delay: 1.4000000000000001s;"
/>
<i
style="height: 29.735764963203792px; animation-delay: 1.4500000000000002s;"
/>
<i
style="height: 26.085500220240995px; animation-delay: 1.5s;"
/>
<i
style="height: 25.59070868046883px; animation-delay: 1.55s;"
/>
<i
style="height: 30.10305417152866px; animation-delay: 1.6s;"
/>
<i
style="height: 20.410372983416416px; animation-delay: 1.6500000000000001s;"
/>
<i
style="height: 32.785692656480826px; animation-delay: 1.7000000000000002s;"
/>
</div>
<div
class="level-tag"
>
1
단계 · 
SNR +6 dB
</div>
<p
class="db-note"
>
소리에 익숙해지기
</p>
<div
class="snr-control"
>
<label
for="snr"
>
목표 음성 SNR
</label>
<input
aria-describedby="snr-help"
id="snr"
max="12"
min="-12"
step="1"
type="range"
value="6"
/>
<output
for="snr"
>
SNR +6 dB
</output>
<p
id="snr-help"
>
음성이 배경보다 6 dB 커요.
 범위 −12~+12 dB · 1 dB 간격. 양수는 음성이 더 크게, 0은 같게, 음수는 더 작게 들려요. 변경은 다음 재생·재청취부터 적용돼요.
</p>
</div>
<div
class="volume-row"
>
<label
for="volume"
>
<span
aria-hidden="true"
>
◖))
</span>
 전체 음량
</label>
<input
id="volume"
max="100"
min="0"
type="range"
value="35"
/>
<output
for="volume"
>
35
%
</output>
</div>
<div
class="mini-stats"
>
<span>
점수 
<b>
0
</b>
</span>
<span>
연속 정답 
<b>
0
</b>
</span>
</div>
</aside>
<div
class="question-panel panel"
>
<div
class="question-meta"
>
<span>
두 번의 힌트
</span>
<span>
객관식
</span>
</div>
<div
class="listening"
>
<span
aria-hidden="true"
class="listening-icon"
>
♬
</span>
<h2
tabindex="-1"
>
소리 사이에 집중해 보세요.
</h2>
<p>
문장이 끝나면 질문이 나타나요.
<br />
작은 단어 하나를 기억해 주세요.
</p>
<p
role="status"
>
에스의 목소리를 듣고 있어요…
</p>
</div>
<div
class="play-controls"
>
<button
class="secondary"
disabled=""
>
듣는 중…
</button>
<button
class="text-button"
>
듣기 중단
</button>
<small>
다시 들어도 점수는 줄어들지 않아요.
</small>
</div>
</div>
</div>
</section>
</main>
<footer
class="footer wrap"
>
<p>
Designed by OZ at Heat-T Medical Inc.
</p>
<p>
청각적 주의·식별 게임 · 의료적 진단이나 치료를 제공하지 않습니다.
</p>
<span>
개인정보 수집 없이, 오직 플레이만.
</span>
<a
href="/AMBIENCE_NOTICES.md"
>
환경음 출처·라이선스
</a>
</footer>
</div>
</body>
❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19
❯ node_modules/@testing-library/dom/dist/query-helpers.js:76:38
❯ node_modules/@testing-library/dom/dist/query-helpers.js:52:17
❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19
❯ tests/App.test.tsx:518:24
516| await f.user.click(screen.getByRole("button", { name: "게임 시작 · 소리 듣기…
517| for (let index = 0; index < 3; index++) {
518| const bar = screen.getByRole("progressbar", { name: "현재 문항 진행률" });
| ^
519| expect(bar).toHaveAttribute("max", "3");
520| expect(bar).toHaveAttribute("value", String(index + 1));
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/8]⎯
FAIL tests/App.test.tsx > 무응답 종료는 정답률 대신 미응답 상태와 단계별 미진행을 보여 준다
TestingLibraryElementError: Unable to find an accessible element with the role "heading" and name "문제 풀이를 중간에 종료했어요."
Here are the accessible roles:
link:
Name "본문으로 건너뛰기":
<a
class="skip-link"
href="#main"
/>
Name "환경음 출처·라이선스":
<a
href="/AMBIENCE_NOTICES.md"
/>
--------------------------------------------------
banner:
Name "":
<header
class="header wrap"
/>
--------------------------------------------------
heading:
Name "단어를 맞춰라":
<h1 />
Name "문제 풀이를 종료했어요.":
<h2
tabindex="-1"
/>
--------------------------------------------------
main:
Name "":
<main
class="wrap"
id="main"
tabindex="-1"
/>
--------------------------------------------------
paragraph:
Name "":
<p
class="muted"
/>
Name "":
<p />
Name "":
<p
class="muted"
/>
Name "":
<p />
Name "":
<p />
--------------------------------------------------
strong:
Name "":
<strong />
Name "":
<strong />
Name "":
<strong />
--------------------------------------------------
table:
Name "난이도별 결과":
<table
aria-label="난이도별 결과"
/>
--------------------------------------------------
rowgroup:
Name "":
<thead />
Name "":
<tbody />
--------------------------------------------------
row:
Name "난이도 SNR 맞힌 문제":
<tr />
Name "1단계 SNR +6 dB 0 / 0":
<tr />
Name "2단계 SNR +3 dB 0 / 0":
<tr />
Name "3단계 SNR 0 dB 0 / 0":
<tr />
Name "4단계 SNR -3 dB 0 / 0":
<tr />
Name "5단계 SNR -6 dB 0 / 0":
<tr />
--------------------------------------------------
columnheader:
Name "난이도":
<th
scope="col"
/>
Name "SNR":
<th
scope="col"
/>
Name "맞힌 문제":
<th
scope="col"
/>
--------------------------------------------------
rowheader:
Name "1단계":
<th
scope="row"
/>
Name "2단계":
<th
scope="row"
/>
Name "3단계":
<th
scope="row"
/>
Name "4단계":
<th
scope="row"
/>
Name "5단계":
<th
scope="row"
/>
--------------------------------------------------
cell:
Name "SNR +6 dB":
<td />
Name "0 / 0":
<td />
Name "SNR +3 dB":
<td />
Name "0 / 0":
<td />
Name "SNR 0 dB":
<td />
Name "0 / 0":
<td />
Name "SNR -3 dB":
<td />
Name "0 / 0":
<td />
Name "SNR -6 dB":
<td />
Name "0 / 0":
<td />
--------------------------------------------------
button:
Name "처음 화면으로":
<button
class="secondary"
/>
--------------------------------------------------
slider:
Name "전체 음량":
<input
id="volume"
max="100"
min="0"
type="range"
value="35"
/>
--------------------------------------------------
status:
Name "":
<output
for="volume"
/>
--------------------------------------------------
contentinfo:
Name "":
<footer
class="footer wrap"
/>
--------------------------------------------------
Ignored nodes: comments, script, style
<body>
<div>
<a
class="skip-link"
href="#main"
>
본문으로 건너뛰기
</a>
<header
class="header wrap"
>
<div
class="brand"
>
<span
aria-hidden="true"
class="brand-mark"
>
<i />
<i />
<i />
<i />
</span>
<h1>
단어를 맞춰라
</h1>
<span
class="version"
>
v0.2.3
</span>
</div>
<div
class="header-actions"
>
<span
class="little-dot"
/>
소리에 집중하는 시간
</div>
</header>
<main
class="wrap"
id="main"
tabindex="-1"
>
<section
class="results-panel panel"
>
<span
class="eyebrow"
>
지금까지의 소리 탐험
</span>
<div
class="result-mascot"
>
<div
aria-hidden="true"
class="mascot small"
>
<span
class="ear left"
/>
<span
class="ear right"
/>
<span
class="eye left"
/>
<span
class="eye right"
/>
<span
class="smile"
/>
<b>
S
</b>
</div>
</div>
<h2
tabindex="-1"
>
문제 풀이를 종료했어요.
</h2>
<p
class="muted"
>
작은 소리에 집중한 당신, 다음 탐험에서도 만나요.
</p>
<p>
응답 
0
문항 · 정답 
0
문항
</p>
<div
class="stats"
>
<div>
<strong>
0
</strong>
<span>
총 점수
</span>
</div>
<div>
<strong>
0
%
</strong>
<span>
정답률 · 
0
/
0
</span>
</div>
<div>
<strong>
0
<small>
회
</small>
</strong>
<span>
최고 연속 정답
</span>
</div>
</div>
<table
aria-label="난이도별 결과"
>
<thead>
<tr>
<th
scope="col"
>
난이도
</th>
<th
scope="col"
>
SNR
</th>
<th
scope="col"
>
맞힌 문제
</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>
1
단계
</th>
<td>
SNR +6 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
2
단계
</th>
<td>
SNR +3 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
3
단계
</th>
<td>
SNR 0 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
4
단계
</th>
<td>
SNR -3 dB
</td>
<td>
0
 / 
0
</td>
</tr>
<tr>
<th
scope="row"
>
5
단계
</th>
<td>
SNR -6 dB
</td>
<td>
0
 / 
0
</td>
</tr>
</tbody>
</table>
<p
class="muted"
>
단계 × 100점 + 연속 정답 보너스 (라운드 내 최대
 
20
점)
</p>
<button
class="secondary"
>
처음 화면으로
</button>
</section>
<div
class="volume-row"
>
<label
for="volume"
>
<span
aria-hidden="true"
>
◖))
</span>
 전체 음량
</label>
<input
id="volume"
max="100"
min="0"
type="range"
value="35"
/>
<output
for="volume"
>
35
%
</output>
</div>
</main>
<footer
class="footer wrap"
>
<p>
Designed by OZ at Heat-T Medical Inc.
</p>
<p>
청각적 주의·식별 게임 · 의료적 진단이나 치료를 제공하지 않습니다.
</p>
<span>
개인정보 수집 없이, 오직 플레이만.
</span>
<a
href="/AMBIENCE_NOTICES.md"
>
환경음 출처·라이선스
</a>
</footer>
</div>
</body>
❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19
❯ node_modules/@testing-library/dom/dist/query-helpers.js:76:38
❯ node_modules/@testing-library/dom/dist/query-helpers.js:52:17
❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19
❯ tests/App.test.tsx:538:17
536| await f.user.click(screen.getByRole("button", { name: "게임 시작 · 소리 듣기…
537| await f.user.click(screen.getByRole("button", { name: "문제 풀이 종료" }));
538| expect(screen.getByRole("heading", { name: "문제 풀이를 중간에 종료했어요." })).t…
| ^
539| expect(screen.getByText("—")).toBeInTheDocument();
540| expect(screen.getByText("정답률 · 응답 없음")).toBeInTheDocument();
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/8]⎯
FAIL tests/neural-release.test.ts > README는 현재 리뷰 증거 경로와 정확한 라운드 보너스를 안내한다
AssertionError: expected '# 단어를 맞춰라 · Find-Word v0.2.3\n\n소음 속에…' to contain '.agent/evidence-v0.2.3/review-fixes/'
- Expected
+ Received
- .agent/evidence-v0.2.3/review-fixes/
+ # 단어를 맞춰라 · Find-Word v0.2.3
+
+ 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다.
+
+ **Designed by OZ at Heat-T Medical Inc.**
+
+ Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word).
+ 프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의
+ `license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요.
+ 런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며,
+ `public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다.
+ 빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다.
+
+ React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장,
+ 분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며
+ 의료적 진단·치료나 효과를 주장하지 않습니다.
+
+ ## 실행
+
+ Node.js 최소 버전은 **22.22.2** (`>=22.22.2`)입니다. 잠긴 jsdom 계열 의존성이
+ 요구하는 지원 범위는 22.x의 22.22.2 이상, 24.x의 24.15.0 이상 또는 26.0.0 이상입니다.
+ 이 중 지원되는 LTS 버전을 사용하세요. `package-lock.json`을
+ 포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다.
+
+ ```bash
+ npm ci
+ npm run dev
+ ```
+
+ 표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를
+ 누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다.
+
+ ## 플레이
+
+ - 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다.
+ - 8개 실제 현장 녹음 환경 중 하나를 지정하거나 라운드마다 섞어 세 곳을 방문합니다.
+ - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다.
+ - 시작 난이도 1~5를 고르고 정확히 **3문항**을 같은 단계에서 풉니다. 단계 기본값은 **SNR +6, +3, 0, −3, −6 dB**입니다.
+ - 목표 음성 SNR을 **−12~+12 dB, 1 dB 간격**으로 직접 조절할 수 있습니다. 재생 중 변경은 다음 재생·재청취와 남은 문제부터 적용합니다.
+ 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다.
+ - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 라운드 내 최대 20점의 보너스를 받습니다.
+ - 정상 결과에서 다음 단계의 새 3문항을 바로 시작합니다. 5단계에서는 같은 단계 재도전과 홈 복귀를 제공합니다. 수동 SNR은 다음 라운드에도 유지되고, 홈 복귀 시 선택한 시작 단계의 기본값으로 돌아갑니다.
+ - 언제든 **문제 풀이 종료**를 누르면 즉시 오디오를 취소하고 **응답 수·정답 수**를 보여 줍니다. 무응답은 오답으로 세지 않으며 결과에서 **처음 화면으로** 돌아갑니다.
+ - 결과의 SNR은 실제 마지막 청취값을 기록합니다. 점수·정답률·최고 연속 정답·단계별 결과를 확인합니다.
+ - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다.
+
+ 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과
+ 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은
+ [WORDS.md](docs/WORDS.md)에 설명했습니다.
+ 청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음
+ 단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용,
+ 오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다.
+
+ ## 검증과 정적 빌드
+
+ ```bash
+ npm run lint
+ npm test
+ python3 -m unittest discover -s tests/python -v
+ npm run build
+ npm audit
+ npm run preview
+ ```
+
+ `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기
+ 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개와 현장음 8개가 manifest 해시 그대로
+ 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다.
+ 런타임 MIT notice와 현장음 attribution/license notice가 `dist/`에 전문 그대로 포함되는지도 검증합니다.
+
+ 실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증:
+
+ ```bash
+ # Chromium이 없는 개발 환경에서 한 번만 실행합니다.
+ npx playwright install chromium
+ npm run test:browser
+ # 이미 설치된 Chrome을 사용할 때의 대안입니다.
+ CHROME_PATH=/usr/bin/google-chrome npm run test:browser
+ ```
+
+ Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격리된 개발
+ 환경에서만 `PLAYWRIGHT_NO_SANDBOX=1`을 명시해 끌 수 있습니다. `0`, `false`,
+ 빈 값이나 미설정은 sandbox를 유지합니다. `CHROME_PATH` 지정은 sandbox를 끄지 않습니다.
+
+ 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요.
+ 실제 음성 재생이 끝나기를 기다려 몇 분이 걸립니다. 로컬 4173 포트를
+ 테스트 중에만 열고 종료합니다. 스크린샷과 109개 WAV의 query URL·해시·디코드
+ 결과는 `test-results/`에 저장합니다. 인수 기록은 실행별 새 evidence 경로에
+ 복사하여 `.agent/evidence-v0.1.1/`의 과거 결과를 보존합니다.
+
+ ## 음성과 문서
+
+ 한국어 WAV 101개는 직접 쓴 문장을 Microsoft Edge online TTS의
+ `ko-KR-HyunsuMultilingualNeural`로 v0.1.1에서 생성했으며 WAV 바이트는 그대로 보존했습니다. edge-tts **7.3.1**,
+ rate `-6%`, pitch `-4Hz`이며 FFmpeg로 **24kHz mono PCM16**으로 변환합니다.
+ 생성 시 네트워크가 필요하며 앱은 저장된 WAV만 재생합니다. 환경음은 공개 라이선스의 실제 현장 녹음 8개를 각 12초로 변환해 번들합니다. 생성 음성의 독점 권리를 주장하지 않습니다.
+
+ intro와 목표 WAV는 모두 `/audio/파일명.wav?v=0.2.3`로 요청합니다.
+ `src/audio/assets.ts`의 자산 버전 한 곳을 사용하며, 같은 파일명으로 음성을
+ 교체할 때 이 값을 갱신해 기존 7일 캐시와 구분합니다. 배경은 `/ambience/환경.wav?v=0.2.3`입니다. 제품 버전은 0.2.3입니다.
+
+ Linux에서 Python 3.11 이상과 FFmpeg를 준비한 뒤 다음 명령을 사용합니다.
+ 클라이언트 버전을 고정해도 원격 모델 변경으로 동일 바이트 재생성은 보장하지 않습니다.
+
+ ```bash
+ python3 -m venv .venv-audio
+ .venv-audio/bin/python -m pip install -r requirements-audio.txt
+ .venv-audio/bin/python -m pip check
+ npm run audio:generate
+ ```
+
+ 고정 venv의 Python이 101개 모두 임시 생성·검증하고 디렉터리를 원자 교체합니다.
+ 요청별 60초 제한과 최대 3회 시도를 적용하며, 실패 시 기존 자산을 보존합니다.
+ 청취 샘플: [안내](public/audio/intro.wav), [행운의 사과](public/audio/luck-apple.wav).
+ 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를
+ 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다.
+
+ 현장음 원본은 `/tmp/findword-ambience-sources`에서만 보관하고 배포하지 않습니다.
+ `python3 scripts/ambience_audio.py`로 검증된 원본의 파생본과 manifest를 재현합니다.
+ PD·CC0·CC BY 4.0·CC BY-SA 4.0의 정확한 출처·저작자·변환·해시는
+ [현장음 manifest](public/ambience/manifest.json), [배포 고지](public/AMBIENCE_NOTICES.md)에 있습니다.
+ 시장 파생본은 CC BY-SA 4.0을 유지합니다. 외부 링크는 출처 확인용이며 앱은 이를 요청하지 않습니다.
+
+ - [변경 이력](CHANGELOG.md)
+ - [v0.2.3 구현·검증 결과](.agent/RESULT-v0.2.3.md)
+ - [설계와 상태 전이](docs/ARCHITECTURE.md)
+ - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md)
+ - [v0.1.1 RED/GREEN 및 실제 검증 결과](.agent/RESULT-v0.1.1.md)
+ - [v0.1 역사 기록](.agent/RESULT.md)
+
+ 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다.
+
❯ tests/neural-release.test.ts:72:20
70| it("README는 현재 리뷰 증거 경로와 정확한 라운드 보너스를 안내한다", () => {
71| const doc = readFileSync("README.md", "utf8");
72| expect.soft(doc).toContain(".agent/evidence-v0.2.3/review-fixes/");
| ^
73| expect.soft(doc).toContain("문항별 최대 20점, 라운드 합계 최대 30점");
74| });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/8]⎯
FAIL tests/neural-release.test.ts > README는 현재 리뷰 증거 경로와 정확한 라운드 보너스를 안내한다
AssertionError: expected '# 단어를 맞춰라 · Find-Word v0.2.3\n\n소음 속에…' to contain '문항별 최대 20점, 라운드 합계 최대 30점'
- Expected
+ Received
- 문항별 최대 20점, 라운드 합계 최대 30점
+ # 단어를 맞춰라 · Find-Word v0.2.3
+
+ 소음 속에 숨은 한국어 단어를 듣고 찾는 반응형 웹 게임입니다.
+
+ **Designed by OZ at Heat-T Medical Inc.**
+
+ Canonical repository: [OZ-Group/Find-Word](https://source.ibit.kr/OZ-Group/Find-Word).
+ 프로젝트 자체는 **private / all-rights-reserved**이며 `package.json`의
+ `license`는 `UNLICENSED`입니다. [LICENSE](LICENSE)를 참고하세요.
+ 런타임 의존성 React, ReactDOM, scheduler의 MIT 전문은
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)에 보존하며,
+ `public/THIRD_PARTY_NOTICES.md`가 빌드 시 `dist/THIRD_PARTY_NOTICES.md`로 복사됩니다.
+ 빌드 검증기는 설치된 의존성의 라이선스 전문·버전과 세 사본을 대조합니다.
+
+ React + TypeScript strict + Vite로 만든 정적 SPA입니다. 로그인, 개인정보 저장,
+ 분석 도구, 외부 CDN, 런타임 TTS/API 요청이 없습니다. 청각적 주의·식별 게임이며
+ 의료적 진단·치료나 효과를 주장하지 않습니다.
+
+ ## 실행
+
+ Node.js 최소 버전은 **22.22.2** (`>=22.22.2`)입니다. 잠긴 jsdom 계열 의존성이
+ 요구하는 지원 범위는 22.x의 22.22.2 이상, 24.x의 24.15.0 이상 또는 26.0.0 이상입니다.
+ 이 중 지원되는 LTS 버전을 사용하세요. `package-lock.json`을
+ 포함하므로 새 환경에서는 `npm ci`를 사용합니다. 이번 검증 환경은 Node 26.7.0입니다.
+
+ ```bash
+ npm ci
+ npm run dev
+ ```
+
+ 표시되는 `http://127.0.0.1:5173`에서 사용합니다. 소리는 사용자가 **게임 시작 · 소리 듣기**를
+ 누른 뒤에만 재생합니다. 낮은 음량부터 조절하고 헤드폰이나 스피커를 사용할 수 있습니다.
+
+ ## 플레이
+
+ - 골고루 풀기, 보기에서 선택, 직접 입력 중 선택합니다.
+ - 8개 실제 현장 녹음 환경 중 하나를 지정하거나 라운드마다 섞어 세 곳을 방문합니다.
+ - 에스의 안내가 배경에 묻힌 뒤 목표 문장이 나옵니다. 재생 후 질문에 답합니다.
+ - 시작 난이도 1~5를 고르고 정확히 **3문항**을 같은 단계에서 풉니다. 단계 기본값은 **SNR +6, +3, 0, −3, −6 dB**입니다.
+ - 목표 음성 SNR을 **−12~+12 dB, 1 dB 간격**으로 직접 조절할 수 있습니다. 재생 중 변경은 다음 재생·재청취와 남은 문제부터 적용합니다.
+ 양수는 음성이 배경보다 크고, 0은 같고, 음수는 더 작다는 뜻입니다.
+ - 다시 듣기는 감점하지 않습니다. 정답은 `단계 × 100점`, 연속 정답은 라운드 내 최대 20점의 보너스를 받습니다.
+ - 정상 결과에서 다음 단계의 새 3문항을 바로 시작합니다. 5단계에서는 같은 단계 재도전과 홈 복귀를 제공합니다. 수동 SNR은 다음 라운드에도 유지되고, 홈 복귀 시 선택한 시작 단계의 기본값으로 돌아갑니다.
+ - 언제든 **문제 풀이 종료**를 누르면 즉시 오디오를 취소하고 **응답 수·정답 수**를 보여 줍니다. 무응답은 오답으로 세지 않으며 결과에서 **처음 화면으로** 돌아갑니다.
+ - 결과의 SNR은 실제 마지막 청취값을 기록합니다. 점수·정답률·최고 연속 정답·단계별 결과를 확인합니다.
+ - 새로고침하면 기록이 사라집니다. 결과를 브라우저나 서버에 저장하지 않습니다.
+
+ 10개 유형 × 10개 단어의 100개 목표 문장을 저장했습니다. 각 세션은 유형과
+ 단어를 섞어 출제하며 정답 위치도 섞습니다. 사전 표제어와 변형 규칙은
+ [WORDS.md](docs/WORDS.md)에 설명했습니다.
+ 청취 과제는 직접 목표 식별, 연습 암호와 진짜 암호 구별, 앞 단어를 잊고 다음
+ 단어 기억, 첫째·마지막·가운데 선택, 반복 찾기, 정정 반영, 신호 조건 적용,
+ 오른쪽 표지 선택의 10가지입니다. 문장에 실제 보기의 오답도 포함됩니다.
+
+ ## 검증과 정적 빌드
+
+ ```bash
+ npm run lint
+ npm test
+ python3 -m unittest discover -s tests/python -v
+ npm run build
+ npm audit
+ npm run preview
+ ```
+
+ `lint`는 ESLint와 TypeScript 검사, `test`는 unit/component/자산/빌드 검증기
+ 테스트를 실행합니다. `build`는 `dist/`를 만들고 음성 101개와 현장음 8개가 manifest 해시 그대로
+ 포함되었는지 확인합니다. 빌드에는 음성 합성 도구가 필요하지 않습니다.
+ 런타임 MIT notice와 현장음 attribution/license notice가 `dist/`에 전문 그대로 포함되는지도 검증합니다.
+
+ 실제 오디오·360px/1280px 화면·키보드·접근성·오류 재시도 검증:
+
+ ```bash
+ # Chromium이 없는 개발 환경에서 한 번만 실행합니다.
+ npx playwright install chromium
+ npm run test:browser
+ # 이미 설치된 Chrome을 사용할 때의 대안입니다.
+ CHROME_PATH=/usr/bin/google-chrome npm run test:browser
+ ```
+
+ Chromium sandbox는 기본으로 켭니다. sandbox를 지원하지 않는 격리된 개발
+ 환경에서만 `PLAYWRIGHT_NO_SANDBOX=1`을 명시해 끌 수 있습니다. `0`, `false`,
+ 빈 값이나 미설정은 sandbox를 유지합니다. `CHROME_PATH` 지정은 sandbox를 끄지 않습니다.
+
+ 브라우저 테스트는 기존 `dist/`를 사용하므로 소스 변경 후 먼저 빌드하세요.
+ 실제 음성 재생이 끝나기를 기다려 몇 분이 걸립니다. 로컬 4173 포트를
+ 테스트 중에만 열고 종료합니다. 스크린샷과 109개 WAV의 query URL·해시·디코드
+ 결과는 `test-results/`에 저장합니다. 인수 기록은 실행별 새 evidence 경로에
+ 복사하여 `.agent/evidence-v0.1.1/`의 과거 결과를 보존합니다.
+
+ ## 음성과 문서
+
+ 한국어 WAV 101개는 직접 쓴 문장을 Microsoft Edge online TTS의
+ `ko-KR-HyunsuMultilingualNeural`로 v0.1.1에서 생성했으며 WAV 바이트는 그대로 보존했습니다. edge-tts **7.3.1**,
+ rate `-6%`, pitch `-4Hz`이며 FFmpeg로 **24kHz mono PCM16**으로 변환합니다.
+ 생성 시 네트워크가 필요하며 앱은 저장된 WAV만 재생합니다. 환경음은 공개 라이선스의 실제 현장 녹음 8개를 각 12초로 변환해 번들합니다. 생성 음성의 독점 권리를 주장하지 않습니다.
+
+ intro와 목표 WAV는 모두 `/audio/파일명.wav?v=0.2.3`로 요청합니다.
+ `src/audio/assets.ts`의 자산 버전 한 곳을 사용하며, 같은 파일명으로 음성을
+ 교체할 때 이 값을 갱신해 기존 7일 캐시와 구분합니다. 배경은 `/ambience/환경.wav?v=0.2.3`입니다. 제품 버전은 0.2.3입니다.
+
+ Linux에서 Python 3.11 이상과 FFmpeg를 준비한 뒤 다음 명령을 사용합니다.
+ 클라이언트 버전을 고정해도 원격 모델 변경으로 동일 바이트 재생성은 보장하지 않습니다.
+
+ ```bash
+ python3 -m venv .venv-audio
+ .venv-audio/bin/python -m pip install -r requirements-audio.txt
+ .venv-audio/bin/python -m pip check
+ npm run audio:generate
+ ```
+
+ 고정 venv의 Python이 101개 모두 임시 생성·검증하고 디렉터리를 원자 교체합니다.
+ 요청별 60초 제한과 최대 3회 시도를 적용하며, 실패 시 기존 자산을 보존합니다.
+ 청취 샘플: [안내](public/audio/intro.wav), [행운의 사과](public/audio/luck-apple.wav).
+ 생성 방법, 도구 라이선스, 합성 음질의 한계는 [AUDIO_PROVENANCE.md](docs/AUDIO_PROVENANCE.md)를
+ 참고하세요. 실제 음량은 출력 장치에 따라 달라지며 표기 dB는 디지털 상대값입니다.
+
+ 현장음 원본은 `/tmp/findword-ambience-sources`에서만 보관하고 배포하지 않습니다.
+ `python3 scripts/ambience_audio.py`로 검증된 원본의 파생본과 manifest를 재현합니다.
+ PD·CC0·CC BY 4.0·CC BY-SA 4.0의 정확한 출처·저작자·변환·해시는
+ [현장음 manifest](public/ambience/manifest.json), [배포 고지](public/AMBIENCE_NOTICES.md)에 있습니다.
+ 시장 파생본은 CC BY-SA 4.0을 유지합니다. 외부 링크는 출처 확인용이며 앱은 이를 요청하지 않습니다.
+
+ - [변경 이력](CHANGELOG.md)
+ - [v0.2.3 구현·검증 결과](.agent/RESULT-v0.2.3.md)
+ - [설계와 상태 전이](docs/ARCHITECTURE.md)
+ - [Neo를 위한 정적 배포 안내](docs/DEPLOYMENT.md)
+ - [v0.1.1 RED/GREEN 및 실제 검증 결과](.agent/RESULT-v0.1.1.md)
+ - [v0.1 역사 기록](.agent/RESULT.md)
+
+ 이번 작업에서는 서버 배포와 Git commit/push를 실행하지 않습니다.
+
❯ tests/neural-release.test.ts:73:20
71| const doc = readFileSync("README.md", "utf8");
72| expect.soft(doc).toContain(".agent/evidence-v0.2.3/review-fixes/");
73| expect.soft(doc).toContain("문항별 최대 20점, 라운드 합계 최대 30점");
| ^
74| });
75|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[7/8]⎯
Test Files 2 failed (2)
Tests 7 failed | 20 passed (27)
Start at 08:46:34
Duration 7.76s (tests 81%, environment 12%, setup 4%, transform 2%, import 1%)
EXIT_CODE=1