Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
0eeec7b
feat(workspace): loop tonight's first section from the map
seonghobae Aug 20, 2026
d329fbc
test(player): require explicit audio and one-shot start authority
seonghobae Aug 20, 2026
aa43a84
fix(player): bind starts to fresh audio-authorized requests
seonghobae Aug 20, 2026
58cf249
test(workspace): bind role loop action to local audio
seonghobae Aug 20, 2026
de00811
fix(workspace): gate role loop by local audio authority
seonghobae Aug 21, 2026
80265c2
test(workspace): reject duplicate section id selection aliasing
seonghobae Aug 21, 2026
28e82d1
fix(workspace): select loop sections by renderer position
seonghobae Aug 21, 2026
1fb2ad1
test(workspace): revoke loop authority fail closed
seonghobae Aug 21, 2026
6675a83
fix(workspace): stop loop when local audio is revoked
seonghobae Aug 21, 2026
9e8f605
test(workspace): reject malformed loop section entries
seonghobae Aug 21, 2026
705b0f7
fix(workspace): validate loop sections before id lookup
seonghobae Aug 21, 2026
65da84c
test(workspace): preserve paused count-in progress
seonghobae Aug 21, 2026
5a839da
fix(workspace): resume paused count-in before looping
seonghobae Aug 21, 2026
eafc121
test(workspace): disable duplicate start during active loop timing
seonghobae Aug 21, 2026
65c73c8
fix(workspace): disable start while loop timing is active
seonghobae Aug 21, 2026
0476b6d
docs: use analysis-engine working directory for Python checks
seonghobae Aug 21, 2026
78527c7
docs(workspace): describe finite non-negative transport guard
seonghobae Aug 21, 2026
ff20581
test(workspace): scope timeline range assertion
seonghobae Aug 21, 2026
affa8da
test(workspace): restore focused App integration coverage
seonghobae Aug 21, 2026
1fc6317
fix(workspace): structure loop selector label fields
seonghobae Aug 21, 2026
6a58bad
test(workspace): preserve live loop across metadata updates
seonghobae Aug 21, 2026
b100551
fix(workspace): preserve live loop on unrelated song updates
seonghobae Aug 21, 2026
646f470
docs: align Python verification working directory
seonghobae Aug 21, 2026
96a191a
test(i18n): name selected-section loop action honestly
seonghobae Aug 22, 2026
b4a5a0d
fix(i18n): name selected-section loop action honestly
seonghobae Aug 22, 2026
3de2b62
fix(i18n): localize selected-section loop action
seonghobae Aug 22, 2026
110ca67
test(workspace): follow selected-section loop copy
seonghobae Aug 22, 2026
ab660b6
test(i18n): keep rehearsal clock copy honest
seonghobae Aug 22, 2026
139e820
fix(i18n): describe timer-only loop honestly
seonghobae Aug 22, 2026
7617f0d
fix(i18n): localize rehearsal clock honestly
seonghobae Aug 22, 2026
c9fb0f6
test(workspace): follow rehearsal clock copy
seonghobae Aug 22, 2026
0512f30
test(player): align loop assertions with shipped next-action copy
seonghobae Aug 23, 2026
f5f753d
test(player): verify revocation after the loop advances
seonghobae Aug 23, 2026
7c1b497
test(player): advance the live-loop clock before revocation
seonghobae Aug 23, 2026
ad2d219
test(workspace): pin loop section descriptor authority
seonghobae Aug 25, 2026
6b8abe5
fix(workspace): snapshot loop section authority
seonghobae Aug 25, 2026
0c7b293
test(workspace): pin loop picker snapshot authority
seonghobae Aug 25, 2026
3d5ac83
fix(workspace): snapshot song loop windows
seonghobae Aug 25, 2026
f420ffe
fix(workspace): render snapshotted loop windows
seonghobae Aug 25, 2026
2a8a96b
docs(changelog): describe map-clock snapshot boundary
seonghobae Aug 25, 2026
d8e0c4f
docs(changelog): preserve historical release text
seonghobae Aug 25, 2026
0a8d316
Merge branch 'develop' into feat/rehearsal-player-first-section-loop
seonghobae Aug 25, 2026
d2f9a2f
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 26, 2026
cfcfa1e
fix(desktop): bound rehearsal loop inputs
seonghobae Aug 28, 2026
6de3f55
fix(a11y): announce rehearsal transport status
seonghobae Aug 29, 2026
7a59047
fix(player): restart externally requested loops
seonghobae Aug 29, 2026
2803109
feat(player): play real audio section loops
seonghobae Aug 29, 2026
c911b41
fix(player): harden cross-platform audio loop boundaries
seonghobae Aug 29, 2026
5187e03
fix(player): cap long loop boundary timers
seonghobae Aug 29, 2026
6a9c576
test(player): reproduce unusable audio authority start
seonghobae Aug 29, 2026
2ff2615
fix(player): require playable media authority
seonghobae Aug 29, 2026
d53758b
test(player): use real media authority in loop tests
seonghobae Aug 29, 2026
8ab7e7d
feat(player): add bounded rehearsal playback rates
seonghobae Aug 29, 2026
9491543
fix(player): share playable audio authority
seonghobae Aug 29, 2026
308c802
Merge real-audio loop authority fix
seonghobae Aug 29, 2026
e517a98
fix(player): align loop timing with playback rate
seonghobae Aug 29, 2026
39927fb
feat(player): filter loops by rehearsal role
seonghobae Aug 29, 2026
43698a6
fix(player): preserve role loop selection
seonghobae Aug 29, 2026
2490ce2
feat(player): add keyboard section cue navigation
seonghobae Aug 29, 2026
04396d9
fix(player): follow focused cue during arrow navigation
seonghobae Aug 29, 2026
9c61a80
feat(player): allow manual cue boundary corrections
seonghobae Aug 29, 2026
8a5160a
feat(player): add bounded cue seeking
seonghobae Aug 29, 2026
81efbe3
fix(player): preserve cue editor focus
seonghobae Aug 29, 2026
c37e589
feat(player): add safe transport shortcuts
seonghobae Aug 29, 2026
dbd15a9
fix(player): ignore repeated transport shortcuts
seonghobae Aug 29, 2026
4d13e56
feat(player): add safe transport shortcuts
seonghobae Aug 30, 2026
b8c17fa
fix(player): scope transport shortcuts safely
seonghobae Aug 30, 2026
c6fb104
feat(player): allow manual cue boundary corrections
seonghobae Aug 30, 2026
e279396
feat(player): filter loops by rehearsal role
seonghobae Aug 30, 2026
aa1c36f
fix(player): ignore modified escape shortcuts
seonghobae Aug 30, 2026
b8a4e1c
fix(player): preserve duplicate cue identity
seonghobae Aug 30, 2026
b96e454
fix(player): preserve cue selection across edits
seonghobae Aug 30, 2026
82ec453
feat(player): play real audio section loops
seonghobae Aug 30, 2026
d92ad78
feat(player): add keyboard section cue navigation
seonghobae Aug 30, 2026
b8b4241
fix(player): preserve count-in timing across rate changes
seonghobae Aug 30, 2026
826b1df
docs: align agent guidance with playback rate
seonghobae Aug 30, 2026
d99c935
test(player): cover repeated count-in rate changes
seonghobae Aug 30, 2026
435cc02
feat(player): add bounded rehearsal playback rates
seonghobae Aug 30, 2026
7c24f59
fix(player): preserve scroll-region Space behavior
seonghobae Aug 30, 2026
b7fd151
Merge latest rehearsal-player base
seonghobae Aug 30, 2026
e29739f
feat(player): add bounded cue seeking
seonghobae Aug 30, 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
20 changes: 18 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# AGENTS.md

## Project overview

- BandScope is a local-first desktop app for rehearsal prep: a practical song view with likely harmony by section and by instrument or vocal role, form and groove cues, stems, playable ranges, simplification guidance, transposition or setup cues, part-overlap cues, visible confidence, and rehearsal priorities.
- The ready workspace must name a next rehearsal action. Tonight's first playable section loop (count-in, pause, stop) is the #961 transport slice; #1063 adds bounded pitch-preserving playback rate, while stem playback remains later work.
- Authoritative delivery rules live in `ARCHITECTURE.md`, `docs/plans/`, and the root verification scripts.
- Brand, tone, UX copy, and prioritization rules live in `docs/brand-story.md` and must be applied to PRDs, TRDs, UI copy, onboarding, empty states, and error messages.
- App security rules live in `docs/security/app-security.md` and must be applied to file handling, URL intake, subprocesses, IPC, WebView usage, model loading, updates, logging, cache handling, and export behavior.
Expand All @@ -12,15 +14,19 @@
- Repository governance and Gitflow rules live in `docs/repository/governance.md`, `docs/repository/bootstrap-plan.md`, and `docs/repository/gitflow.md`.

## Security workflow

- Before writing PRDs, TRDs, UX copy, architecture changes, or implementation plans that touch risky boundaries, read `docs/security/app-security.md`.
- If a task touches files, URLs, subprocesses, ffmpeg or native tools, WebView, local backend or IPC, updates, model downloads, project formats, logs, telemetry, or exports, the result must include `Security Notes`.
- `Security Notes` should cover untrusted inputs, trust boundaries, allowlists or validation, safe failure, logging/privacy impact, and test points.

<!-- BEGIN cwl-agent-guidance -->

## Agent guidance (CWL governance)

This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working in this repo.

### Security & review gate

- Every PR runs a central **Security Scan** required gate: `osv-scan` + `dependency-review` (diff-scoped) and `trivy-fs` (repo-wide, CRITICAL/HIGH, fixable). It runs on every PR base, **including stacked PRs**. Gating is by the Security Scan **job result**.
- A failing `trivy-fs` is a **REAL finding, not a flake.** Read the job log (it prints each finding's rule id / severity / file) or the run's SARIF results, then **remediate**:
- This repo ships **no Dockerfile and no k8s manifests**, so findings are almost always dependency vulns. Bump the offending package in the relevant lockfile — `apps/desktop/src-tauri/Cargo.lock` (Rust/Tauri), `package-lock.json` (Node), or `uv.lock` / `services/analysis-engine` (Python).
Expand All @@ -30,37 +36,45 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- The org `code_scanning` ruleset is intentionally **CodeQL-only** (multiple code-scanning tools can't converge on one PR ref). Do **not** add tools to the `code_scanning` rule; enforcement stays on the Security Scan job.

### Code exploration

- This repo has **no `.codegraph/` index**, so use normal search (grep/find/ripgrep) to locate and understand code. If a `.codegraph/` directory is later added at the repo root, prefer CodeGraph (`codegraph explore "<query>"`, or the code-review-graph MCP tools) **before** grep/find — it surfaces callers/callees/impact that text search misses.

<!-- END cwl-agent-guidance -->

## Supply chain workflow

- Before adding or changing dependencies, GitHub Actions, bundled binaries, or model artifacts, read `docs/security/dependency-policy.md`.
- New direct dependencies must include admission rationale covering purpose, dependency class, alternatives, maintainer trust, license fit, known security issues, transitive footprint, and BandScope release risk.
- Lockfiles, dependency review, audit, SBOM generation, and supplemental component inventory are mandatory and must not be skipped or loosened.
- Supply-chain work is not complete without evidence: workflow paths, required check names for `main` and `develop`, SBOM format and retention, Dependabot baseline state, and the bundled-binary/model inventory path.
- Use `FAILED` when repo-controlled supply-chain artifacts are missing; use `BLOCKED` only when GitHub permission, auth, network, or platform capability prevents enforcement.

## Cross-platform build workflow

- Before changing CI, packaging, release flows, or native desktop build settings, read `docs/security/cross-platform-build-policy.md`.
- Windows and macOS builds are required security controls for `develop`, `main`, and release validation.
- Protected-branch build checks for Windows and macOS must not be removed, downgraded, or treated as optional.

## GitHub bootstrap workflow

- Before declaring a GitHub task blocked, read `docs/workflow/github-bootstrap-execution-policy.md`.
- Missing local git state, missing GitHub repo, missing `main`, missing `develop`, or missing initial workflows are bootstrap conditions, not default blockers.
- For GitHub tasks, only use `BLOCKED` when the failure is caused by missing GitHub permissions, missing auth, missing network access, or platform-level feature limits.
Comment thread
seonghobae marked this conversation as resolved.

## Setup commands

- Node: `npm install`
- Python: `uv sync --project services/analysis-engine --group dev`

## Build / Test commands

- Full harness check: `./scripts/harness/quickcheck.sh`
- Frontend tests: `npm run test --workspaces --if-present`
- Python tests: `uv run --project services/analysis-engine pytest --cov=src/bandscope_analysis --cov-report=term-missing --cov-fail-under=100`
- Typecheck: `npm run typecheck --workspaces --if-present && uv run --project services/analysis-engine mypy src`
- Python tests: `uv run --directory services/analysis-engine pytest --cov=src/bandscope_analysis --cov-report=term-missing --cov-fail-under=100`
- Typecheck: `npm run typecheck --workspaces --if-present && uv run --directory services/analysis-engine mypy src`

## Architecture references

- `ARCHITECTURE.md`
- `docs/engineering/acceptance-criteria.md`
- `docs/engineering/harness-engineering.md`
Expand All @@ -80,13 +94,15 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- `docs/plans/2026-03-10-bandscope-harness.md`

## Code style

- 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 frame usability as a reason to accept weak analysis quality; BandScope should aim for both easy use and high accuracy.

## Safety

- Do not add network-dependent runtime paths for local analysis.
- Treat YouTube import as policy-constrained and fallback-friendly.
- Treat files, URLs, metadata, model artifacts, and project files as untrusted input.
Expand Down
3 changes: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ARCHITECTURE.md

Last updated: 2026-03-11
Last updated: 2026-08-21

## Brand source

Expand Down Expand Up @@ -86,6 +86,7 @@ Last updated: 2026-03-11
- 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
- a local rehearsal transport that arms the first valid section loop, counts in at the admitted tempo, and names the next play/pause/stop action without pretending disk audio is playing when no local song is loaded

## Confidence, edits, and provenance

Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

### Added

- Tonight's rehearsal map now arms the first valid section map-clock loop, runs a tempo count-in, and names the next start, pause, stop, or choose-local-song action without claiming decoded audio playback; admitted section timing and picker copy use the same descriptor-snapshotted transport window.
- 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.
- Move between playable section cues with Left and Right Arrow and keep the selected cue focused.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### Changed
Expand All @@ -14,6 +16,8 @@

### Fixed

- Kept the rehearsal player section picker aligned with the selected player or
vocal role while preserving the full song-form roadmap.
- 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
Expand Down Expand Up @@ -74,4 +78,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 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ npm --workspace @bandscope/desktop exec vitest run src/lib/export.test.ts # on
npm run dev --workspace @bandscope/desktop # Vite dev server (browser fallback mode)
npm run storybook --workspace @bandscope/desktop # component workbench

uv run --project services/analysis-engine pytest tests/test_chords.py # one Python test file (no coverage gate)
uv run --project services/analysis-engine pytest --cov=src/bandscope_analysis --cov-report=term-missing --cov-fail-under=100 # full Python gate
uv run --directory services/analysis-engine pytest tests/test_chords.py # one Python test file (no coverage gate)
uv run --directory services/analysis-engine pytest --cov=src/bandscope_analysis --cov-report=term-missing --cov-fail-under=100 # full Python gate
```

## Architecture
Expand All @@ -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). The workspace `RehearsalPlayer` owns tonight's first section loop and count-in clock; it does not decode local audio in this slice. The ready workspace also 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/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
7 changes: 7 additions & 0 deletions apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ bandscope-desktop-core = { path = "../core" }
rfd = "0.17.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2.11.1", default-features = false, features = ["wry"] }
tauri = { version = "2.11.1", default-features = false, features = ["protocol-asset", "wry"] }
time = { version = "0.3", features = ["formatting", "macros"] }
tokio = { version = "1.50.0", features = ["time"] }
url = "2.5.8"
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,20 @@ fn lookup_bootstrap_source(
.ok_or_else(|| "Analysis job source was not found. Choose local audio again.".to_string())
}

/// Allow only the already-normalized source file to be served by the asset protocol.
///
/// Security Notes: the path is produced by the native file dialog or by the
/// validated, app-owned YouTube cache path. The protocol starts with an empty
/// scope, so this does not expose a directory or accept a path from JavaScript.
fn allow_audio_source_for_playback<R: Runtime>(
app: &tauri::AppHandle<R>,
source: &LocalAudioSourcePayload,
) -> Result<(), String> {
app.asset_protocol_scope()
.allow_file(&source.source_path)
.map_err(|_| "Could not prepare the selected audio for playback.".to_string())
}
Comment on lines +325 to +332

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Playback scope accumulates selected files

allow_file entries persist after switching songs. Review whether previously selected audio must remain available for the entire application session.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


fn drain_analysis_status_updates(
state: &AppState,
app: &tauri::AppHandle<impl Runtime>,
Expand Down Expand Up @@ -643,6 +657,7 @@ fn select_local_audio_source(
.pick_file()
.ok_or_else(|| "Choose a WAV, MP3, FLAC, or M4A file to start analysis.".to_string())?;
let source = normalize_local_audio_source(&path)?;
allow_audio_source_for_playback(&app, &source)?;
let project_id = next_project_id(&state);
let project_root = app_owned_root(&app, "projects", &project_id)?;
let cache_root = app_owned_root(&app, "cache", &project_id)?;
Expand Down Expand Up @@ -712,6 +727,7 @@ async fn import_youtube_url(
if parsed.get("ok").and_then(|v| v.as_bool()) == Some(true) {
if let Some(metadata) = parsed.get("metadata") {
let source = youtube_source_from_metadata(metadata, &cache_root)?;
allow_audio_source_for_playback(&app, &source)?;

let summary = ProjectBootstrapSummaryPayload {
project_id,
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
}
],
"security": {
"csp": "default-src 'self'; img-src 'self' asset: data: blob:; style-src 'self'; script-src 'self'; connect-src 'self' ipc: http://ipc.localhost; media-src 'self' asset: data: blob:; font-src 'self' data:",
"csp": "default-src 'self'; img-src 'self' asset: data: blob:; style-src 'self'; script-src 'self'; connect-src 'self' ipc: http://ipc.localhost; media-src 'self' asset: http://asset.localhost data: blob:; font-src 'self' data:",
"assetProtocol": {
"enable": true,
"scope": []
},
"capabilities": ["main-capability"]
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { render, screen } from "@testing-library/react";
import { createDemoRehearsalSong } from "@bandscope/shared-types";
import { afterEach, describe, expect, it, vi } from "vitest";
import { RehearsalPlayer } from "./RehearsalPlayer";

const originalTauriInternals = Object.getOwnPropertyDescriptor(
window,
"__TAURI_INTERNALS__",
);

describe("RehearsalPlayer audio authority", () => {
afterEach(() => {
vi.restoreAllMocks();
if (originalTauriInternals) {
Object.defineProperty(window, "__TAURI_INTERNALS__", originalTauriInternals);
} else {
delete (window as Window & { __TAURI_INTERNALS__?: unknown })
.__TAURI_INTERNALS__;
}
});

it("refuses to start when local-audio metadata has no playable asset URL", () => {
const song = createDemoRehearsalSong();

render(
<RehearsalPlayer
song={song}
hasLocalAudio={true}
audioSourcePath="browser://selected-audio"
startNonce={1}
/>,
);

expect(
screen.getByRole("button", { name: /start the count-in/i }),
).toBeDisabled();
expect(
screen.getByTestId("rehearsal-loop-next-action").textContent,
).not.toMatch(/count in 4 beats/i);
});

it("reports native asset conversion failures instead of presenting missing-audio copy", () => {
Object.defineProperty(window, "__TAURI_INTERNALS__", {
configurable: true,
value: {
convertFileSrc: () => {
throw new Error("asset conversion failed");
},
},
});
const song = createDemoRehearsalSong();

render(
<RehearsalPlayer
song={song}
hasLocalAudio={true}
audioSourcePath="/Users/test/Music/late-night-set.wav"
/>,
);

expect(
screen.getByRole("button", { name: /start the count-in/i }),
).toBeDisabled();
expect(screen.getByRole("alert").textContent).toMatch(
/could not play this local audio/i,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { render, screen } from "@testing-library/react";
import { createDemoRehearsalSong } from "@bandscope/shared-types";
import { describe, expect, it } from "vitest";
import { RehearsalPlayer } from "./RehearsalPlayer";

describe("RehearsalPlayer descriptor authority", () => {
it("renders the admitted section snapshot instead of Proxy get values", () => {
const song = createDemoRehearsalSong();
const section = song.sections[0]!;
const expectedLabel = section.label;
const expectedStart = section.timeRange.start;
const expectedEnd = section.timeRange.end;
song.sections = [
new Proxy(section, {
get(target, property, receiver) {
if (property === "id") {
return "proxy-injected-section";
}
if (property === "label") {
return "outro";
}
if (property === "timeRange") {
return { start: 90, end: 100 };
}
return Reflect.get(target, property, receiver);
}
})
];

render(<RehearsalPlayer song={song} hasLocalAudio={true} />);

expect(
screen.getByRole("button", {
name: new RegExp(`${expectedLabel}.*0:${String(expectedStart).padStart(2, "0")}.*0:${String(expectedEnd).padStart(2, "0")}`, "i")
})
).toBeTruthy();
expect(screen.queryByRole("button", { name: /outro.*1:30.*1:40/i })).toBeNull();
});
});
Loading
Loading