Files
hyperframes/packages/studio/src/components/editor/useFxCarveGrouping.test.ts
T
Vance IngallsandClaude Opus 5 c3daa90ba7 fix(core,studio): a voice track is never a carve bed, and never carves its own group
A narration clip inside a Voiceover group had a carve pointed at that
group — a member ducking the bus it feeds. Three faults, each sufficient
on its own.

**No bed-eligibility rule.** `couldBeCarveSource` has said since it was
written that music and sfx cannot be sources, and it is called from
nowhere — exported, tested, dead. Nothing ever asked the near-end
question: can this track be the BED. `showCarve` only asked "is anything
already carving against me, and is there anything to listen to", so a
voice track was offered the control like any other. Added
`couldBeCarveBed` beside its sibling and wired it in.

**Offering is not applying.** A bed with exactly one candidate carves
itself unasked, which is right for a track named `music-bed` and wrong
for one named `a1` — a decision taken off a name that said nothing is how
a carve appears that nobody remembers configuring. `isNamedCarveBed`
gates self-application on a name that positively reads as a bed; the
picker stays looser, the same split the source side already makes between
`sourceOptions` and `autoSourceIds`.

**A bed was offered its own group.** The candidate scan excluded exactly
one element, the bed itself. Its siblings survived that filter and rolled
up into the very group the bed belongs to, which came back as a
candidate — and being the only one, was applied. The mirror case too: a
group bed's id matches no <audio> id, so nothing stopped a group carving
against itself. `collectCarveCandidates` now takes the bed's id and drops
both it and its group.

An existing carve still shows its module (`carve !== null`), so nothing
already configured becomes unreachable — only newly offered and
self-applied ones are refused.

The bed/relationship predicates moved to `useFxCarveGrouping.ts`, next to
the source-eligibility rules they belong with. That is also what puts
`useFxCarve.ts` back under the 600-line ceiling it crossed here.

Five tests, each mutation-checked against the pre-fix code. Verified live:
selecting `vo-2` renders no carve module; `music-bed` still gets one,
listening to `Voiceover (4)`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 02:20:02 -07:00

56 lines
2.3 KiB
TypeScript

// @vitest-environment happy-dom
import { describe, expect, it } from "vitest";
import { collectCarveCandidates } from "./useFxCarveGrouping";
function previewDoc(html: string): Document {
const doc = document.implementation.createHTMLDocument("preview");
doc.body.innerHTML = html;
return doc;
}
/** What the panel would offer as sources for `bedId`, the way useFxCarve asks. */
function candidatesFor(doc: Document, bedId: string) {
const others = Array.from(doc.querySelectorAll<HTMLAudioElement>("audio[id]")).filter(
(a) => a.id !== bedId,
);
return collectCarveCandidates(doc, others, () => true, bedId).map((c) => c.id);
}
const GROUPED_VOICES = `
<hf-audio-group id="voiceover" data-label="Voiceover"></hf-audio-group>
<audio id="vo-1" data-audio-group="voiceover"></audio>
<audio id="vo-2" data-audio-group="voiceover"></audio>
<audio id="music-bed"></audio>
`;
describe("collectCarveCandidates", () => {
// The observed bug: selecting vo-2 offered "Voiceover (2)" — the group vo-2 is
// itself a member of. The caller filters out the bed element, but vo-1 survives
// that filter and rolls up into exactly that group. Being the only candidate, it
// was then applied without the author asking: a member ducking the bus it feeds.
it("never offers a member the group it belongs to", () => {
expect(candidatesFor(previewDoc(GROUPED_VOICES), "vo-2")).toEqual(["music-bed"]);
});
// And the mirror: a group bed's own id matches no <audio> id, so nothing
// excluded it. Its members rolled up and handed the group back to itself.
it("never offers a group itself", () => {
const doc = previewDoc(GROUPED_VOICES);
expect(candidatesFor(doc, "voiceover")).toEqual(["music-bed"]);
});
it("still offers a group the bed has nothing to do with", () => {
const doc = previewDoc(`
${GROUPED_VOICES}
<hf-audio-group id="sfx" data-label="SFX"></hf-audio-group>
<audio id="sfx-click" data-audio-group="sfx"></audio>
`);
expect(candidatesFor(doc, "music-bed")).toEqual(["voiceover", "sfx"]);
});
it("leaves an ungrouped bed's candidates alone", () => {
const doc = previewDoc(`<audio id="music-bed"></audio><audio id="vo-1"></audio>`);
expect(candidatesFor(doc, "music-bed")).toEqual(["vo-1"]);
});
});