// Standalone tests for reasampler::render_settings — no REAPER, no framework. // Covers the pure pieces behind the capture family: the source-mode -> // RENDER_SETTINGS bit mapping, the TailMode -> RENDER_* (tail/normalize/trim-end) // mapping + the -72 dB derived ratio + the 8 s manual clamp, P_RAZOREDITS parsing // -> ranges + union, scope -> source mode, range inference (razor-else-time), the // FX-bypass plan (corrects the "items captured through parent FX" defect), and the // capture-action taxonomy table (stable ids, scope x tail-variant matrix). #include "../src/core/capture/render_settings.h" #include #include #include #include #include using namespace reasampler; using namespace reasampler::capture; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- renderSettingsFor: wet-only bit mapping --------------------------------- static void testMasterMixWet() { // Master mix -> value 0 (no source bits), supported. RenderSettingsChoice c = renderSettingsFor(SourceMode::MasterMix, 1.0); CHECK(c.settings == kRenderMasterMix); CHECK(c.supported); // TimeSelection aliases master mix — same result. CHECK(renderSettingsFor(SourceMode::TimeSelection, 1.0).settings == kRenderMasterMix); // wetDry argument is irrelevant (all three-scope capture actions are wet); passing 0.0 // must still yield the same wet master-mix bits. CHECK(renderSettingsFor(SourceMode::MasterMix, 0.0).settings == kRenderMasterMix); } static void testSelectedTracksWet() { // Selected tracks -> via master (&128), header-confirmed. RenderSettingsChoice c = renderSettingsFor(SourceMode::SelectedTracks, 1.0); CHECK(c.settings == kRenderSelTracksViaMaster); CHECK(c.supported); } static void testSelectedItemsSingleFile() { // Items render to ONE file (single-file bit set) so N items -> 1 bank entry. RenderSettingsChoice c = renderSettingsFor(SourceMode::SelectedItems, 1.0); CHECK((c.settings & kRenderSelItems) != 0); CHECK((c.settings & kRenderSingleFile) != 0); CHECK(c.supported); } static void testRazorSingleFile() { // Razor edits render to ONE file (same single-file rationale as items). RenderSettingsChoice c = renderSettingsFor(SourceMode::RazorArea, 1.0); CHECK((c.settings & kRenderRazorEdits) != 0); CHECK((c.settings & kRenderSingleFile) != 0); CHECK(c.supported); } static void testRealtimeIsUnsupportedOffline() { // The realtime mode is not an offline-render source — must report unsupported // so the offline backend refuses it rather than rendering the master mix. CHECK(!renderSettingsFor(SourceMode::Realtime, 1.0).supported); } static void testEveryRenderSourceLabelIsPinnedVerbatim() { // docs/VERIFICATION.md asks Daniel to report the refusal's `Render source:` line // back verbatim, so every label is pinned to its literal — a typo in any of them // breaks the report that quotes it, and only a literal catches that. CHECK(std::strcmp(renderSourceLabel(SourceMode::MasterMix), "master mix") == 0); CHECK(std::strcmp(renderSourceLabel(SourceMode::TimeSelection), "master mix") == 0); CHECK(std::strcmp(renderSourceLabel(SourceMode::SelectedTracks), "selected tracks via master") == 0); CHECK(std::strcmp(renderSourceLabel(SourceMode::SelectedItems), "selected media items") == 0); CHECK(std::strcmp(renderSourceLabel(SourceMode::RazorArea), "razor edits") == 0); CHECK(std::strcmp(renderSourceLabel(SourceMode::Realtime), "realtime record") == 0); } static void testLabelsSeparateExactlyWhatTheRenderSeparates() { // The labels partition the offline modes the way RENDER_SETTINGS does, and no // finer: same bits => same words (MasterMix/TimeSelection both render the master // mix, custom-time-bounded), different bits => different words. Naming two modes // apart that render identically would put a distinction in a bug report that does // not exist in the render. const SourceMode offline[] = { SourceMode::MasterMix, SourceMode::TimeSelection, SourceMode::SelectedTracks, SourceMode::SelectedItems, SourceMode::RazorArea, }; constexpr std::size_t n = sizeof(offline) / sizeof(offline[0]); for (std::size_t i = 0; i < n; ++i) { const char* label = renderSourceLabel(offline[i]); CHECK(label != nullptr && label[0] != '\0'); CHECK(std::strcmp(label, "unknown") != 0); CHECK(renderSettingsFor(offline[i], 1.0).supported); for (std::size_t j = i + 1; j < n; ++j) { const bool sameRender = renderSettingsFor(offline[i], 1.0).settings == renderSettingsFor(offline[j], 1.0).settings; const bool sameLabel = std::strcmp(label, renderSourceLabel(offline[j])) == 0; CHECK(sameRender == sameLabel); } } // Realtime is not an offline render source at all, so it names its own mechanism // rather than a RENDER_SETTINGS value — outside the partition above by design. CHECK(!renderSettingsFor(SourceMode::Realtime, 1.0).supported); CHECK(std::strcmp(renderSourceLabel(SourceMode::Realtime), renderSourceLabel(SourceMode::MasterMix)) != 0); } // --- tail: TailMode -> RENDER_* mapping (docs/product/capture-tail.md) -------- // The tail assertions below are about the MODE's mapping; the one value that also // depends on the bounds channel has its own test, so they all pin the channel that // every shipped capture rendered on. static TailRenderSettings tailFor(TailMode mode, double manualTailMs) { return tailRenderSettingsFor(mode, manualTailMs, RenderBoundsChannel::CustomTimeBounds); } static void testTailNoneIsExactBounds() { // None -> exact bounds, byte-identical to the pre-tail capture: tail flag clear, // 0 ms, disable-all normalize (the current default), no trim. Asserting the exact // bit values (not just "some value") pins the byte-identical contract: if the // mapping regressed to set a tail bit or a non-disable-all normalize, this fails. TailRenderSettings t = tailFor(TailMode::None, 0.0); CHECK(t.tailFlag == kTailFlagNone); // 0 CHECK(t.tailMs == 0.0); CHECK(t.normalize == kNormalizeDisableAll); // 262144 CHECK(t.trimEnd == 0.0); // manualTailMs must be ignored for None (a stray tail from a leftover ms is the bug). TailRenderSettings t2 = tailFor(TailMode::None, 5000.0); CHECK(t2.tailFlag == kTailFlagNone); CHECK(t2.tailMs == 0.0); } static void testTailAutoIsSurgicalTrim() { // Auto -> custom-bounds tail bit, 8 s cap, SURGICAL normalize (ONLY &32768), and // the -72 dB TRIMEND ratio. The disable-all bit must NOT be set (it is semantically // opposed to trim — this assertion catches a regression to the None normalize). TailRenderSettings t = tailFor(TailMode::Auto, 0.0); CHECK(t.tailFlag == kTailFlagCustomBounds); // &1 CHECK(t.tailMs == kMaxTailMs); // 8000 CHECK(t.normalize == kNormalizeTrimEnd); // exactly 32768, nothing else CHECK((t.normalize & kNormalizeDisableAll) == 0); // disable-all is NOT set // TRIMEND is the derived -72 dB ratio ~= 0.00025119 (the DAW-confirm value). CHECK(std::fabs(t.trimEnd - 0.00025119) < 1e-8); // manualTailMs is ignored for Auto (Auto always uses the 8 s cap). CHECK(tailFor(TailMode::Auto, 3000.0).tailMs == kMaxTailMs); } static void testAutoTrimRatioDerivesFromDb() { // The ratio must DERIVE from the -72 dB constant (10^(dB/20)), not be a hardcoded // float — recompute it independently and require an exact match with the mapping. double expected = std::pow(10.0, kAutoTrimThresholdDb / 20.0); CHECK(autoTrimEndRatio() == expected); CHECK(tailFor(TailMode::Auto, 0.0).trimEnd == expected); // Sanity: -72 dB is well below unity but above zero. CHECK(expected > 0.0 && expected < 0.001); } static void testTailManualFixedNoTrim() { // Manual -> custom-bounds tail, the requested ms (within cap), disable-all // normalize (no trim). A Manual capture is a fixed tail, so it keeps today's // disable-all exactly like the no-tail path. TailRenderSettings t = tailFor(TailMode::Manual, 2500.0); CHECK(t.tailFlag == kTailFlagCustomBounds); CHECK(t.tailMs == 2500.0); CHECK(t.normalize == kNormalizeDisableAll); CHECK(t.trimEnd == 0.0); } static void testTailManualClampsToCap() { // The 8 s cap is a runaway guard that applies to Manual too: ms > 8000 -> 8000. CHECK(tailFor(TailMode::Manual, 9000.0).tailMs == kMaxTailMs); CHECK(tailFor(TailMode::Manual, 8000.0).tailMs == kMaxTailMs); // Below the cap is passed through unchanged. CHECK(tailFor(TailMode::Manual, 100.0).tailMs == 100.0); // A negative request floors to 0 (no negative tail leaks into RENDER_TAILMS). CHECK(tailFor(TailMode::Manual, -50.0).tailMs == 0.0); } // --- bounds channel: RENDER_BOUNDSFLAG mode + the tail bit it drags along ------ static void testEachChannelNamesItsOwnBoundsFlagMode() { // The two RENDER_BOUNDSFLAG values, as literals from the SDK header — 0 = custom // time bounds, 2 = time selection. Pinned as numbers so a renumbering of the enum // cannot silently point a capture at "entire project" or "selected media items". CHECK(renderBoundsFlagFor(RenderBoundsChannel::CustomTimeBounds) == 0); CHECK(renderBoundsFlagFor(RenderBoundsChannel::TimeSelection) == 2); } static void testTailBitFollowsTheBoundsChannel() { // RENDER_TAILFLAG's bits are per-bounds-mode: &1 covers custom time bounds, &4 // covers the time selection. A tail set under the other channel's bit renders no // tail at all, which is why the mapping takes the channel rather than trusting a // caller to OR the right one in. CHECK(tailFlagBitFor(RenderBoundsChannel::CustomTimeBounds) == 1); CHECK(tailFlagBitFor(RenderBoundsChannel::TimeSelection) == 4); // Both tail-bearing modes follow it — a fix applied to Auto alone would leave // Manual rendering under a bit the bounds mode does not read. for (TailMode mode : {TailMode::Auto, TailMode::Manual}) { CHECK(tailRenderSettingsFor(mode, 2500.0, RenderBoundsChannel::CustomTimeBounds) .tailFlag == kTailFlagCustomBounds); CHECK(tailRenderSettingsFor(mode, 2500.0, RenderBoundsChannel::TimeSelection) .tailFlag == kTailFlagTimeSelection); } } static void testNoneSetsNoTailBitOnEitherChannel() { // None is exact bounds on every channel: no tail bit, so no channel's bit either. CHECK(tailRenderSettingsFor(TailMode::None, 5000.0, RenderBoundsChannel::CustomTimeBounds) .tailFlag == kTailFlagNone); CHECK(tailRenderSettingsFor(TailMode::None, 5000.0, RenderBoundsChannel::TimeSelection) .tailFlag == kTailFlagNone); } static void testTheChannelLabelNamesTheModeAndItsStore() { // The console verdict is read by someone deciding which channel to keep, so the // label has to name both the mode number and where the window actually went. const std::string custom = renderBoundsChannelLabel(RenderBoundsChannel::CustomTimeBounds); CHECK(custom.find("RENDER_BOUNDSFLAG=0") != std::string::npos); CHECK(custom.find("RENDER_STARTPOS") != std::string::npos); const std::string ts = renderBoundsChannelLabel(RenderBoundsChannel::TimeSelection); CHECK(ts.find("RENDER_BOUNDSFLAG=2") != std::string::npos); CHECK(ts.find("GetSet_LoopTimeRange") != std::string::npos); // Two channels that read alike in the console would make the experiment unreadable. CHECK(custom != ts); } // --- realtimeRecordWindowEnd: the T2 record-window extension ----------------- static void testRealtimeWindowNoneIsExact() { // None -> the exact range end, no extra recording (byte-identical to today). CHECK(realtimeRecordWindowEnd(TailMode::None, 12.5, 2000.0) == 12.5); // manualTailMs is ignored for None. CHECK(realtimeRecordWindowEnd(TailMode::None, 12.5, 0.0) == 12.5); } static void testRealtimeWindowAutoAddsCap() { // Auto -> range end + the 8 s runaway cap (trimmed later by the decay scan). CHECK(realtimeRecordWindowEnd(TailMode::Auto, 10.0, 0.0) == 10.0 + kMaxTailSeconds); // manualTailMs is ignored for Auto (the cap is fixed). CHECK(realtimeRecordWindowEnd(TailMode::Auto, 10.0, 3000.0) == 10.0 + kMaxTailSeconds); } static void testRealtimeWindowManualAddsClampedLength() { // Manual -> range end + the set length in seconds (fixed, no trim). CHECK(realtimeRecordWindowEnd(TailMode::Manual, 5.0, 2000.0) == 5.0 + 2.0); // Clamped to the 8 s cap: > 8000 ms -> +8 s. CHECK(realtimeRecordWindowEnd(TailMode::Manual, 5.0, 9000.0) == 5.0 + kMaxTailSeconds); // Negative floors to 0 -> no extra window (never records before the range end). CHECK(realtimeRecordWindowEnd(TailMode::Manual, 5.0, -100.0) == 5.0); } // --- parseRazorEdits: P_RAZOREDITS string -> ranges -------------------------- static void testParseSingleTrackAudioArea() { // One track-audio triple: start end "" (empty quoted GUID = track audio). auto r = parseRazorEdits("1.5 3.25 \"\""); CHECK(r.size() == 1); CHECK(r[0].startSeconds == 1.5); CHECK(r[0].endSeconds == 3.25); } static void testParseMultipleAreas() { auto r = parseRazorEdits("0.0 1.0 \"\" 2.0 4.0 \"\""); CHECK(r.size() == 2); CHECK(r[0].startSeconds == 0.0 && r[0].endSeconds == 1.0); CHECK(r[1].startSeconds == 2.0 && r[1].endSeconds == 4.0); } static void testParseSkipsEnvelopeLaneAreas() { // A triple whose GUID is a real {…} is an ENVELOPE-lane area — skipped, since // razor captures render track audio only. Only the track-audio triple survives. auto r = parseRazorEdits( "1.0 2.0 \"\" 3.0 4.0 {AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}"); CHECK(r.size() == 1); CHECK(r[0].startSeconds == 1.0 && r[0].endSeconds == 2.0); } static void testParseEmptyAndMalformed() { CHECK(parseRazorEdits("").empty()); // Empty/inverted range dropped (end <= start). CHECK(parseRazorEdits("5.0 5.0 \"\"").empty()); CHECK(parseRazorEdits("5.0 1.0 \"\"").empty()); // Trailing garbage token in a time field -> that triple dropped, not a crash. CHECK(parseRazorEdits("1.0x 2.0 \"\"").empty()); // A dangling partial triple (missing GUID token) is ignored. CHECK(parseRazorEdits("1.0 2.0").empty()); } static void testRazorUnionBounds() { // Union = min start .. max end across all areas (the exact render window). std::vector ranges = {{2.0, 3.0}, {0.5, 1.0}, {4.0, 6.5}}; RazorRange u = razorUnionBounds(ranges); CHECK(u.startSeconds == 0.5); CHECK(u.endSeconds == 6.5); // Empty -> {0,0} sentinel (caller treats as "no razor area"). RazorRange empty = razorUnionBounds({}); CHECK(empty.startSeconds == 0.0 && empty.endSeconds == 0.0); } // --- sourceModeForScope: scope -> render source mode ------------------------- static void testScopeSourceModes() { // Track scope always renders its selected tracks (there is no master scope — to // capture the master you render a track), whatever the window. CHECK(sourceModeForScope(CaptureScope::Track, true) == SourceMode::SelectedTracks); CHECK(sourceModeForScope(CaptureScope::Track, false) == SourceMode::SelectedTracks); // Item scope renders the selected items only when their extent already prints // the requested window. CHECK(sourceModeForScope(CaptureScope::Item, true) == SourceMode::SelectedItems); // Every scope's source mode is an offline-supported render source. CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Item, true), 1.0).supported); CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Track, true), 1.0).supported); } static void testRangedItemScopeRendersTimeBounded() { // The widening defect pinned at the bit level. A window the item extent does NOT // print must never reach the &32 selected-items source: that source is INFERRED // (unverified — src/core/capture/CLAUDE.md §Gotchas) to take its bounds from the // item extents, so RENDER_STARTPOS/ENDPOS cannot narrow it and the capture widens // to the whole item. The ranged item capture renders through the selected-tracks // source instead, which IS time-bounded. const SourceMode ranged = sourceModeForScope(CaptureScope::Item, false); CHECK(ranged == SourceMode::SelectedTracks); const RenderSettingsChoice c = renderSettingsFor(ranged, 1.0); CHECK(c.supported); CHECK((c.settings & kRenderSelItems) == 0); // the widening bit is absent CHECK((c.settings & kRenderSingleFile) == 0); // and its single-file companion CHECK(c.settings == kRenderSelTracksViaMaster); // The regression floor, at the same resolution: an item capture whose window IS // the item extent still renders through &32 | single-file, unchanged. const RenderSettingsChoice floorCase = renderSettingsFor(sourceModeForScope(CaptureScope::Item, true), 1.0); CHECK((floorCase.settings & kRenderSelItems) != 0); CHECK((floorCase.settings & kRenderSingleFile) != 0); // FX scope is orthogonal to the re-source: a ranged item capture still hears // take/item FX only (this is what makes the swap safe). CHECK(fxBypassPlanFor(CaptureScope::Item).bypassSelfFx); } static void testMultiTrackStemRenderIsNamedForRefusal() { // The one shape that cannot land: a selected-tracks render over more than one // track. That source is INFERRED to render one file per track with no single-file // bit available (unverified; see src/shell/capture/CLAUDE.md §Gotchas for what that // inference rests on), so N stems would collapse onto one render pattern and one // track's audio would land as a successful capture. CHECK(isMultiTrackStemRender(SourceMode::SelectedTracks, 2)); CHECK(isMultiTrackStemRender(SourceMode::SelectedTracks, 7)); // One track is the common case for BOTH scopes — it must still render. This is the // regression floor for the plain single-track track capture. CHECK(!isMultiTrackStemRender(SourceMode::SelectedTracks, 1)); CHECK(!isMultiTrackStemRender(SourceMode::SelectedTracks, 0)); // A full-extent item capture keeps the selected-items source, whose single-file // bit already sums a multi-track item selection into one file. CHECK(!isMultiTrackStemRender(SourceMode::SelectedItems, 3)); // Razor's single-file bit does the same; master mix is one file by definition. CHECK(!isMultiTrackStemRender(SourceMode::RazorArea, 3)); CHECK(!isMultiTrackStemRender(SourceMode::MasterMix, 3)); // Realtime never reaches the offline render at all (sends sum into one temp track). CHECK(!isMultiTrackStemRender(SourceMode::Realtime, 3)); // The predicate is reachable from the mappings it guards: BOTH the ranged item // capture's source mode and the track scope's resolve to the one it names, while a // full-extent item capture does not. CHECK(isMultiTrackStemRender(sourceModeForScope(CaptureScope::Item, false), 2)); CHECK(isMultiTrackStemRender(sourceModeForScope(CaptureScope::Track, false), 2)); CHECK(isMultiTrackStemRender(sourceModeForScope(CaptureScope::Track, true), 2)); CHECK(!isMultiTrackStemRender(sourceModeForScope(CaptureScope::Item, true), 2)); } static void testSourceBypassesBoundsChannelOnlyForContentDerivedSources() { // SelectedItems (&32) and RazorArea (&4096) derive their bounds from the // selected items'/areas' own extents -- RENDER_BOUNDSFLAG is never consulted, so // a bounds-channel verdict is not evidence for either (the regression this // predicate exists to catch: RunBatchCaptureItems always renders through // SelectedItems, so this false-EXACT would fire on every batch-item capture). CHECK(sourceBypassesBoundsChannel(SourceMode::SelectedItems)); CHECK(sourceBypassesBoundsChannel(SourceMode::RazorArea)); // Every other source is genuinely time-bounded through RENDER_STARTPOS/ENDPOS or // the time selection, so the channel IS the evidence for these. CHECK(!sourceBypassesBoundsChannel(SourceMode::MasterMix)); CHECK(!sourceBypassesBoundsChannel(SourceMode::TimeSelection)); CHECK(!sourceBypassesBoundsChannel(SourceMode::SelectedTracks)); CHECK(!sourceBypassesBoundsChannel(SourceMode::Realtime)); } static void testRefusalMessagesAreSiblingsWithDistinctExits() { const std::string item = multiTrackRefusalMessage(CaptureScope::Item); const std::string track = multiTrackRefusalMessage(CaptureScope::Track); // Both name the same reason — a shape that cannot land as one file — so a user who // hits the mistake in either scope reads one story, not two. CHECK(item.find("single file") != std::string::npos); CHECK(track.find("single file") != std::string::npos); // And both name a way out. The shared one is "one track at a time"; each scope then // adds the exit only it has (widen the range / capture the folder). CHECK(item.find("one track's items at a time") != std::string::npos); CHECK(item.find("range match the items' extent") != std::string::npos); CHECK(track.find("one track at a time") != std::string::npos); CHECK(track.find("folder") != std::string::npos); // Distinct texts: the track message must not be the item message's wording about // items and ranges, which would misdescribe what the user actually did. CHECK(item != track); CHECK(track.find("selected items") == std::string::npos); } // Golden literals: docs/verify-track-scope-multitrack.md §2 quotes the track message as // an exact console match. A substring check alone leaves that doc free to drift from // whatever ships, so pin both strings byte-for-byte here. static void testRefusalMessagesMatchGoldenLiterals() { CHECK(multiTrackRefusalMessage(CaptureScope::Item) == "This range is narrower than the selected items, so it renders " "through their tracks -- and those items span more than one track, " "which this shape cannot land as a single file. Capture one track's " "items at a time, or make the range match the items' extent."); CHECK(multiTrackRefusalMessage(CaptureScope::Track) == "A track capture renders the selected tracks through the master, " "and more than one track cannot land as a single file. Capture one " "track at a time, or route them into a folder/bus track and capture " "that (a folder's own output is its children summed)."); } // --- inferRangeSource: razor-else-time (orthogonal to scope) ----------------- static void testRangeInference() { // Razor present -> razor union wins; no razor -> time selection. CHECK(inferRangeSource(true) == RangeSource::Razor); CHECK(inferRangeSource(false) == RangeSource::TimeSelection); } // --- fxBypassPlanFor: the FX-scope invariant ---------------------------------- static void testItemScopeBypassesEverythingButTake() { // Item = take/item FX ONLY. Bypass the item's own track FX, its ancestors, and // the master. (If this returned bypassSelfFx=false the M7 defect — items heard // through the track's FX — would recur; the assertion pins the fix.) FxBypassPlan p = fxBypassPlanFor(CaptureScope::Item); CHECK(p.bypassSelfFx); CHECK(p.bypassAncestorFx); CHECK(p.bypassMaster); } static void testTrackScopeKeepsSelfBypassesAncestorsAndMaster() { // Track = item FX + the track's OWN FX. Keep self FX; bypass ancestors + master. // Master stays a bypass target even though it is no longer a capture scope. FxBypassPlan p = fxBypassPlanFor(CaptureScope::Track); CHECK(!p.bypassSelfFx); // the whole point: the track's own FX stays live CHECK(p.bypassAncestorFx); // no parent/folder FX CHECK(p.bypassMaster); // no master FX } // --- captureActionTable: the scope taxonomy ---------------------------------- static void testTableHasBothScopes() { const auto& table = captureActionTable(); // Two rows: item + track. No master scope, and NO tail variants — tail is a // panel setting the capture reads at fire time, not a per-action row. CHECK(table.size() == 2); std::set ids; int item = 0, track = 0; for (const auto& def : table) { // Every command SUFFIX (Phase V, V4 — the channel prefix is prepended by the shell) // is a non-empty, UNIQUE string (duplicate suffixes would collide once composed). std::string suffix = def.commandSuffix; CHECK(!suffix.empty()); // The suffix is NOT prefixed with the channel family here — that is composed at // register time. A leftover "CEREBELLUM_REASAMPLER_" in the table would be a // double-prefix bug, so assert its ABSENCE. CHECK(suffix.rfind("CEREBELLUM_REASAMPLER_", 0) != 0); CHECK(ids.insert(suffix).second); // false if duplicate // Every scope resolves to a supported offline source, on BOTH the // extent-prints-the-window path and the time-bounded one. CHECK(renderSettingsFor(sourceModeForScope(def.scope, true), 1.0).supported); CHECK(renderSettingsFor(sourceModeForScope(def.scope, false), 1.0).supported); if (def.scope == CaptureScope::Item) ++item; if (def.scope == CaptureScope::Track) ++track; } // Exactly one row per scope — no dupes, no gaps, no tail variants. CHECK(item == 1); CHECK(track == 1); } static void testScopeActionIdsAreTheShippedStrings() { // Pin the shipped CAPTURE_ITEM / CAPTURE_TRACK SUFFIXES so a future edit that silently // changes them (breaking user keybindings once composed with the channel prefix) fails // the gate. The full stable id is prefix + suffix ("CEREBELLUM_REASAMPLER_CAPTURE_ITEM"). const auto& table = captureActionTable(); std::string itemId, trackId; for (const auto& def : table) { if (def.scope == CaptureScope::Item) itemId = def.commandSuffix; if (def.scope == CaptureScope::Track) trackId = def.commandSuffix; } CHECK(itemId == "CAPTURE_ITEM"); CHECK(trackId == "CAPTURE_TRACK"); } int main() { testMasterMixWet(); testSelectedTracksWet(); testSelectedItemsSingleFile(); testRazorSingleFile(); testRealtimeIsUnsupportedOffline(); testEveryRenderSourceLabelIsPinnedVerbatim(); testLabelsSeparateExactlyWhatTheRenderSeparates(); testTailNoneIsExactBounds(); testTailAutoIsSurgicalTrim(); testAutoTrimRatioDerivesFromDb(); testTailManualFixedNoTrim(); testTailManualClampsToCap(); testEachChannelNamesItsOwnBoundsFlagMode(); testTailBitFollowsTheBoundsChannel(); testNoneSetsNoTailBitOnEitherChannel(); testTheChannelLabelNamesTheModeAndItsStore(); testRealtimeWindowNoneIsExact(); testRealtimeWindowAutoAddsCap(); testRealtimeWindowManualAddsClampedLength(); testParseSingleTrackAudioArea(); testParseMultipleAreas(); testParseSkipsEnvelopeLaneAreas(); testParseEmptyAndMalformed(); testRazorUnionBounds(); testScopeSourceModes(); testRangedItemScopeRendersTimeBounded(); testMultiTrackStemRenderIsNamedForRefusal(); testSourceBypassesBoundsChannelOnlyForContentDerivedSources(); testRefusalMessagesAreSiblingsWithDistinctExits(); testRefusalMessagesMatchGoldenLiterals(); testRangeInference(); testItemScopeBypassesEverythingButTake(); testTrackScopeKeepsSelfBypassesAncestorsAndMaster(); testTableHasBothScopes(); testScopeActionIdsAreTheShippedStrings(); if (g_fail == 0) std::printf("render_settings: all tests passed\n"); else std::printf("render_settings: %d CHECK(s) FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }