// Standalone tests for reasampler::instrument_drop — no REAPER, no VST3 SDK, no framework. // The S17 drop-and-load blob-construction contract: the extension builds a vst_chunk blob // whose bytes are EXACTLY what ReaSampler 9000's own setState (deserializeComponentState) // accepts, with the dragged capture pre-selected. The round-trip proof (build -> base64 // decode -> the instrument's OWN reader -> assert the capture selected) IS the cross-artifact // contract guard — the same pattern assignment_request_tests uses for its wire format. #include "../src/instrument_drop.h" #include "../src/vst/sample_map.h" // deserializeComponentState — the instrument's OWN reader #include #include #include using namespace reasampler; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // A representative project rate for the reader (the legacy-v3 conversion parameter; our v5 // blob never consumes it, but the reader signature requires a positive rate). static constexpr double kRate = 48000.0; // THE contract test: a blob built for a capture id decodes — through the instrument's OWN // reader — to a ComponentState with THAT id selected, no zones, default mono. If this fails, // the extension would inject bytes the instrument's setState rejects and the drop would load // a silent/wrong instance. static void testBlobRoundTripsThroughInstrumentReader() { const std::string id = "cap-7f3a-guid"; const std::string b64 = buildInstrumentDropChunk(id); CHECK(!b64.empty()); const std::vector bytes = decodeBase64(b64); CHECK(!bytes.empty()); // The base64 must decode to EXACTLY the pre-encode state bytes (no corruption). CHECK(bytes == instrumentDropStateBytes(id)); const ComponentState cs = deserializeComponentState(bytes, kRate); CHECK(cs.selectionId == id); // the capture IS selected — the whole point CHECK(cs.map.zones.empty()); // a drop selects one capture, authors no zones CHECK(cs.channelMode == ChannelMode::Mono); // fresh-instance default CHECK(cs.lastConsumedAssignGeneration == 0); // fresh instance, no consumed assign } // A GUID-shaped id with bytes that would trip a naive delimiter-based encoder round-trips // whole (the length-prefixed component-state framing + base64 carry arbitrary bytes). static void testGuidLikeIdRoundTrips() { const std::string id = "{9A2F0C11-4B6E-4D01-8F3A-0011223344FF}"; const std::vector bytes = decodeBase64(buildInstrumentDropChunk(id)); const ComponentState cs = deserializeComponentState(bytes, kRate); CHECK(cs.selectionId == id); } // An empty id yields the empty-state blob: it still decodes cleanly to {"", no zones} — the // S10 silent empty state. (The shell guards against dropping nothing; the pure contract holds.) static void testEmptyIdYieldsEmptyState() { const std::vector bytes = decodeBase64(buildInstrumentDropChunk("")); CHECK(!bytes.empty()); // still a versioned envelope, just an empty selection const ComponentState cs = deserializeComponentState(bytes, kRate); CHECK(cs.selectionId.empty()); CHECK(cs.map.zones.empty()); } // Deterministic: the same id always produces the same blob (no time/random in the path). static void testDeterministic() { CHECK(buildInstrumentDropChunk("abc") == buildInstrumentDropChunk("abc")); CHECK(buildInstrumentDropChunk("abc") != buildInstrumentDropChunk("abd")); } // --- base64 codec unit coverage (the encode side the shell actually ships) ----- static std::vector b(std::initializer_list v) { std::vector out; for (int x : v) out.push_back(static_cast(x)); return out; } // Known RFC-4648 vectors, incl. every padding case (0/1/2 trailing bytes). static void testBase64KnownVectors() { CHECK(encodeBase64(b({})) == ""); CHECK(encodeBase64(b({'f'})) == "Zg=="); CHECK(encodeBase64(b({'f', 'o'})) == "Zm8="); CHECK(encodeBase64(b({'f', 'o', 'o'})) == "Zm9v"); CHECK(encodeBase64(b({'f', 'o', 'o', 'b'})) == "Zm9vYg=="); CHECK(encodeBase64(b({'f', 'o', 'o', 'b', 'a'})) == "Zm9vYmE="); CHECK(encodeBase64(b({'f', 'o', 'o', 'b', 'a', 'r'})) == "Zm9vYmFy"); } // encode -> decode is identity across every residue class + all-byte values. static void testBase64RoundTripAllBytes() { for (int len = 0; len <= 300; ++len) { std::vector in; for (int i = 0; i < len; ++i) in.push_back(static_cast((i * 37 + 11) & 0xFF)); CHECK(decodeBase64(encodeBase64(in)) == in); } } // Malformed decode inputs return empty (never throw / never UB): bad length, illegal char, // misplaced padding. static void testBase64DecodeRejectsMalformed() { CHECK(decodeBase64("Zg=").empty()); // length not a multiple of 4 CHECK(decodeBase64("Zm9v!ba=").empty()); // illegal char '!' CHECK(decodeBase64("Z===").empty()); // illegal char in v1 position CHECK(decodeBase64("Zg==Zg==").empty()); // interior padding (pad before the final quad) } // --- FX-hotspot classification (S-VIEW-BUG-1) --------------------------------- // // THE BUG: dropping a capture onto a track's FX button (in the TCP) never armed the // instrument drop, because the old predicate matched only "fx_" — which the SDK reserves for // the FX CHAIN / FLOATING-FX windows. A track-panel hit (where the FX button lives) reports a // string beginning with "tcp"/"mcp". These tests fix the classifier at the boundary the shell // consumes: each would FAIL under the old "fx_"-only rule for the tcp/mcp cases. // The regression case: the TCP FX-button surface reports "tcp*" and MUST classify as an FX // hotspot. Under the pre-fix "fx_"-only predicate these all returned false — the exact miss // that produced the "drops as audio to arrange" symptom. static void testTcpMcpPanelIsHotspot() { CHECK(infoNamesFxHotspot("tcp")); // bare track control panel CHECK(infoNamesFxHotspot("tcp.fx")); // the TCP FX-button WALTER element CHECK(infoNamesFxHotspot("tcp.mute")); // any tcp.* sub-element resolves to the track CHECK(infoNamesFxHotspot("mcp")); // mixer control panel CHECK(infoNamesFxHotspot("mcp.fx")); // the MCP FX area } // The FX chain / floating-FX windows (the surfaces the ORIGINAL predicate matched) still // classify as hotspots — the fix widens the rule, it does not regress the "fx_" surface. static void testFxWindowStillHotspot() { CHECK(infoNamesFxHotspot("fx_chain")); // FX chain window CHECK(infoNamesFxHotspot("fx_0")); // first FX, floating CHECK(infoNamesFxHotspot("fx_12")); // arbitrary floating-FX index } // Non-FX surfaces are NOT hotspots — a drop here is not an instrument drop (it would fall // through to the OS drag / no-op). Guards against over-broad matching (e.g. "spacer_0" must // not match despite living near the tracks; "arrange" is the audio-import surface). static void testNonFxSurfacesAreNotHotspot() { CHECK(!infoNamesFxHotspot("arrange")); CHECK(!infoNamesFxHotspot("spacer_0")); CHECK(!infoNamesFxHotspot("")); // pointer over nothing REAPER classifies CHECK(!infoNamesFxHotspot("trans")); // transport CHECK(!infoNamesFxHotspot("envcp")); // envelope control panel — a track thing, not FX } int main() { testBlobRoundTripsThroughInstrumentReader(); testGuidLikeIdRoundTrips(); testEmptyIdYieldsEmptyState(); testDeterministic(); testBase64KnownVectors(); testBase64RoundTripAllBytes(); testBase64DecodeRejectsMalformed(); testTcpMcpPanelIsHotspot(); testFxWindowStillHotspot(); testNonFxSurfacesAreNotHotspot(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }