Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
8e2e8d1
test(handoff): specify bounded metadata import and request focus
seonghobae Aug 3, 2026
ce411bf
test(handoff): specify accessible import and clear controls
seonghobae Aug 3, 2026
2e85257
test(handoff): specify App round-trip reanalysis wiring
seonghobae Aug 3, 2026
9d02593
feat(handoff): validate bounded metadata imports
seonghobae Aug 3, 2026
9a533cc
feat(handoff): add accessible import control
seonghobae Aug 3, 2026
f89777f
chore(handoff): add one-shot App integration patch
seonghobae Aug 3, 2026
d50af83
ci(handoff): apply one-shot App integration patch
seonghobae Aug 3, 2026
e9eba05
ci(handoff): allow one-shot patch on pull request
seonghobae Aug 3, 2026
d037510
ci(handoff): verify desktop contract before integration commit
seonghobae Aug 3, 2026
1bbf1dc
security(ci): remove PR-controlled write-token bootstrap
seonghobae Aug 3, 2026
ad1ffad
fix(handoff): bound file allocation with Blob slicing
seonghobae Aug 3, 2026
37b3bc2
ci(handoff): apply import integration bootstrap
seonghobae Aug 3, 2026
eb9ac19
ci(handoff): trigger one-shot integration on PR updates
seonghobae Aug 3, 2026
d53aa6d
test(handoff): align file doubles with bounded slice contract
seonghobae Aug 3, 2026
9a5303c
ci(handoff): verify bounded Blob slice test contract
seonghobae Aug 3, 2026
2895dd2
test(handoff): exercise visible file-picker activation
seonghobae Aug 3, 2026
914e810
test(handoff): instrument import runtime for changed-line coverage
seonghobae Aug 3, 2026
a57d5de
test(handoff): cover error mapping and successful cleanup paths
seonghobae Aug 3, 2026
92d937d
docs(changelog): record safe metadata-handoff reanalysis flow
seonghobae Aug 3, 2026
bf804b4
docs(handoff): document recipient workflow and trust boundary
seonghobae Aug 3, 2026
27eaa38
test(handoff): cover bounded Blob reads and unsafe size branches
seonghobae Aug 3, 2026
ea177e3
ci(handoff): narrow bootstrap to App and locale integration
seonghobae Aug 3, 2026
fe4016c
test(handoff): require explicit source pairing after import
seonghobae Aug 3, 2026
336b407
chore(handoff): add explicit source-pairing finalizer
seonghobae Aug 3, 2026
25c9e43
ci(handoff): verify explicit source pairing before commit
seonghobae Aug 3, 2026
d876f9b
docs(handoff): document explicit source re-pairing
seonghobae Aug 3, 2026
554b11e
docs(changelog): record fresh audio pairing for handoffs
seonghobae Aug 3, 2026
385062a
docs(changelog): preserve release history while noting source pairing
seonghobae Aug 3, 2026
114b64c
test(analysis): require role focus to affect returned rehearsal result
seonghobae Aug 3, 2026
4f8811b
chore(analysis): add one-shot role-focus enforcement
seonghobae Aug 3, 2026
f846ec9
ci(handoff): verify engine-enforced role focus and full cache reuse
seonghobae Aug 3, 2026
61c9f66
docs(handoff): specify backend role-focus and cache semantics
seonghobae Aug 3, 2026
0e63935
ci: scope handoff bootstrap write permission
seonghobae Aug 3, 2026
c8b5254
fix(handoff): satisfy exported constant JSDoc contract
seonghobae Aug 3, 2026
c549ad1
ci(handoff): retrigger one-shot integration bootstrap
seonghobae Aug 3, 2026
081ecc0
fix(handoff): attach file limit docs to declaration
seonghobae Aug 3, 2026
8815463
feat(handoff): enforce focused local reanalysis
github-actions[bot] Aug 3, 2026
2c1e9d8
docs(handoff): clarify pre-parse allocation bound
seonghobae Aug 3, 2026
8256ded
chore(ci): stage handoff API formatting fix
seonghobae Aug 3, 2026
96001e4
chore(ci): trigger handoff API formatting fix
seonghobae Aug 3, 2026
090ecba
chore(ci): expose handoff formatting validation
seonghobae Aug 3, 2026
c31a90e
style(analysis): format handoff API changes
github-actions[bot] Aug 3, 2026
70b829e
docs(handoff): state source-reference authority boundary
seonghobae Aug 3, 2026
89bfc8b
fix(handoff): expose validation activity to source controls
seonghobae Aug 3, 2026
a4910e3
test(handoff): report validation activity transitions
seonghobae Aug 3, 2026
e3d4f32
chore(ci): stage handoff reading source gate
seonghobae Aug 3, 2026
3523784
chore(ci): run handoff reading source gate
seonghobae Aug 3, 2026
a3105a8
chore(ci): trigger handoff reading source gate
seonghobae Aug 3, 2026
0c5c3ce
fix(handoff): gate sources during validation
github-actions[bot] Aug 3, 2026
5c6a2a6
docs(handoff): document source gating during validation
seonghobae Aug 3, 2026
b3de77f
docs(handoff): attach byte limit JSDoc to export
seonghobae Aug 3, 2026
adbb7f6
test(handoff): verify cache miss stores complete analysis
seonghobae Aug 3, 2026
4fe7348
fix(desktop): satisfy handoff export documentation lint
seonghobae Aug 3, 2026
34d9534
fix(security): require patched PDF.js
seonghobae Aug 11, 2026
6c40148
fix(security): refresh vulnerable npm transitive pins
seonghobae Aug 11, 2026
08dcd1e
fix(deps): restore manifest-lock consistency
seonghobae Aug 11, 2026
9a6b6bc
fix(scope): keep handoff PR dependency-neutral
seonghobae Aug 14, 2026
9916d96
test(ui): lock source actions during local selection
seonghobae Aug 15, 2026
ae2e25c
test(ui): block analysis during source replacement
seonghobae Aug 15, 2026
c773eab
ci(repair): lock competing source actions in PR 740
seonghobae Aug 15, 2026
0242be5
ci(repair): provision Python and Rust quickcheck deps
seonghobae Aug 15, 2026
e0a8bb3
fix(ui): lock competing source actions
Aug 15, 2026
97b70ea
chore(ci): revalidate source-action lock exact head
seonghobae Aug 15, 2026
0998884
fix(changelog): restore released history
seonghobae Aug 15, 2026
7436556
test(handoff): require local source while handoff is pending
seonghobae Aug 15, 2026
2e7fdc3
fix(handoff): require explicit local audio pairing
seonghobae Aug 15, 2026
0ecc855
docs(handoff): preserve explicit local pairing boundary
seonghobae Aug 15, 2026
5311c83
docs(changelog): record handoff source authority lock
seonghobae Aug 15, 2026
a10c664
test(handoff): block project replacement during validation
seonghobae Aug 15, 2026
3c74c36
fix(handoff): block project load during validation
seonghobae Aug 15, 2026
b936436
docs(handoff): document project-load exclusion during validation
seonghobae Aug 15, 2026
6a346da
docs(changelog): record handoff project-load race guard
seonghobae Aug 15, 2026
4c018cb
test(handoff): lock source controls during project load
seonghobae Aug 15, 2026
1385f77
test(handoff): keep project-load regression focused
seonghobae Aug 15, 2026
8f7428d
fix(handoff): make project loading an atomic UI transition
seonghobae Aug 15, 2026
7b1a39c
docs(handoff): document atomic project-load transition
seonghobae Aug 15, 2026
2c9a50f
docs(changelog): record atomic project-load source lock
seonghobae Aug 15, 2026
a85b842
test: lock project replacement during source selection
seonghobae Aug 15, 2026
aa60271
fix: serialize source selection with project loading
seonghobae Aug 15, 2026
cd76ffc
test: cover reciprocal source project exclusion
seonghobae Aug 16, 2026
40f5502
test: cover handoff YouTube fail-closed guard
seonghobae Aug 16, 2026
4c30624
test: exercise handoff YouTube handler guard
seonghobae Aug 16, 2026
d2fe377
test(ui): require source transition handler guards
seonghobae Aug 16, 2026
5fa4b31
chore: stage source transition guard repair
seonghobae Aug 16, 2026
90f295c
fix(ui): enforce source transition guards in handlers
github-actions[bot] Aug 16, 2026
473f692
chore(ci): retrigger exact-head gates
seonghobae Aug 16, 2026
d188796
test(ui): preserve source cancellation semantics
seonghobae Aug 16, 2026
bd87702
test(desktop): reject bypassed save during project load
seonghobae Aug 16, 2026
ce24093
fix(desktop): guard save during project load
seonghobae Aug 16, 2026
a84a7cf
test(desktop): reject stale handoff control events
seonghobae Aug 16, 2026
2e5300e
fix(desktop): guard stale handoff control events
seonghobae Aug 16, 2026
6eb1e4e
docs: record handoff handler authority
seonghobae Aug 16, 2026
3f610e8
docs: document stale handoff event rejection
seonghobae Aug 16, 2026
80dc865
test(handoff): clear stale source errors on valid import
seonghobae Aug 16, 2026
78a65d4
fix(handoff): clear stale source error on valid import
seonghobae Aug 16, 2026
7d38059
test: reject stale analysis poll results
seonghobae Aug 19, 2026
7f451f1
fix: ignore stale analysis poll responses
seonghobae Aug 19, 2026
6192813
test: reject mismatched analysis poll identity
seonghobae Aug 19, 2026
c86da25
test(desktop): isolate stale poll regression
seonghobae Aug 19, 2026
4e7d322
test(desktop): reject foreign analysis job status
seonghobae Aug 19, 2026
8427ae2
fix(desktop): bind polled status to requested job
seonghobae Aug 19, 2026
1a1246d
test: make stale poll status assertion unambiguous
seonghobae Aug 19, 2026
4761a88
test: align retry poll fixture job identity
seonghobae Aug 19, 2026
3c49446
merge(develop): inherit first playable range with handoff import
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Import a bounded, validated BandScope metadata-handoff JSON file, require an explicit fresh local-audio pairing, and reuse the handoff's focused rehearsal roles for reanalysis (#739). Competing source actions and project replacement remain locked while handoff validation is pending; project loading reciprocally locks source/handoff changes and saving the prior project until the load settles; source/project/analysis handlers and the handoff import/open/change/clear handlers independently reject stale overlapping transitions even if UI disablement is bypassed; once a handoff validates, YouTube import stays unavailable until the handoff is cleared or consumed so only an explicit recipient-selected local file can satisfy the pairing boundary.
- 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 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.
Expand Down
86 changes: 86 additions & 0 deletions apps/desktop/src/App.handoff-project-load.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { RehearsalSong } from "@bandscope/shared-types";
import { App } from "./App";
import { loadProject } from "./lib/analysis";

vi.mock("./features/score/ScoreView", () => ({
ScoreView: () => <div>Score view</div>
}));

vi.mock("./features/workspace/Workspace", () => ({
Workspace: () => <div>Workspace result</div>
}));

vi.mock("./lib/analysis", () => ({
MAX_YOUTUBE_URL_LENGTH: 2048,
createDefaultAnalysisRequest: vi.fn(() => ({
sourceKind: "demo",
sourceLabel: "Late Night Set",
roleFocus: ["keys-right"]
})),
getAnalysisJobStatus: vi.fn(),
importYoutubeUrl: vi.fn(),
isSupportedYoutubeUrl: vi.fn(() => true),
loadProject: vi.fn(),
saveProject: vi.fn(),
selectLocalAudioSource: vi.fn(),
startAnalysisJob: vi.fn(),
subscribeToAnalysisJobUpdates: vi.fn(async () => () => undefined)
}));

vi.mock("./lib/handoff", async (importActual) => {
const actual = await importActual<typeof import("./lib/handoff")>();
return {
...actual,
readMetadataHandoffFile: vi.fn()
};
});

const mockedLoadProject = vi.mocked(loadProject);

function loadedSong(): RehearsalSong {
return {
id: "loaded-song",
title: "Loaded song",
sections: [],
exportSummary: {
format: "cue-sheet",
headline: "Loaded rehearsal project",
focusSections: []
}
} as RehearsalSong;
}

describe("App project-load source exclusion", () => {
beforeEach(() => {
mockedLoadProject.mockReset();
});

it("locks source and handoff controls until project loading settles", async () => {
let resolveProject: ((song: RehearsalSong) => void) | null = null;
mockedLoadProject.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveProject = resolve;
})
);
render(<App />);

fireEvent.click(screen.getByRole("button", { name: /open project/i }));

expect(screen.getByRole("button", { name: /open project/i })).toBeDisabled();
expect(screen.getByRole("button", { name: /choose local audio/i })).toBeDisabled();
expect(screen.getByRole("button", { name: /import handoff/i })).toBeDisabled();
expect(screen.getByLabelText(/youtube url/i)).toBeDisabled();

resolveProject?.(loadedSong());
await screen.findByText("Workspace result");
await waitFor(() => {
expect(screen.getByRole("button", { name: /open project/i })).not.toBeDisabled();
expect(screen.getByRole("button", { name: /choose local audio/i })).not.toBeDisabled();
expect(screen.getByRole("button", { name: /import handoff/i })).not.toBeDisabled();
expect(screen.getByLabelText(/youtube url/i)).not.toBeDisabled();
});
});
});
124 changes: 124 additions & 0 deletions apps/desktop/src/App.handoff-youtube-guard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { ButtonHTMLAttributes } from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { MetadataHandoffArtifact } from "@bandscope/shared-types";
import { App } from "./App";
import { importYoutubeUrl } from "./lib/analysis";
import { readMetadataHandoffFile } from "./lib/handoff";

vi.mock("./features/score/ScoreView", () => ({
ScoreView: () => <div>Score view</div>
}));

vi.mock("./features/workspace/Workspace", () => ({
Workspace: () => <div>Workspace result</div>
}));

vi.mock("@/components/ui/button", () => ({
Button: ({ disabled, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => (
<button {...props} data-disabled={disabled ? "true" : "false"} />
)
}));

vi.mock("./lib/analysis", () => ({
MAX_YOUTUBE_URL_LENGTH: 2048,
createDefaultAnalysisRequest: vi.fn(() => ({
sourceKind: "demo",
sourceLabel: "Late Night Set",
roleFocus: ["keys-right"]
})),
getAnalysisJobStatus: vi.fn(),
importYoutubeUrl: vi.fn(),
isSupportedYoutubeUrl: vi.fn(() => true),
loadProject: vi.fn(),
saveProject: vi.fn(),
selectLocalAudioSource: vi.fn(),
startAnalysisJob: vi.fn(),
subscribeToAnalysisJobUpdates: vi.fn(async () => () => undefined)
}));

vi.mock("./lib/handoff", async (importActual) => {
const actual = await importActual<typeof import("./lib/handoff")>();
return {
...actual,
readMetadataHandoffFile: vi.fn()
};
});

const mockedImportYoutubeUrl = vi.mocked(importYoutubeUrl);
const mockedReadMetadataHandoffFile = vi.mocked(readMetadataHandoffFile);

function handoff(): MetadataHandoffArtifact {
return {
artifactKind: "bandscope.metadata-handoff",
artifactVersion: 1,
createdAt: "2026-08-16T00:00:00.000Z",
workspace: { id: "workspace-1", title: "Friday rehearsal", workspaceVersion: 1 },
song: {
id: "song-1",
title: "Late Night Set",
exportSummary: {
format: "cue-sheet",
headline: "Start with the chorus entrance.",
focusSections: ["chorus"]
}
},
sections: [
{
id: "chorus-1",
label: "chorus",
timeRange: { start: 0, end: 30 },
confidence: { level: "high", source: "model", notes: "" },
roleBuckets: [
{
id: "keys-right",
name: "Keys Right",
roleType: "instrument",
confidence: { level: "high", source: "model", notes: "" },
rehearsalPriority: "high"
}
]
}
],
sourceAssets: []
};
}

describe("App handoff YouTube defense in depth", () => {
beforeEach(() => {
mockedImportYoutubeUrl.mockReset();
mockedReadMetadataHandoffFile.mockReset();
});

it("keeps the handler fail-closed if the UI disablement boundary is bypassed", async () => {
mockedReadMetadataHandoffFile.mockResolvedValueOnce({
ok: true,
fileName: "handoff.json",
artifact: handoff(),
roleFocus: ["keys-right"]
});

render(<App />);

fireEvent.change(screen.getByLabelText(/youtube url/i), {
target: { value: "https://youtu.be/abc123DEF45" }
});
const importButton = screen.getByRole("button", { name: /import youtube/i });

fireEvent.change(screen.getByLabelText(/handoff JSON file/i), {
target: {
files: [new File(["{}"], "handoff.json", { type: "application/json" })]
}
});

await screen.findByText("Friday rehearsal");
expect(importButton).toHaveAttribute("data-disabled", "true");

// The test Button deliberately records but does not enforce `disabled`,
// modeling a compromised UI primitive. The handler must still reject the
// stale YouTube action while validated handoff authority is pending.
fireEvent.click(importButton);

expect(mockedImportYoutubeUrl).not.toHaveBeenCalled();
});
});
Loading
Loading