Fix drag-handoff bugs: gate FX re-resolve on outside-panel, cache unresolvable OS-drag verdict, block double FX-add retry

This commit is contained in:
2026-07-30 00:09:00 -04:00
parent 0800760833
commit 875d5b4632
9 changed files with 110 additions and 75 deletions
+11 -26
View File
@@ -223,39 +223,26 @@ static void testMixedTallies() {
// --- OS hand-off ordering -----------------------------------------------------
//
// The drag-out-lands-without-audio regression: the shell used to wind its internal drag down
// The empty-payload teardown regression: the shell used to wind its internal drag down
// (release capture, clear drag state) BEFORE checking whether the payload had resolved to any
// on-disk file. A payload that resolved to nothing therefore consumed the gesture — the drag
// died half-torn-down and the user saw a drop with no audio and dragged again. The two side
// effects are one decision; these pin that.
// on-disk file. For an unresolvable payload, initiateDragOut is never reached — no OS drop
// happens at all — but the teardown ran anyway, so the drag just silently stopped mid-gesture
// with no highlight and no drop. The user has to press and start an entirely new drag; retrying
// the SAME stale selection resolves to the same empty payload and fails identically. These pin
// the fix: the two side effects (teardown, hand-off) are one decision.
// Nothing draggable -> hand off nothing AND keep the internal drag alive.
static void testEmptyPathsHandsOffNothingAndKeepsInternalDrag() {
const OsHandoff h = decideOsHandoff({});
CHECK(!h.startOsDrag);
CHECK(!h.releaseInternalDrag);
CHECK(!h.handOffToOs);
}
// A resolvable payload -> release the internal drag, then start the OS drag.
// A resolvable payload -> hand off (release the internal drag, then start the OS drag).
static void testResolvablePayloadHandsOff() {
const OsHandoff one = decideOsHandoff({"C:/proj/bank/a.wav"});
CHECK(one.startOsDrag);
CHECK(one.releaseInternalDrag);
CHECK(one.handOffToOs);
const OsHandoff many = decideOsHandoff({"a.wav", "b.wav", "c.wav"});
CHECK(many.startOsDrag);
CHECK(many.releaseInternalDrag);
}
// The coupling itself: releasing without starting is the defect, so it must be unreachable
// for every input the assembler can produce.
static void testNeverReleasesWithoutStarting() {
const std::vector<std::vector<std::string>> inputs = {
{}, {"a.wav"}, {"a.wav", "b.wav"}, {""},
};
for (const std::vector<std::string>& in : inputs) {
const OsHandoff h = decideOsHandoff(in);
CHECK(!(h.releaseInternalDrag && !h.startOsDrag));
}
CHECK(many.handOffToOs);
}
// End to end through the assembler: a selection whose every entry is stale/unresolvable
@@ -263,8 +250,7 @@ static void testNeverReleasesWithoutStarting() {
static void testAllSkippedSelectionKeepsInternalDrag() {
const PathList l = assemblePathList({missing("g1.wav"), unresolved()});
const OsHandoff h = decideOsHandoff(l.paths);
CHECK(!h.startOsDrag);
CHECK(!h.releaseInternalDrag);
CHECK(!h.handOffToOs);
}
int main() {
@@ -292,7 +278,6 @@ int main() {
testEmptyPathsHandsOffNothingAndKeepsInternalDrag();
testResolvablePayloadHandsOff();
testNeverReleasesWithoutStarting();
testAllSkippedSelectionKeepsInternalDrag();
if (g_fail == 0) std::printf("All tests passed.\n");
+9 -24
View File
@@ -237,29 +237,15 @@ static void testEmbedStripIsNotHotspot() {
// --- Drop surface parity: container vs FX button ------------------------------
//
// The container-drop regression (instance loads, capture does not): the two surfaces must be
// one code path carrying one payload. The payload is a function of the capture alone, so the
// surface cannot influence it — and both surfaces must classify as hotspots so both reach it.
// Every surface a drop can land on admits the SAME capture and yields a payload that decodes
// back to it. The payload takes the capture and nothing else, so the surface cannot influence
// it — walking the surfaces here is what pins that the container family is not second-class.
static void testEveryDropSurfaceCarriesTheCapture() {
const char* surfaces[] = {
"fx_chain", // FX chain window / container list — the reported failing surface
"fx_0", // floating FX window
"tcp.fx", // TCP FX button — the reference path
"mcp.fx", // MCP FX button
};
const std::string sampleId = "cap-7f3a";
for (const char* info : surfaces) {
CHECK(infoNamesFxHotspot(info));
const ParsedPreset p = parsePreset(buildInstrumentDropPreset(sampleId));
CHECK(p.ok);
CHECK(p.classId == vstClassIdHex());
const ComponentState cs = deserializeComponentState(p.compChunk, kRate);
CHECK(cs.selectionId == sampleId);
}
}
// one code path carrying one payload. buildInstrumentDropPreset takes only sampleId, so the
// payload side of that claim is already proven once by testPresetRoundTripsThroughInstrumentReader
// and every "fx_"/"tcp.fx"/"mcp.fx" surface classifying as a hotspot is proven by
// testTcpMcpFxFamilyIsHotspot / testFxWindowStillHotspot. A loop that reruns both against a
// fixed sampleId per surface string can't distinguish the surfaces (the loop body is identical
// every iteration) — it isn't a stronger test than those two, so there is no separate test here.
// The one thing that DOES vary by surface — the shell's TrackFX_AddByName `instantiate` value
// picked for a container drop vs. a bare FX-button drop — lives in instrument_drop_win.cpp and
// is untestable without a live DAW (GetThingFromPoint/TrackFX_AddByName have no pure model).
// --- All-or-nothing rollback --------------------------------------------------
@@ -314,7 +300,6 @@ int main() {
testNonFxSurfacesAreNotHotspot();
testEmbedStripIsNotHotspot();
testEveryDropSurfaceCarriesTheCapture();
testAddFailureLeavesNothingToRollBack();
testPresetFailureRollsBackTheCreatedIndex();
testSuccessKeepsTheInstance();