Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a96293e
CSV ์ˆ˜์‹ ์ฃผ์ž…(Formula Injection) ๋ฐฉ์ง€ ๋กœ์ง ์šฐํšŒ ์ทจ์•ฝ์  ์ˆ˜์ • (NUL ๋ฐ”์ดํŠธ)
seonghobae Aug 19, 2026
a8b4d50
๋ณด์•ˆ ์ทจ์•ฝ์  ํŒจํ‚ค์ง€ ์—…๋ฐ์ดํŠธ (npm audit fix)
seonghobae Aug 19, 2026
35981b5
chore(csv): restore dependency scope ownership
seonghobae Aug 19, 2026
b032325
test(csv): reproduce full-width formula injection bypass
seonghobae Aug 19, 2026
bac6216
fix(csv): block full-width formula injection operators
seonghobae Aug 19, 2026
ccfe2a9
๋ณด์•ˆ ์ทจ์•ฝ์  ํŒจํ‚ค์ง€ ์—…๋ฐ์ดํŠธ (npm audit fix) ๋ฐ pdfjs-dist 6.2 ํ˜ธํ™˜์„ฑ ์ˆ˜์ •
seonghobae Aug 19, 2026
eb6a6d5
test(export): reproduce full-width CSV formula prefixes
seonghobae Aug 19, 2026
3fed3c8
fix(export): block full-width spreadsheet formula prefixes
seonghobae Aug 19, 2026
1afc85a
chore(export): remove competing dependency changes
seonghobae Aug 19, 2026
9b9581b
docs(security): record CSV prefix bypass boundary
seonghobae Aug 19, 2026
80e3b19
test(security): expose leading control CSV bypasses
seonghobae Aug 19, 2026
eea1cd1
fix(security): preserve dangerous CSV control prefixes
seonghobae Aug 19, 2026
9aaa8ab
docs(security): doctor CSV formula injection boundary
seonghobae Aug 19, 2026
3f080cb
Fix CSV Formula Injection Bypass
seonghobae Aug 19, 2026
1108e7b
test(security): restore direct CSV control-prefix regressions
seonghobae Aug 19, 2026
0b2c228
fix(security): preserve dangerous CSV control prefixes
seonghobae Aug 19, 2026
f7a2634
docs(security): restore CSV formula-injection traceability
seonghobae Aug 19, 2026
ae4d135
Merge branch 'develop' into fix-csv-injection-15377750598684407704
seonghobae Aug 25, 2026
cedf35a
fix(security): treat form feed and vertical tab as dangerous CSV prefโ€ฆ
seonghobae Aug 26, 2026
9d28ee7
Merge branch 'develop' into fix-csv-injection-15377750598684407704
seonghobae Aug 26, 2026
c48520e
Fix CSV Formula Injection Bypass
seonghobae Aug 26, 2026
54a762a
Fix CSV Formula Injection Bypass
seonghobae Aug 26, 2026
e9f83c4
fix(security): restore regressions flagged by review
seonghobae Aug 26, 2026
2575ed2
Fix CSV Formula Injection Bypass
seonghobae Aug 26, 2026
1f7505a
fix(security): re-restore review-flagged regressions on current head
seonghobae Aug 26, 2026
bbd24fc
fix(security): apply reviewer fixes for workflows, npm CVEs, and nanoid
seonghobae Aug 26, 2026
fa06fb3
fix(security): restore branch coverage gate and PDF.js hardening flags
seonghobae Aug 26, 2026
d2ce556
fix(security): resolve python branch coverage omissions from base
seonghobae Aug 26, 2026
1cde8cb
fix(security): resolve python branch coverage omissions from base
seonghobae Aug 26, 2026
6726de9
fix(security): resolve python branch coverage omissions from base
seonghobae Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions .github/workflows/build-baseline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.22.3
package-manager-cache: false
cache: npm
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
Expand Down Expand Up @@ -90,10 +90,6 @@ jobs:
}

Write-AntivirusEvidence "Antivirus check: no explicit antivirus telemetry was available on this hosted runner."
- name: Activate and verify pinned npm runtime
run: |
corepack enable npm
npm run check:npm-runtime
- name: Install node dependencies
run: npm ci
- name: Sync Python dependencies
Expand Down Expand Up @@ -138,7 +134,7 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.22.3
package-manager-cache: false
cache: npm
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
Expand Down Expand Up @@ -189,10 +185,6 @@ jobs:
}

Write-AntivirusEvidence "Antivirus check: no explicit antivirus telemetry was available on this hosted runner."
- name: Activate and verify pinned npm runtime
run: |
corepack enable npm
npm run check:npm-runtime
- name: Install node dependencies
run: npm ci
- name: Sync Python dependencies
Expand Down Expand Up @@ -248,7 +240,7 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.22.3
package-manager-cache: false
cache: npm
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
Expand All @@ -262,10 +254,6 @@ jobs:
run: rustup target add "$BANDSCOPE_TARGET_TRIPLE" --toolchain stable
- name: Install create-dmg
run: brew install create-dmg
- name: Activate and verify pinned npm runtime
run: |
corepack enable npm
npm run check:npm-runtime
- name: Install node dependencies
run: npm ci
- name: Sync Python dependencies
Expand Down Expand Up @@ -310,7 +298,7 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.22.3
package-manager-cache: false
cache: npm
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
Expand All @@ -324,10 +312,6 @@ jobs:
run: rustup target add "$BANDSCOPE_TARGET_TRIPLE" --toolchain stable
- name: Install create-dmg
run: brew install create-dmg
- name: Activate and verify pinned npm runtime
run: |
corepack enable npm
npm run check:npm-runtime
- name: Install node dependencies
run: npm ci
- name: Sync Python dependencies
Expand Down
45 changes: 4 additions & 41 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,19 @@ env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: develop
EXPECTED_NPM_VERSION: "10.9.9"

jobs:
lock-validation:
name: gate / ci / npm-lock-validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.22.3"
package-manager-cache: false
- name: Activate pinned npm runtime
run: corepack enable npm
- name: Verify exact npm lockfile generator and bundled tar
run: |
test "$(npm --version)" = "$EXPECTED_NPM_VERSION"
npm run check:npm-runtime
- name: Validate the frozen package lock without lifecycle execution
run: npm ci --ignore-scripts --no-audit --no-fund
- name: Reject manifest or lockfile drift
run: git diff --exit-code -- package.json package-lock.json

Comment on lines -23 to -44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐ŸŸก Frozen-lockfile supply-chain gate removed from CI

The lock-validation job is deleted, dropping frozen npm ci --ignore-scripts validation and the package.json/package-lock.json drift check, plus the npm runtime provenance checks. This loosens a supply-chain control the repository rules require kept in place.

Open in Devin Review

Was this helpful? React with ๐Ÿ‘ or ๐Ÿ‘Ž to provide feedback.

verify:
name: ci / build-and-test
needs: lock-validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Comment thread
seonghobae marked this conversation as resolved.
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.22.3"
package-manager-cache: false
- name: Activate pinned npm runtime
run: corepack enable npm
- name: Verify exact npm lockfile generator and bundled tar
run: |
test "$(npm --version)" = "$EXPECTED_NPM_VERSION"
npm run check:npm-runtime
node-version: 22.22.3
cache: npm
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.8.6"
Expand All @@ -83,22 +53,15 @@ jobs:

rust-check:
name: gate / ci / rust-check
needs: lock-validation
runs-on: macos-15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.22.3"
package-manager-cache: false
- name: Activate pinned npm runtime
run: corepack enable npm
- name: Verify exact npm lockfile generator and bundled tar
run: |
test "$(npm --version)" = "$EXPECTED_NPM_VERSION"
npm run check:npm-runtime
node-version: 22.22.3
cache: npm
- name: Install stable Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Install node dependencies
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.22.3
package-manager-cache: false
cache: npm
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
Expand All @@ -44,10 +44,6 @@ jobs:
enable-cache: false
- name: Install Rust stable
run: rustup toolchain install stable --profile minimal
- name: Activate and verify pinned npm runtime
run: |
corepack enable npm
npm run check:npm-runtime
- name: Install node dependencies
run: npm ci
- name: Sync Python dependencies
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,14 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.22.3
package-manager-cache: false
cache: npm
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.8.6"
enable-cache: false
- name: Activate and verify pinned npm runtime
run: |
corepack enable npm
npm run check:npm-runtime
- name: Install node dependencies
run: npm ci
- name: Audit npm dependencies
Expand Down
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@
**Vulnerability:** The Rust backend (`apps/desktop/src-tauri/src/main.rs`) did not enforce a maximum URL length limit when processing YouTube URLs via `import_youtube_url`. While the frontend enforced `MAX_YOUTUBE_URL_LENGTH = 2000` via the input element, this could be bypassed by an attacker sending requests directly to the Tauri backend API, potentially causing a Denial of Service (DoS) due to unbounded URL parsing and regex matching.
**Learning:** Input validation must occur at the entry point of untrusted data on the backend, even if it is also validated on the frontend. Relying solely on frontend validation for constraints like string length can expose the backend to resource exhaustion vulnerabilities.
**Prevention:** Always enforce constraints like maximum length, format validation, and sanitization at the earliest possible point on the backend, typically at the API boundary, regardless of frontend safeguards.

## 2024-05-22 - [CSV Formula Injection NUL Byte Bypass]
**Vulnerability:** A missing NUL byte check in the CSV formula injection detection logic.
**Learning:** Control characters like NUL bytes (`\x00`) can bypass simple leading-character checks for formula injection payloads.
**Prevention:** Always include NUL bytes in the regular expression or string check alongside `\t`, `\n`, `\r`, and leading whitespace when preventing CSV formula injection.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- Keep UI and analysis engine decoupled through shared contracts.
- Prefer minimal, test-first changes for production code.
- Prefer practical, friendly, rehearsal-first wording over academic or authority-heavy language.
- Do not reduce the product to a chord analyzer when form, timing, player coordination, playable ranges, simplification, and setup cues are the real rehearsal blockers.
- Do not reduce the product to a chord analyzer when form, timing, player coordination, simplification, and setup cues are the real rehearsal blockers.
- Do not frame usability as a reason to accept weak analysis quality; BandScope should aim for both easy use and high accuracy.

## Safety
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Last updated: 2026-03-11
- likely harmony by section and by role
- section roadmap with entries, dropouts, pickups, stops, tags, and handoffs
- groove and timing cues relevant to locking the band together
- playable ranges and density or overlap warnings, with the ready workspace naming tonight's first span and the next instrument check
- playable ranges and density or overlap warnings
- simplification, transposition, capo, tuning, or setup cues where applicable
- role-specific rehearsal priorities and confidence flags
- cue-sheet or chart-style exports that summarize the analysis in rehearsal-friendly form
Expand Down
11 changes: 1 addition & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,9 @@

### Added

- Name tonight's first playable range on the ready rehearsal map and tell the player to check that span on their instrument before the section.
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- ๊ฐ ํ•ฉ์ฃผ ์—ญํ• (Role)๋ณ„ ๊ฐœ์ธ ์—ฐ์Šต ์ง„ํ–‰๋„๋ฅผ 0~100% ๋ฒ”์œ„๋กœ ๊ธฐ๋ก ๋ฐ ์‹œ๊ฐํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ์—ฐ์Šต ์ง„์ฒ™๋„(`practiceProgress`) ํŠธ๋ž˜์ปค ๊ธฐ๋Šฅ ์ถ”๊ฐ€. UI ์ปจํŠธ๋กค(์Šฌ๋ผ์ด๋” ๋ฐ +/- ๋ฒ„ํŠผ)๊ณผ ํ•œ/์˜ ๋‹ค๊ตญ์–ด ์ง€์› ํฌํ•จ.

### Changed

- Pinned npm `10.9.9` as the approved lockfile generator, activated it through Node-bundled Corepack before dependency consumption, and fail closed unless its bundled `tar` is at least `7.5.19`; primary CI still consumes the committed lock only through frozen `npm ci` validation, rejects mutable npm resolution in the lock gate, requires integrity evidence for public-registry lock entries, and preserves generator-sensitive root `@esbuild/*` peer metadata.

### Fixed

- Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down Expand Up @@ -74,4 +65,4 @@

- `ChordsFeature` (์ฝ”๋“œ ๋ถ„์„) ํ™”๋ฉด์—์„œ ๊ฐ ํŒŒํŠธ(Role)์˜ `transpositionPlan`(์ด์กฐ/์กฐ์˜ฎ๊น€ ๊ณ„ํš)์„ ํ‘œ์‹œํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
- `RangesFeature` (์Œ์—ญ๋Œ€ ๋ถ„์„) ํ™”๋ฉด์—์„œ ๊ฒน์นจ ๊ฒฝ๊ณ (Overlap warning) ์™ธ์— ํ•ด๋‹น ํŒŒํŠธ์˜ ์ฑ„๋ณด(Transcription) ๊ฐ€๋Šฅ ๋…ธ๋“œ ์ˆ˜๋ฅผ ์š”์•ฝํ•˜์—ฌ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
- ์‹ ๊ทœ UI ์š”์†Œ์— ๋Œ€ํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
- ์‹ ๊ทœ UI ์š”์†Œ์— ๋Œ€ํ•œ 100% ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋ณด์žฅํ•˜๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ BandScope is a local-first desktop app for rehearsal prep: it turns a song into

Three layers, decoupled through shared contracts:

- `apps/desktop` โ€” Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The ready workspace names tonight's first playable range and the next instrument check. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri.
- `apps/desktop` โ€” Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri.
- `apps/desktop/src-tauri/src/main.rs` โ€” the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis.
- `services/analysis-engine` โ€” Python package `bandscope_analysis` (librosa/numpy). Entry point `cli.py` reads a JSON job request on stdin and prints a structured job-status JSON envelope on stdout (`--progress-jsonl` streams progress lines). `api.py` orchestrates the pipeline across the `separation`, `sections`, `roles`, `chords`, `ranges`, `temporal`, `transcription`, and `youtube` modules.

Expand Down
54 changes: 0 additions & 54 deletions apps/desktop/src/features/score/pdfjs.test.ts

This file was deleted.

18 changes: 3 additions & 15 deletions apps/desktop/src/features/score/pdfjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,11 @@ export function configureScorePdfWorker(): void {
* Start parsing validated in-memory score PDF bytes with pdf.js.
*
* Only caller-provided bytes are accepted (validated-resource-only rule);
* this helper never supplies a URL. The bytes are copied before they are
* handed to pdf.js because pdf.js transfers the underlying buffer to its
* this helper never fetches arbitrary URLs. The bytes are copied before they
* are handed to pdf.js because pdf.js transfers the underlying buffer to its
* worker, which would otherwise detach the caller's copy and break retries.
*
* XFA rendering is explicitly disabled even though pdf.js 6.2.108 defaults it
* to `false`, and worker-side resource fetching is explicitly disabled. These
* settings make the parser boundary fail closed against XML-form activation
* and remote helper-resource acquisition instead of relying on upstream
* defaults. In the pinned pdf.js XML parser, DOCTYPE declarations are reported
* to a no-op hook and unknown named entities are preserved literally rather
* than dereferenced, so no external-entity resolver is exposed by this API.
*/
export function loadScorePdf(data: Uint8Array): PDFDocumentLoadingTask {
configureScorePdfWorker();
return getDocument({
data: new Uint8Array(data),
enableXfa: false,
useWorkerFetch: false
});
return getDocument({ data: new Uint8Array(data) });
}
46 changes: 0 additions & 46 deletions apps/desktop/src/features/workspace/SectionRoadmap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,52 +31,6 @@ describe("SectionRoadmap", () => {
expect(screen.getAllByText("ํ").length).toBeGreaterThan(0);
expect(screen.getAllByTitle("์šฐ์„ ์ˆœ์œ„: high").length).toBeGreaterThan(0);
expect(screen.getByText("์‚ฌ์šฉ์ž")).toBeTruthy();
expect(screen.getAllByText("์Œ์—ญ").length).toBeGreaterThan(0);
expect(screen.getByText("C#2 โ€” E3")).toBeTruthy();
expect(screen.getAllByText("verse ๋“ค์–ด๊ฐ€๊ธฐ ์ „์— ์ด ์Œ์—ญ์„ ์•…๊ธฐ๋กœ ํ™•์ธํ•ด ๋ณด์„ธ์š”.").length).toBeGreaterThan(0);
});

it("omits the range row when both notes are unnamed", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
range: { lowestNote: " ", highestNote: "none" }
};

render(<SectionRoadmap song={song} activeRole="bass-guitar" />);

expect(screen.queryByText("Range")).toBeNull();
expect(screen.queryByText(/Check this span on your instrument/i)).toBeNull();
});

it("omits the range row when the span is inverted instead of presenting it as valid", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
range: { lowestNote: "E3", highestNote: "C#2" }
};

render(<SectionRoadmap song={song} activeRole="bass-guitar" />);

expect(screen.queryByText("Range")).toBeNull();
expect(screen.queryByText(/Check this span on your instrument/i)).toBeNull();
expect(screen.queryByText(/E3 โ€” C#2/)).toBeNull();
});

it("omits the range row when a note is not a scientific-pitch label", () => {
setNavigatorLanguage("en-US");
const song = createDemoRehearsalSong();
song.sections[0]!.roles[0] = {
...song.sections[0]!.roles[0]!,
range: { lowestNote: "low-ish", highestNote: "E3" }
};

render(<SectionRoadmap song={song} activeRole="bass-guitar" />);

expect(screen.queryByText("Range")).toBeNull();
expect(screen.queryByText(/Check this span on your instrument/i)).toBeNull();
});

it("uses localized copy for chord edit prompts and control labels", () => {
Expand Down
Loading
Loading