From 33a6f78a56379f2d295a4414966674d83dae6e7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 17:54:40 +0900 Subject: [PATCH 1/4] test: add analysis edge coverage --- .../tests/test_sections_utils.py | 38 ++++++++++ .../analysis-engine/tests/test_segmenter.py | 72 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 services/analysis-engine/tests/test_sections_utils.py diff --git a/services/analysis-engine/tests/test_sections_utils.py b/services/analysis-engine/tests/test_sections_utils.py new file mode 100644 index 00000000..0fa249d1 --- /dev/null +++ b/services/analysis-engine/tests/test_sections_utils.py @@ -0,0 +1,38 @@ +"""Tests for section utility functions.""" + +import logging +from unittest.mock import Mock + +import pytest + +from bandscope_analysis.sections.utils import validate_section + + +@pytest.mark.parametrize( + ("section", "index", "expected_id"), + [ + ({"id": "custom-id", "label": "intro"}, 1, "custom-id"), + ({"label": "intro"}, 2, "section-2"), + ], +) +def test_validate_section_accepts_dict_sections( + section: dict[str, str], index: int, expected_id: str +) -> None: + """Verify valid section dictionaries return stable ids without warnings.""" + mock_logger = Mock(spec=logging.Logger) + + result = validate_section(section, index=index, logger=mock_logger) + + assert result == expected_id + mock_logger.warning.assert_not_called() + + +def test_validate_section_warns_for_invalid_section_type() -> None: + """Verify invalid sections log type context and return a generated id.""" + mock_logger = Mock(spec=logging.Logger) + + result = validate_section(["intro"], index=3, logger=mock_logger) + + assert result == "section-3" + mock_logger.warning.assert_called_once() + assert mock_logger.warning.call_args.args[1:] == (3, "list") diff --git a/services/analysis-engine/tests/test_segmenter.py b/services/analysis-engine/tests/test_segmenter.py index 37fee9f9..81852a95 100644 --- a/services/analysis-engine/tests/test_segmenter.py +++ b/services/analysis-engine/tests/test_segmenter.py @@ -324,3 +324,75 @@ def test_segment_with_boundaries_handles_empty_short_and_failed_inputs() -> None assert "bad combined boundary" in failed_sections[0]["confidence_notes"] assert failed_boundaries == [(0.0, 20.0)] + + +def test_detect_boundaries_ignores_peak_indexes_without_frame_times() -> None: + """Ensure peaks beyond frame_times length are skipped.""" + novelty = np.array([0.0, 0.1, 0.2, 0.9, 0.2, 0.1, 0.0], dtype=np.float64) + frame_times = np.array([0.0, 1.0], dtype=np.float64) + + boundaries = detect_boundaries(novelty, frame_times, 10.0) + + assert boundaries == [0.0] + + +def test_detect_boundaries_ignores_peaks_near_end_of_duration() -> None: + """Ensure boundaries are not created within one second of total duration.""" + novelty = np.array([0.0, 0.1, 0.9, 0.1, 0.9, 0.1, 0.0], dtype=np.float64) + frame_times = np.array([0.0, 2.5, 5.0, 7.5, 9.5, 10.0, 10.5], dtype=np.float64) + + boundaries = detect_boundaries(novelty, frame_times, 10.0) + + assert boundaries == [0.0, 5.0] + + +def test_detect_boundaries_threshold_floor_filters_small_peaks() -> None: + """Ensure the adaptive threshold floor suppresses tiny local maxima.""" + novelty = np.array([0.0, 0.01, 0.09, 0.01, 0.0], dtype=np.float64) + frame_times = np.array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=np.float64) + + boundaries = detect_boundaries(novelty, frame_times, 10.0) + + assert boundaries == [0.0] + + +def test_detect_boundaries_flat_novelty_returns_start_only() -> None: + """Ensure flat novelty does not produce boundaries.""" + novelty = np.ones(10, dtype=np.float64) * 0.5 + frame_times = np.arange(10, dtype=np.float64) + + boundaries = detect_boundaries(novelty, frame_times, 10.0) + + assert boundaries == [0.0] + + +def test_detect_boundaries_skips_candidates_too_close_to_previous_boundary() -> None: + """Ensure candidate boundaries must satisfy the minimum segment length.""" + novelty = np.array([0.0, 0.9, 0.0, 0.9, 0.0], dtype=np.float64) + frame_times = np.array([0.0, 1.0, 3.0, 5.0, 7.0], dtype=np.float64) + + boundaries = detect_boundaries(novelty, frame_times, 10.0, min_segment_seconds=4.0) + + assert boundaries == [0.0, 5.0] + + +def test_detect_boundaries_truncates_to_unique_increasing_boundaries() -> None: + """Ensure truncation preserves ordered unique boundary times.""" + novelty = np.tile(np.array([0.0, 1.0, 0.0], dtype=np.float64), 60) + frame_times = np.arange(len(novelty), dtype=np.float64) + + boundaries = detect_boundaries(novelty, frame_times, 200.0, min_segment_seconds=1.0) + + assert len(boundaries) == 20 + assert boundaries == sorted(set(boundaries)) + assert all(left < right for left, right in zip(boundaries, boundaries[1:], strict=False)) + + +def test_detect_boundaries_accepts_right_edge_peak_when_not_near_duration_end() -> None: + """Ensure the last novelty frame can be used as a boundary.""" + novelty = np.array([0.0, 0.1, 0.1, 0.1, 0.9], dtype=np.float64) + frame_times = np.array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=np.float64) + + boundaries = detect_boundaries(novelty, frame_times, 10.0, min_segment_seconds=2.0) + + assert boundaries == [0.0, 4.0] From 7f93c9519a96235bf07e0cf36ac1d0f121f33272 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 19:13:34 +0900 Subject: [PATCH 2/4] test: harden analysis edge coverage assertions --- services/analysis-engine/tests/test_sections_utils.py | 4 +++- services/analysis-engine/tests/test_segmenter.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/analysis-engine/tests/test_sections_utils.py b/services/analysis-engine/tests/test_sections_utils.py index 0fa249d1..b33628ff 100644 --- a/services/analysis-engine/tests/test_sections_utils.py +++ b/services/analysis-engine/tests/test_sections_utils.py @@ -35,4 +35,6 @@ def test_validate_section_warns_for_invalid_section_type() -> None: assert result == "section-3" mock_logger.warning.assert_called_once() - assert mock_logger.warning.call_args.args[1:] == (3, "list") + warning_context = " ".join(str(value) for value in mock_logger.warning.call_args.args) + assert "3" in warning_context + assert "list" in warning_context diff --git a/services/analysis-engine/tests/test_segmenter.py b/services/analysis-engine/tests/test_segmenter.py index 81852a95..f0284b30 100644 --- a/services/analysis-engine/tests/test_segmenter.py +++ b/services/analysis-engine/tests/test_segmenter.py @@ -385,7 +385,7 @@ def test_detect_boundaries_truncates_to_unique_increasing_boundaries() -> None: assert len(boundaries) == 20 assert boundaries == sorted(set(boundaries)) - assert all(left < right for left, right in zip(boundaries, boundaries[1:], strict=False)) + assert all(boundaries[index] < boundaries[index + 1] for index in range(len(boundaries) - 1)) def test_detect_boundaries_accepts_right_edge_peak_when_not_near_duration_end() -> None: From e8a64ae00ff2640caf5536c37ebaf4399ffd5258 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 15:20:17 +0900 Subject: [PATCH 3/4] 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 35f6935b9c591c3ca9bc39803b49be3b802f9c97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 19:45:56 +0900 Subject: [PATCH 4/4] 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: