From 664686c414ee072d9657d306f8c8ac18e3dc2aab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 18:20:41 +0900 Subject: [PATCH 1/5] test: expand section roadmap interactions --- .../workspace/SectionRoadmap.test.tsx | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx b/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx index 75a19924..bd7f11d0 100644 --- a/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx +++ b/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx @@ -16,6 +16,7 @@ describe("SectionRoadmap", () => { afterEach(() => { setNavigatorLanguage(originalLanguage); vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); it("localizes roadmap controls and provenance badges", () => { @@ -60,4 +61,98 @@ describe("SectionRoadmap", () => { expect(onSongUpdate).not.toHaveBeenCalled(); }); + + it("cancels chord editing without updating the song", () => { + setNavigatorLanguage("ko-KR"); + const song = createDemoRehearsalSong(); + const onSongUpdate = vi.fn(); + const promptSpy = vi.spyOn(window, "prompt").mockReturnValue(null); + + render(); + + fireEvent.click(screen.getByRole("button", { name: "Bass Guitar의 verse 코드 수정, 현재 C#m7" })); + + expect(promptSpy).toHaveBeenCalledWith("새 코드 입력:", "C#m7"); + expect(onSongUpdate).not.toHaveBeenCalled(); + }); + + it("disables chord editing when no update handler is provided", () => { + setNavigatorLanguage("ko-KR"); + const song = createDemoRehearsalSong(); + + render(); + + expect(screen.getByRole("button", { name: "Bass Guitar의 verse 코드 수정, 현재 C#m7" })).toBeDisabled(); + }); + + it("renders priority indicators for high, medium, and low roles", () => { + setNavigatorLanguage("ko-KR"); + const song = createDemoRehearsalSong(); + + song.sections[0].roles.push({ + ...song.sections[0].roles[0], + id: "medium-priority-role", + name: "Medium Role", + rehearsalPriority: "medium" + }); + song.sections[0].roles.push({ + ...song.sections[0].roles[0], + id: "low-priority-role", + name: "Low Role", + rehearsalPriority: "low" + }); + + render(); + + expect(screen.getAllByTitle("우선순위: high").length).toBeGreaterThan(0); + expect(screen.getAllByTitle("우선순위: medium").length).toBeGreaterThan(0); + expect(screen.getAllByTitle("우선순위: low").length).toBeGreaterThan(0); + }); + + it("renders low confidence badges for low-confidence sections and roles", () => { + setNavigatorLanguage("ko-KR"); + const song = createDemoRehearsalSong(); + song.sections[0].confidence.level = "low"; + song.sections[0].roles[0].confidence.level = "low"; + + render(); + + expect(screen.getAllByText("확신이 낮음").length).toBeGreaterThan(0); + }); + + it("filters the roadmap to the active role", () => { + setNavigatorLanguage("ko-KR"); + const song = createDemoRehearsalSong(); + + render(); + + expect(screen.getByText("Bass Guitar")).toBeTruthy(); + expect(screen.queryByText("Keyboard 1 Right Hand")).toBeNull(); + }); + + it("does not update when the entered chord is empty or whitespace", () => { + setNavigatorLanguage("ko-KR"); + const song = createDemoRehearsalSong(); + const onSongUpdate = vi.fn(); + const promptSpy = vi.spyOn(window, "prompt"); + + promptSpy.mockReturnValueOnce("").mockReturnValueOnce(" "); + render(); + + const editButton = screen.getByRole("button", { name: "Bass Guitar의 verse 코드 수정, 현재 C#m7" }); + fireEvent.click(editButton); + fireEvent.click(editButton); + + expect(promptSpy).toHaveBeenCalledTimes(2); + expect(onSongUpdate).not.toHaveBeenCalled(); + }); + + it("renders overlap warnings", () => { + setNavigatorLanguage("ko-KR"); + const song = createDemoRehearsalSong(); + + render(); + + expect(screen.getByText("Density warning: competing with Keyboard Left Hand in low register.")).toBeTruthy(); + }); }); From 135f34b336e88c74b98f07c40d68419898f038f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 19:37:47 +0900 Subject: [PATCH 2/5] test: harden section roadmap assertions --- .../workspace/SectionRoadmap.test.tsx | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx b/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx index bd7f11d0..3e4ee7c5 100644 --- a/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx +++ b/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, within } from "@testing-library/react"; import { createDemoRehearsalSong } from "@bandscope/shared-types"; import { afterEach, describe, expect, it, vi } from "vitest"; import { SectionRoadmap } from "./SectionRoadmap"; @@ -12,6 +12,18 @@ function setNavigatorLanguage(language: string) { }); } +function getChordEditButton(roleName = "Bass Guitar", sectionLabel = "verse", chord = "C#m7") { + return screen.getByRole("button", { + name: new RegExp(`${roleName}.*${sectionLabel}.*${chord}`) + }); +} + +function roleCard(roleName: string) { + const card = screen.getByText(roleName).closest(".border-l-4"); + if (!card) throw new Error(`Role card not found for ${roleName}`); + return card as HTMLElement; +} + describe("SectionRoadmap", () => { afterEach(() => { setNavigatorLanguage(originalLanguage); @@ -26,12 +38,12 @@ describe("SectionRoadmap", () => { render(); - expect(screen.getByText("더 많은 구간은 옆으로 스크롤하세요 →")).toBeTruthy(); + expect(screen.getByText("더 많은 구간은 옆으로 스크롤하세요 →")).toBeInTheDocument(); expect(screen.getAllByText("그루브").length).toBeGreaterThan(0); expect(screen.getAllByText("코드").length).toBeGreaterThan(0); expect(screen.getAllByText("큐").length).toBeGreaterThan(0); expect(screen.getAllByTitle("우선순위: high").length).toBeGreaterThan(0); - expect(screen.getByText("사용자")).toBeTruthy(); + expect(screen.getByText("사용자")).toBeInTheDocument(); }); it("uses localized copy for chord edit prompts and control labels", () => { @@ -42,7 +54,7 @@ describe("SectionRoadmap", () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Bass Guitar의 verse 코드 수정, 현재 C#m7" })); + fireEvent.click(getChordEditButton()); expect(promptSpy).toHaveBeenCalledWith("새 코드 입력:", "C#m7"); expect(screen.getAllByTitle("코드 수정").length).toBeGreaterThan(0); @@ -57,7 +69,7 @@ describe("SectionRoadmap", () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Edit chord for Bass Guitar in verse, current C#m7" })); + fireEvent.click(getChordEditButton()); expect(onSongUpdate).not.toHaveBeenCalled(); }); @@ -70,7 +82,7 @@ describe("SectionRoadmap", () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Bass Guitar의 verse 코드 수정, 현재 C#m7" })); + fireEvent.click(getChordEditButton()); expect(promptSpy).toHaveBeenCalledWith("새 코드 입력:", "C#m7"); expect(onSongUpdate).not.toHaveBeenCalled(); @@ -82,7 +94,7 @@ describe("SectionRoadmap", () => { render(); - expect(screen.getByRole("button", { name: "Bass Guitar의 verse 코드 수정, 현재 C#m7" })).toBeDisabled(); + expect(getChordEditButton()).toBeDisabled(); }); it("renders priority indicators for high, medium, and low roles", () => { @@ -104,9 +116,9 @@ describe("SectionRoadmap", () => { render(); - expect(screen.getAllByTitle("우선순위: high").length).toBeGreaterThan(0); - expect(screen.getAllByTitle("우선순위: medium").length).toBeGreaterThan(0); - expect(screen.getAllByTitle("우선순위: low").length).toBeGreaterThan(0); + expect(within(roleCard("Bass Guitar")).getByTitle("우선순위: high")).toBeInTheDocument(); + expect(within(roleCard("Medium Role")).getByTitle("우선순위: medium")).toBeInTheDocument(); + expect(within(roleCard("Low Role")).getByTitle("우선순위: low")).toBeInTheDocument(); }); it("renders low confidence badges for low-confidence sections and roles", () => { @@ -126,7 +138,7 @@ describe("SectionRoadmap", () => { render(); - expect(screen.getByText("Bass Guitar")).toBeTruthy(); + expect(screen.getByText("Bass Guitar")).toBeInTheDocument(); expect(screen.queryByText("Keyboard 1 Right Hand")).toBeNull(); }); @@ -139,7 +151,7 @@ describe("SectionRoadmap", () => { promptSpy.mockReturnValueOnce("").mockReturnValueOnce(" "); render(); - const editButton = screen.getByRole("button", { name: "Bass Guitar의 verse 코드 수정, 현재 C#m7" }); + const editButton = getChordEditButton(); fireEvent.click(editButton); fireEvent.click(editButton); @@ -153,6 +165,6 @@ describe("SectionRoadmap", () => { render(); - expect(screen.getByText("Density warning: competing with Keyboard Left Hand in low register.")).toBeTruthy(); + expect(screen.getByText(/Density warning/i)).toBeInTheDocument(); }); }); From d00ccf67ffef1ce9aaf6b967bf655a886d791e3f 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 74cde83bec77c955ca0bef04ce126d941c9b7113 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 9a66ae2d8af536d015e7f4976dc1ffa8c90ae018 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 3 Jul 2026 07:41:36 +0900 Subject: [PATCH 5/5] chore: refresh macos build evidence