From 42989c210138fa8bfd3e09d7b78df15ac9f83b95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 18:16:24 +0900 Subject: [PATCH 1/5] test: cover role extractor helper fallbacks --- services/analysis-engine/tests/test_roles.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/services/analysis-engine/tests/test_roles.py b/services/analysis-engine/tests/test_roles.py index 070ca69f..e387952d 100644 --- a/services/analysis-engine/tests/test_roles.py +++ b/services/analysis-engine/tests/test_roles.py @@ -4,7 +4,7 @@ import numpy as np -from bandscope_analysis.roles.extractor import RoleExtractor +from bandscope_analysis.roles.extractor import RoleExtractor, _most_common_chord from bandscope_analysis.roles.model import ( CueAnchorKind, RehearsalPriority, @@ -86,6 +86,22 @@ def test_role_extractor_basic() -> None: assert verse_graph[0]["handoff_to"] == [] +def test_most_common_chord_prefers_first_chord_on_ties() -> None: + """Ensure equal chord counts keep the first chord encountered.""" + assert _most_common_chord(["C", "C", "G"]) == "C" + assert _most_common_chord(["C", "G", "G", "C", "G"]) == "G" + assert _most_common_chord(["A", "B", "A", "B"]) == "A" + assert _most_common_chord(["B", "A", "B", "A"]) == "B" + assert _most_common_chord(["Am"]) == "Am" + + +def test_extract_without_audio_features_sets_extraction_notes() -> None: + """Ensure extraction without audio features still reports fallback notes.""" + result = RoleExtractor().extract([{"id": "intro"}]) + + assert result["extraction_notes"] == "Extracted roles and computed handoffs." + + def test_role_extractor_empty() -> None: """Test extractor with empty sections list.""" extractor = RoleExtractor() From 92ba08dd407d3eb38daf6f81ebc3a06de42c749f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 19:49:57 +0900 Subject: [PATCH 2/5] test: exercise role chord fallback via public API --- services/analysis-engine/tests/test_roles.py | 44 +++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/services/analysis-engine/tests/test_roles.py b/services/analysis-engine/tests/test_roles.py index e387952d..ee1fd532 100644 --- a/services/analysis-engine/tests/test_roles.py +++ b/services/analysis-engine/tests/test_roles.py @@ -4,7 +4,7 @@ import numpy as np -from bandscope_analysis.roles.extractor import RoleExtractor, _most_common_chord +from bandscope_analysis.roles.extractor import RoleExtractor from bandscope_analysis.roles.model import ( CueAnchorKind, RehearsalPriority, @@ -86,20 +86,44 @@ def test_role_extractor_basic() -> None: assert verse_graph[0]["handoff_to"] == [] -def test_most_common_chord_prefers_first_chord_on_ties() -> None: - """Ensure equal chord counts keep the first chord encountered.""" - assert _most_common_chord(["C", "C", "G"]) == "C" - assert _most_common_chord(["C", "G", "G", "C", "G"]) == "G" - assert _most_common_chord(["A", "B", "A", "B"]) == "A" - assert _most_common_chord(["B", "A", "B", "A"]) == "B" - assert _most_common_chord(["Am"]) == "Am" +def test_role_extractor_prefers_first_detected_chord_on_ties() -> None: + """Ensure public role extraction keeps the first detected chord on ties.""" + audio_features = { + "stems": { + "bass": np.ones(100, dtype=np.float32), + "other": np.ones(100, dtype=np.float32), + }, + "sr": 10, + } + + with ( + patch("bandscope_analysis.chords.chord_recognizer.ChordRecognizer") as recognizer_cls, + patch("bandscope_analysis.ranges.pitch_tracker.PitchTracker") as tracker_cls, + ): + tracker_cls.return_value.track.return_value = { + "lowest_note": "C2", + "highest_note": "C3", + } + recognizer_cls.return_value.recognize.side_effect = [ + [{"chord": "A"}, {"chord": "B"}, {"chord": "A"}, {"chord": "B"}], + [{"chord": "D"}, {"chord": "C"}, {"chord": "C"}], + ] + + result = RoleExtractor().extract([{"id": "intro"}], audio_features) + + roles_by_id = { + role["id"]: role + for role in result["topologies"][0]["active_roles"] + } + assert roles_by_id["bass-guitar"]["harmony"]["chord"] == "A" + assert roles_by_id["lead-vocal"]["harmony"]["chord"] == "C" def test_extract_without_audio_features_sets_extraction_notes() -> None: - """Ensure extraction without audio features still reports fallback notes.""" + """Ensure extraction without audio features still reports computed handoff notes.""" result = RoleExtractor().extract([{"id": "intro"}]) - assert result["extraction_notes"] == "Extracted roles and computed handoffs." + assert "computed handoffs" in result["extraction_notes"] def test_role_extractor_empty() -> None: From 35dc52a5dd4c90ae509e708470ec45ebad13ad2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 15:20:17 +0900 Subject: [PATCH 3/5] fix: update anyhow for RustSec 2026-0190 --- apps/desktop/src-tauri/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 4d9ae737..0df254ea 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "atk" From 076256c5c172e0a47a1523413c030411728e3387 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 19:45:56 +0900 Subject: [PATCH 4/5] fix: document quick-xml advisory exceptions --- apps/desktop/src-tauri/.cargo/audit.toml | 2 ++ apps/desktop/src-tauri/osv-scanner.toml | 8 ++++++++ docs/security/dependency-policy.md | 1 + 3 files changed, 11 insertions(+) diff --git a/apps/desktop/src-tauri/.cargo/audit.toml b/apps/desktop/src-tauri/.cargo/audit.toml index 9fc2a4f3..861e0aa5 100644 --- a/apps/desktop/src-tauri/.cargo/audit.toml +++ b/apps/desktop/src-tauri/.cargo/audit.toml @@ -17,4 +17,6 @@ ignore = [ "RUSTSEC-2025-0100", # unic-ucd-ident: unmaintained "RUSTSEC-2025-0098", # unic-ucd-version: unmaintained "RUSTSEC-2024-0429", # glib 0.18.5: VariantStrIter unsoundness, transitive via Tauri/wry/webkit2gtk/gtk GTK3 stack; remove when upstream drops or patches the chain + "RUSTSEC-2026-0194", # quick-xml 0.39.4: inherited via Tauri/plist and rfd/wayland-scanner; no compatible upstream release has moved both chains to quick-xml >=0.41.0 yet + "RUSTSEC-2026-0195", # quick-xml 0.39.4: same owner chain and removal condition as RUSTSEC-2026-0194 ] diff --git a/apps/desktop/src-tauri/osv-scanner.toml b/apps/desktop/src-tauri/osv-scanner.toml index 16b3b20e..c8fc5e44 100644 --- a/apps/desktop/src-tauri/osv-scanner.toml +++ b/apps/desktop/src-tauri/osv-scanner.toml @@ -65,3 +65,11 @@ reason = "Inherited through the current Tauri GTK3 owner chain and already track [[IgnoredVulns]] id = "RUSTSEC-2024-0429" reason = "glib 0.18.5 VariantStrIter advisory inherited through Tauri/wry/webkit2gtk/gtk; allowed only until upstream drops or patches the chain, with scope guarded by scripts/checks/verify_supply_chain.py." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0194" +reason = "quick-xml 0.39.4 duplicate-attribute advisory is inherited through Tauri/plist and rfd/wayland-scanner; current compatible upstream crates do not yet allow quick-xml >=0.41.0, and this app does not expose those XML parser paths to untrusted user XML." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0195" +reason = "quick-xml 0.39.4 namespace-allocation advisory is inherited through the same Tauri/plist and rfd/wayland-scanner owner chain as RUSTSEC-2026-0194; remove once compatible upstream crates move to quick-xml >=0.41.0." diff --git a/docs/security/dependency-policy.md b/docs/security/dependency-policy.md index d3a9680e..d7c7acad 100644 --- a/docs/security/dependency-policy.md +++ b/docs/security/dependency-policy.md @@ -104,6 +104,7 @@ Current controlled exceptions: - No Python vulnerability exceptions are active. `GHSA-5239-wwwm-4pmq` (`Pygments <2.20.0`) was removed by locking `Pygments` to `2.20.0`; the CI `security-audit` workflow must run `pip-audit --local --strict` against the synced `uv` environment without a targeted ignore for that advisory. - Cargo audit warnings for legacy `gtk3` vulnerabilities (e.g. `RUSTSEC-2024-0413`) inherited through Tauri v2 `wry`/`webkit2gtk` integration are explicitly allowed. These are deep framework dependencies with no alternative, so they are documented exceptions and ignored by default. - `RUSTSEC-2024-0429` for `glib 0.18.5` is allowed only for the `VariantStrIter` advisory inherited through the Tauri/wry/webkit2gtk/gtk GTK3 stack. A compatible lockfile refresh can move the desktop stack to `tauri 2.11.3`, `wry 0.55.1`, `tao 0.35.3`, `muda 0.19.3`, and related transitive patches, but it still does not move this stack to patched `glib >=0.20.0`; the exception must remain encoded in repo-controlled audit configuration and guarded by `scripts/checks/verify_supply_chain.py`, and it must be removed when upstream drops or patches the chain. +- `RUSTSEC-2026-0194` and `RUSTSEC-2026-0195` for `quick-xml 0.39.4` are allowed only while the current compatible upstream owner chains still require vulnerable `quick-xml`: `plist 1.9.0` through Tauri, and `wayland-scanner 0.31.10` through Linux `rfd`/Wayland dependencies. `quick-xml >=0.41.0` is patched, but `plist 1.9.0` requires `quick-xml ^0.39.2` and the current `wayland-scanner` release also has no compatible patched path. BandScope does not expose either owner chain as a user-controlled XML ingestion surface; the exception must stay encoded in repo-controlled cargo-audit and OSV configuration, and must be removed once compatible upstream crates publish a patched dependency path. Retired third-party deprecation and advisory signal: From ac44e95383e43d3b0d4ddf829c63baf665da337d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 3 Jul 2026 00:42:07 +0900 Subject: [PATCH 5/5] test: format role extractor helper coverage --- services/analysis-engine/tests/test_roles.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/analysis-engine/tests/test_roles.py b/services/analysis-engine/tests/test_roles.py index ee1fd532..6e2916d3 100644 --- a/services/analysis-engine/tests/test_roles.py +++ b/services/analysis-engine/tests/test_roles.py @@ -111,10 +111,7 @@ def test_role_extractor_prefers_first_detected_chord_on_ties() -> None: result = RoleExtractor().extract([{"id": "intro"}], audio_features) - roles_by_id = { - role["id"]: role - for role in result["topologies"][0]["active_roles"] - } + roles_by_id = {role["id"]: role for role in result["topologies"][0]["active_roles"]} assert roles_by_id["bass-guitar"]["harmony"]["chord"] == "A" assert roles_by_id["lead-vocal"]["harmony"]["chord"] == "C"