-
Notifications
You must be signed in to change notification settings - Fork 1
feat(workspace): name tonight's first part handoff on the map #1094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
9746570
feat(workspace): name tonight's first part handoff on the map
seonghobae ecc8d84
test(workspace): reproduce handoff destination labeling
seonghobae 3dd89a5
fix(workspace): label handoffs by destination section
seonghobae 8c09a23
test(workspace): align handoff fixtures with transition semantics
seonghobae 6cf16dd
test(workspace): exercise handoff at real section boundary
seonghobae 0c20a5e
docs(workspace): document handoff destination semantics
seonghobae 884c329
test(workspace): reproduce activity handoff receiver gap
seonghobae d60f376
fix(workspace): resolve handoff receiver from destination roles
seonghobae beca1e6
docs(workspace): align handoff role authority with analysis topology
seonghobae 460ac88
test(analysis): reject heuristic handoff authority
seonghobae d89f92e
test(analysis): require truthful fallback provenance
seonghobae 99e2d2d
fix(analysis): remove heuristic handoff authority
seonghobae 9086344
style(analysis): restore extractor newline
seonghobae c083756
docs(handoff): mark heuristic transitions unavailable
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
245 changes: 245 additions & 0 deletions
245
apps/desktop/src/features/workspace/firstHandoff.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| import { createDemoRehearsalSong, type RehearsalSong } from "@bandscope/shared-types"; | ||
| import { describe, expect, it } from "vitest"; | ||
| import { fillRangeCopy } from "./firstRangeSqueeze"; | ||
| import { firstHandoff } from "./firstHandoff"; | ||
|
|
||
| function withDestination(song: RehearsalSong): RehearsalSong { | ||
| const source = song.sections[0]; | ||
| return { | ||
| ...song, | ||
| sections: [ | ||
| source, | ||
| { | ||
| ...source, | ||
| id: "chorus-1", | ||
| label: "chorus", | ||
| partGraph: source.partGraph.map((node) => ({ | ||
| ...node, | ||
| handoff_to: [], | ||
| handoff_from: [] | ||
| })) | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
|
|
||
| function withPartGraph( | ||
| song: RehearsalSong, | ||
| partGraph: RehearsalSong["sections"][number]["partGraph"] | ||
| ): RehearsalSong { | ||
| const readySong = withDestination(song); | ||
| return { | ||
| ...readySong, | ||
| sections: readySong.sections.map((section, index) => | ||
| index === 0 ? { ...section, partGraph } : section | ||
| ) | ||
| }; | ||
| } | ||
|
|
||
| describe("firstHandoff", () => { | ||
| it("names the first active pass from the source graph at its destination section", () => { | ||
| expect(firstHandoff(withDestination(createDemoRehearsalSong()))).toEqual({ | ||
| sectionLabel: "chorus", | ||
| fromRole: "Bass Guitar", | ||
| toRole: "Lead Vocal" | ||
| }); | ||
| }); | ||
|
|
||
| it("names an analysis-derived handoff at the destination section", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const source = song.sections[0]; | ||
| const destination = { | ||
| ...source, | ||
| id: "chorus-1", | ||
| label: "chorus" as const, | ||
| partGraph: source.partGraph.map((node) => ({ | ||
| ...node, | ||
| handoff_to: [], | ||
| handoff_from: [] | ||
| })) | ||
| }; | ||
|
|
||
| expect(firstHandoff({ ...song, sections: [source, destination] })).toEqual({ | ||
| sectionLabel: "chorus", | ||
| fromRole: "Bass Guitar", | ||
| toRole: "Lead Vocal" | ||
| }); | ||
| }); | ||
|
|
||
| it("resolves an activity-derived entering receiver from the destination roles", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const template = song.sections[0]; | ||
| const bass = template.roles.find((role) => role.id === "bass-guitar"); | ||
| const vocal = template.roles.find((role) => role.id === "lead-vocal"); | ||
| if (!bass || !vocal) { | ||
| throw new Error("Demo fixture must contain bass and lead vocal roles"); | ||
| } | ||
|
|
||
| const source = { | ||
| ...template, | ||
| roles: [bass], | ||
| partGraph: [ | ||
| { | ||
| role_id: "bass-guitar", | ||
| is_active: true, | ||
| handoff_to: ["lead-vocal"], | ||
| handoff_from: [] | ||
| }, | ||
| { | ||
| role_id: "lead-vocal", | ||
| is_active: false, | ||
| handoff_to: [], | ||
| handoff_from: [] | ||
| } | ||
| ] | ||
| }; | ||
| const destination = { | ||
| ...template, | ||
| id: "chorus-1", | ||
| label: "chorus", | ||
| roles: [vocal], | ||
| partGraph: [ | ||
| { | ||
| role_id: "bass-guitar", | ||
| is_active: false, | ||
| handoff_to: [], | ||
| handoff_from: [] | ||
| }, | ||
| { | ||
| role_id: "lead-vocal", | ||
| is_active: true, | ||
| handoff_to: [], | ||
| handoff_from: ["bass-guitar"] | ||
| } | ||
| ] | ||
| }; | ||
|
|
||
| expect(firstHandoff({ ...song, sections: [source, destination] })).toEqual({ | ||
| sectionLabel: "chorus", | ||
| fromRole: "Bass Guitar", | ||
| toRole: "Lead Vocal" | ||
| }); | ||
| }); | ||
|
|
||
| it("fails closed when a transition handoff has no valid destination section", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const malformed = { | ||
| ...song, | ||
| sections: [song.sections[0], null] | ||
| } as unknown as RehearsalSong; | ||
|
|
||
| expect(firstHandoff(malformed)).toBeNull(); | ||
| }); | ||
|
|
||
| it("fails closed when the destination section has no meaningful label", () => { | ||
| const song = withDestination(createDemoRehearsalSong()); | ||
| const destination = { ...song.sections[1], label: " " }; | ||
|
|
||
| expect(firstHandoff({ ...song, sections: [song.sections[0], destination] })).toBeNull(); | ||
| }); | ||
|
|
||
| it("does not invent a transition for a one-section song", () => { | ||
| expect(firstHandoff(createDemoRehearsalSong())).toBeNull(); | ||
| }); | ||
|
|
||
| it("skips inactive nodes until an active named receiver exists", () => { | ||
| const song = withPartGraph(createDemoRehearsalSong(), [ | ||
| { | ||
| role_id: "bass-guitar", | ||
| is_active: false, | ||
| handoff_to: ["lead-vocal"], | ||
| handoff_from: [] | ||
| }, | ||
| { | ||
| role_id: "keys-right", | ||
| is_active: true, | ||
| handoff_to: ["lead-vocal"], | ||
| handoff_from: [] | ||
| }, | ||
| { | ||
| role_id: "lead-vocal", | ||
| is_active: true, | ||
| handoff_to: [], | ||
| handoff_from: ["keys-right"] | ||
| } | ||
| ]); | ||
|
|
||
| expect(firstHandoff(song)).toEqual({ | ||
| sectionLabel: "chorus", | ||
| fromRole: "Keyboard 1 Right Hand", | ||
| toRole: "Lead Vocal" | ||
| }); | ||
| }); | ||
|
|
||
| it("skips blank, none, self, and unknown receivers", () => { | ||
| const song = withPartGraph(createDemoRehearsalSong(), [ | ||
| { | ||
| role_id: "bass-guitar", | ||
| is_active: true, | ||
| handoff_to: ["", "none", "bass-guitar", "missing-role", "lead-vocal"], | ||
| handoff_from: [] | ||
| } | ||
| ]); | ||
|
|
||
| expect(firstHandoff(song)).toEqual({ | ||
| sectionLabel: "chorus", | ||
| fromRole: "Bass Guitar", | ||
| toRole: "Lead Vocal" | ||
| }); | ||
| }); | ||
|
|
||
| it("limits the pass to the selected role as giver or receiver", () => { | ||
| expect(firstHandoff(withDestination(createDemoRehearsalSong()), "lead-vocal")).toEqual({ | ||
| sectionLabel: "chorus", | ||
| fromRole: "Bass Guitar", | ||
| toRole: "Lead Vocal" | ||
| }); | ||
| }); | ||
|
|
||
| it("returns null when the selected role is not on a named pass", () => { | ||
| const song = withDestination(createDemoRehearsalSong()); | ||
| expect(firstHandoff(song, "keys-right")).toBeNull(); | ||
| expect(firstHandoff(song, "missing-role")).toBeNull(); | ||
| }); | ||
|
|
||
| it("skips inherited or malformed part-graph evidence", () => { | ||
| const inherited = Object.create({ | ||
| role_id: "bass-guitar", | ||
| is_active: true, | ||
| handoff_to: ["lead-vocal"] | ||
| }) as RehearsalSong["sections"][number]["partGraph"][number]; | ||
| const song = withPartGraph(createDemoRehearsalSong(), [ | ||
| inherited, | ||
| { | ||
| role_id: "keys-right", | ||
| is_active: true, | ||
| handoff_to: ["lead-vocal"], | ||
| handoff_from: [] | ||
| } | ||
| ]); | ||
|
|
||
| expect(firstHandoff(song)).toEqual({ | ||
| sectionLabel: "chorus", | ||
| fromRole: "Keyboard 1 Right Hand", | ||
| toRole: "Lead Vocal" | ||
| }); | ||
| }); | ||
|
|
||
| it("fails closed on malformed runtime roots", () => { | ||
| for (const malformed of [null, {}, { sections: {} }, { sections: [null] }]) { | ||
| expect(firstHandoff(malformed as unknown as RehearsalSong)).toBeNull(); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe("handoff copy filling", () => { | ||
| it("keeps rehearsal values literal", () => { | ||
| expect( | ||
| fillRangeCopy("{fromRole} hands off to {toRole} in {sectionLabel}.", { | ||
| fromRole: "Bass {toRole}", | ||
| toRole: "Lead Vocal", | ||
| sectionLabel: "verse" | ||
| }) | ||
| ).toBe("Bass {toRole} hands off to Lead Vocal in verse."); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.