// Bug 4: Non-reducible minimums prevent idle concurrency from reaching zero. // Even when shouldAllowZeroPostIngestTargets() returns true, multiple minimum // floors prevent actual zero concurrency: // // 1. TuneAdvisor::postIngestTotalConcurrent() clamps to min 2 (line 1946) // 2. Budget allocator guarantees 1 slot per active stage when budget >= activeStages (line 2414) // 3. Extraction + Embed hardcoded minimum of 1 each when budget >= 2 (lines 2460-2466) // 4. ONNX slots minimum of 2 (TuningManager.cpp:336,352) // 5. Ingest workers minimum of 1 (TuningManager.cpp:433-434) // // TDD Red Phase: Tests assert that idle daemon can reach zero concurrency. // They FAIL against current code where minimums are unconditional. #include #include #include #include #include using namespace yams::daemon; namespace { class ProfileGuard { TuneAdvisor::Profile prev_; public: explicit ProfileGuard(TuneAdvisor::Profile profile) : prev_(TuneAdvisor::tuningProfile()) { TuneAdvisor::setTuningProfile(profile); } ~ProfileGuard() { TuneAdvisor::setTuningProfile(prev_); } ProfileGuard(const ProfileGuard&) = delete; ProfileGuard& operator=(const ProfileGuard&) = delete; }; class HwGuard { unsigned prev_; public: explicit HwGuard(unsigned hw) : prev_(TuneAdvisor::hardwareConcurrency()) { TuneAdvisor::setHardwareConcurrencyForTests(hw); } ~HwGuard() { TuneAdvisor::setHardwareConcurrencyForTests(prev_); } HwGuard(const HwGuard&) = delete; HwGuard& operator=(const HwGuard&) = delete; }; void resetPostIngestOverridesAndCaps() { TuneAdvisor::setPostExtractionConcurrent(0); TuneAdvisor::setPostKgConcurrent(0); TuneAdvisor::setPostSymbolConcurrent(0); TuneAdvisor::setPostEntityConcurrent(0); TuneAdvisor::setPostTitleConcurrent(0); TuneAdvisor::setPostEmbedConcurrent(0); // Clear dynamic caps with the production unset sentinel. TuneAdvisor::setPostExtractionConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostKgConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostSymbolConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostEntityConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostTitleConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostEmbedConcurrentDynamicCap(UINT32_MAX); } void setAllPostIngestStagesActive() { TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Extraction, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::KnowledgeGraph, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Symbol, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Entity, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Title, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Embed, true); } } // namespace // ============================================================================= // Test Group A: Budget allocator minimum floors (TuneAdvisor.h) // ============================================================================= TEST_CASE("Budget allocator allows zero per-stage when all DynamicCaps are zero", "[daemon][tune][idle][minimums][catch2]") { // This test verifies that the budget allocator in TuneAdvisor can produce // zero-allocation results when DynamicCaps cap every stage to 0. // Currently, the allocator gives minimum 1 per active stage (Bug 4, point 2). // After Bug 1 fix (sentinel change), DynamicCap(0) will cap stages to 0. // After Bug 4 fix, the allocator won't force minimums on zero-capped stages. resetPostIngestOverridesAndCaps(); setAllPostIngestStagesActive(); HwGuard hwGuard(16); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); TuneAdvisor::setPostIngestTotalConcurrent(12); // Cap all stages to 0 via DynamicCap TuneAdvisor::beginDynamicCapWrite(); TuneAdvisor::setPostExtractionConcurrentDynamicCap(0); TuneAdvisor::setPostKgConcurrentDynamicCap(0); TuneAdvisor::setPostSymbolConcurrentDynamicCap(0); TuneAdvisor::setPostEntityConcurrentDynamicCap(0); TuneAdvisor::setPostTitleConcurrentDynamicCap(0); TuneAdvisor::setPostEmbedConcurrentDynamicCap(0); TuneAdvisor::endDynamicCapWrite(); auto budget = TuneAdvisor::testing_postIngestBudget(/*includeDynamicCaps=*/true); // BUG: allocator guarantees 1 per active stage + extraction/embed hardcoded min // FIXED: all stages return 0 when their DynamicCap is 0 CHECK(budget.extraction == 0); CHECK(budget.kg == 0); CHECK(budget.symbol == 0); CHECK(budget.entity == 0); CHECK(budget.title == 0); CHECK(budget.embed == 0); } TEST_CASE("Extraction + embed hardcoded minimum removed when caps are zero", "[daemon][tune][idle][minimums][catch2]") { // Lines 2460-2466: "if (totalBudget >= 2) { if extraction alloc == 0 && cap > 0: alloc = 1 }" // This hardcoded minimum prevents extraction and embed from reaching zero. // After fix, caps of 0 must override the hardcoded minimum. resetPostIngestOverridesAndCaps(); setAllPostIngestStagesActive(); HwGuard hwGuard(16); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); TuneAdvisor::setPostIngestTotalConcurrent(12); // Only cap extraction and embed to 0 — leave others uncapped TuneAdvisor::beginDynamicCapWrite(); TuneAdvisor::setPostExtractionConcurrentDynamicCap(0); TuneAdvisor::setPostEmbedConcurrentDynamicCap(0); TuneAdvisor::endDynamicCapWrite(); // BUG: hardcoded minimum forces extraction=1, embed=1 even with cap=0 // FIXED: extraction=0, embed=0 CHECK(TuneAdvisor::postExtractionConcurrent() == 0); CHECK(TuneAdvisor::postEmbedConcurrent() == 0); } // ============================================================================= // Test Group B: postIngestTotalConcurrent minimum (TuneAdvisor.h:1946) // ============================================================================= TEST_CASE("postIngestTotalConcurrent can return 1 on small systems", "[daemon][tune][idle][minimums][catch2]") { // Line 1946: std::clamp(total, 2u, hw) // The minimum of 2 prevents single-threaded idle behavior. // After fix: minimum should be 1 (or allow 0 through explicit override). // For a 2-core system with Efficient profile, base = max(2, ceil(2*0.2)) = 2 // which already satisfies the clamp. But the explicit floor of 2 prevents // postIngestTotalConcurrent from ever being overridden below 2. resetPostIngestOverridesAndCaps(); setAllPostIngestStagesActive(); // With override set to 1, verify it's honored HwGuard hwGuard(8); ProfileGuard profileGuard(TuneAdvisor::Profile::Efficient); TuneAdvisor::setPostIngestTotalConcurrent(1); // BUG: setPostIngestTotalConcurrent clamps min to 1, but the // postIngestTotalConcurrent() getter at line 1946 clamps output to min 2. // Actually, the override path at line 1915-1917 returns ov directly if > 0. // So override=1 → returns 1. But the default computed path clamps to min 2. // Let's test the override path: it should return 1. CHECK(TuneAdvisor::postIngestTotalConcurrent() == 1); } // ============================================================================= // Test Group C: TuningManager idle allows zero targets (integration with // shouldAllowZeroPostIngestTargets) // ============================================================================= TEST_CASE("shouldAllowZeroPostIngestTargets returns true when idle with no work", "[daemon][tune][idle][minimums][catch2]") { // Existing: verified in tuning_reconciliation_catch2_test.cpp // Reconfirm here for completeness CHECK(TuningManager::testing_shouldAllowZeroPostIngestTargets( /*daemonIdle=*/true, /*postIngestBusy=*/false) == true); } TEST_CASE("ForceIdle mode enables zero targets regardless of daemon state", "[daemon][tune][idle][minimums][catch2]") { TuningManager::testing_setPostIngestScaleTestMode( TuningManager::PostIngestScaleTestMode::ForceIdle); CHECK(TuningManager::testing_shouldAllowZeroPostIngestTargets( /*daemonIdle=*/false, /*postIngestBusy=*/true) == true); // Reset TuningManager::testing_setPostIngestScaleTestMode( TuningManager::PostIngestScaleTestMode::Normal); }