// Catch2 migration of daemon_test.cpp // Migration: yams-3s4 (daemon unit tests) // Unit tests for YamsDaemon - creation, start/stop, signal handling, error paths #include #include #include #include #include #include #include #include #include #include #include #include #include using nlohmann::json; #include #include #include #include #include #include #include #include #include #include "tests/common/test_helpers_catch2.h" #ifdef _WIN32 #include #include #include using pid_t = int; #else #include #include #include #include #endif namespace fs = std::filesystem; using namespace std::chrono_literals; using namespace yams; using namespace yams::daemon; namespace yams::daemon::test { // Test fixture for Daemon tests struct DaemonFixture { DaemonConfig config_; std::unique_ptr daemon_; fs::path runtime_root_; std::thread runLoopThread_; std::vector environment_; DaemonFixture() { #ifdef _WIN32 fs::path tmp = fs::temp_directory_path() / "yams_test"; #else fs::path tmp{"/tmp"}; #endif auto unique_suffix = std::to_string(::getpid()) + "_" + std::to_string(static_cast(reinterpret_cast(this) & 0xffff)); runtime_root_ = tmp / ("ydtest_" + unique_suffix); config_.dataDir = runtime_root_ / "data"; config_.socketPath = runtime_root_ / "sock"; config_.pidFile = runtime_root_ / "daemon.pid"; config_.logFile = runtime_root_ / "daemon.log"; // When ABI plugins are disabled, no model provider can be adopted, // so disable the requirement to avoid init failure. if (const auto value = yams::config::getenv_nonempty("YAMS_DISABLE_ABI_PLUGINS"); value && *value == "1") { config_.enableModelProvider = false; } else { config_.enableModelProvider = true; } config_.autoLoadPlugins = true; config_.modelPoolConfig.lazyLoading = true; config_.modelPoolConfig.preloadModels.clear(); environment_.emplace_back("YAMS_DB_OPEN_TIMEOUT_MS", std::string{"1500"}); environment_.emplace_back("YAMS_DB_MIGRATE_TIMEOUT_MS", std::string{"2000"}); environment_.emplace_back("YAMS_SEARCH_BUILD_TIMEOUT_MS", std::string{"1500"}); environment_.emplace_back("YAMS_DISABLE_VECTORS", std::string{"1"}); environment_.emplace_back("YAMS_DISABLE_SESSION_WATCHER", std::string{"1"}); std::error_code se; fs::create_directories(config_.dataDir, se); environment_.emplace_back("YAMS_RUNTIME_DIR", runtime_root_.string()); environment_.emplace_back("YAMS_SOCKET_PATH", config_.socketPath.string()); environment_.emplace_back("YAMS_PID_FILE", config_.pidFile.string()); } ~DaemonFixture() { stopRunLoop(); if (daemon_) { daemon_->stop(); daemon_.reset(); } cleanupDaemonFiles(); } void startRunLoop() { if (!daemon_ || runLoopThread_.joinable()) { return; } runLoopThread_ = std::thread([this]() { daemon_->runLoop(); }); std::this_thread::sleep_for(50ms); } void stopRunLoop() { if (daemon_ && daemon_->isRunning()) { daemon_->requestStop(); } if (runLoopThread_.joinable()) { runLoopThread_.join(); } } void cleanupDaemonFiles() { std::error_code ec; if (!runtime_root_.empty()) { fs::remove_all(runtime_root_, ec); } } static bool isSocketPermissionDenied(const yams::Error& error) { const std::string_view message{error.message}; return message.find("Operation not permitted") != std::string_view::npos || message.find("Permission denied") != std::string_view::npos; } bool waitForDaemonReady(std::chrono::milliseconds timeout = 5000ms) { auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { const auto& state = daemon_->getState(); if (state.readiness.contentStoreReady.load()) { return true; } std::this_thread::sleep_for(50ms); } return false; } static bool waitForCondition(std::chrono::milliseconds timeout, const std::function& predicate) { auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { if (predicate()) { return true; } std::this_thread::sleep_for(10ms); } return predicate(); } }; #ifdef _WIN32 #define SKIP_ON_WINDOWS() SKIP("Daemon IPC tests unstable on Windows") #else #define SKIP_ON_WINDOWS() ((void)0) #endif TEST_CASE_METHOD(DaemonFixture, "Daemon creation and destruction", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); REQUIRE(daemon_ != nullptr); REQUIRE_FALSE(daemon_->isRunning()); } TEST_CASE_METHOD(DaemonFixture, "Daemon starts repair service while provider-degraded", "[daemon][lifecycle][repair]") { SKIP_ON_WINDOWS(); yams::test::ScopedEnvVar skipModelLoading("YAMS_SKIP_MODEL_LOADING", "1"); yams::test::ScopedEnvVar embeddingBackend("YAMS_EMBED_BACKEND", "daemon"); config_.enableModelProvider = true; config_.modelProviderRequired = false; config_.autoLoadPlugins = false; daemon_ = std::make_unique(config_); auto startResult = daemon_->start(); if (!startResult && isSocketPermissionDenied(startResult.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(startResult); startRunLoop(); REQUIRE(waitForCondition(5s, [&] { return !daemon_->getState().readiness.modelProviderReady.load(std::memory_order_acquire); })); REQUIRE(waitForCondition( 2s, [&] { return daemon_->getServiceManager()->getRepairServiceShared() != nullptr; })); } TEST_CASE_METHOD(DaemonFixture, "Daemon restores compatibility path environment", "[daemon][lifecycle][environment]") { yams::test::ScopedEnvVar storage("YAMS_STORAGE", "/before/storage"); yams::test::ScopedEnvVar data("YAMS_DATA_DIR", "/before/data"); yams::test::ScopedEnvVar config("YAMS_CONFIG", "/before/config.toml"); const auto compatibilityConfig = runtime_root_ / "before-compatibility.toml"; std::ofstream(compatibilityConfig) << "[daemon]\nmode = \"old\"\n"; yams::test::ScopedEnvVar configCompatibility("YAMS_CONFIG_PATH", compatibilityConfig.string()); yams::test::ScopedEnvVar socket("YAMS_DAEMON_SOCKET", "/before/socket.sock"); yams::test::ScopedEnvVar inDaemon("YAMS_IN_DAEMON", "before"); config_.configFilePath = runtime_root_ / "config.toml"; daemon_ = std::make_unique(config_); CHECK((yams::config::getenv_copy("YAMS_STORAGE") == config_.dataDir.string())); CHECK((yams::config::getenv_copy("YAMS_DATA_DIR") == config_.dataDir.string())); CHECK((yams::config::getenv_copy("YAMS_CONFIG") == config_.configFilePath.string())); CHECK((yams::config::getenv_copy("YAMS_CONFIG_PATH") == config_.configFilePath.string())); CHECK((yams::config::get_config_path() == config_.configFilePath)); CHECK((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == config_.socketPath.string())); CHECK((yams::config::getenv_copy("YAMS_IN_DAEMON") == "1")); daemon_.reset(); CHECK((yams::config::getenv_copy("YAMS_STORAGE") == "/before/storage")); CHECK((yams::config::getenv_copy("YAMS_DATA_DIR") == "/before/data")); CHECK((yams::config::getenv_copy("YAMS_CONFIG") == "/before/config.toml")); CHECK((yams::config::getenv_copy("YAMS_CONFIG_PATH") == compatibilityConfig.string())); CHECK((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == "/before/socket.sock")); CHECK((yams::config::getenv_copy("YAMS_IN_DAEMON") == "before")); } TEST_CASE_METHOD(DaemonFixture, "Daemon environment lease preserves newer writers", "[daemon][lifecycle][environment][ownership]") { yams::test::ScopedEnvVar socket{"YAMS_DAEMON_SOCKET", std::string{"/before/socket.sock"}}; daemon_ = std::make_unique(config_); REQUIRE((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == config_.socketPath.string())); socket.set("/newer/owner.sock"); daemon_.reset(); CHECK((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == "/newer/owner.sock")); } TEST_CASE_METHOD(DaemonFixture, "Daemon environment lease detects same-value newer writers", "[daemon][lifecycle][environment][ownership]") { yams::test::ScopedEnvVar socket{"YAMS_DAEMON_SOCKET", std::string{"/before/socket.sock"}}; daemon_ = std::make_unique(config_); const auto installed = config_.socketPath.string(); REQUIRE((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == installed)); socket.set(installed); daemon_.reset(); CHECK((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == installed)); } TEST_CASE_METHOD(DaemonFixture, "Nested daemons restore every runtime environment lease", "[daemon][lifecycle][environment][ownership][nested]") { std::vector original; original.reserve(6); original.emplace_back("YAMS_IN_DAEMON", std::string{"before-daemon"}); original.emplace_back("YAMS_STORAGE", std::string{"/before/storage"}); original.emplace_back("YAMS_DATA_DIR", std::string{"/before/data"}); original.emplace_back("YAMS_CONFIG", std::string{"/before/config"}); original.emplace_back("YAMS_CONFIG_PATH", std::string{"/before/config-path"}); original.emplace_back("YAMS_DAEMON_SOCKET", std::string{"/before/socket"}); auto outerConfig = config_; outerConfig.dataDir = runtime_root_ / "outer-data"; outerConfig.configFilePath = runtime_root_ / "outer.toml"; outerConfig.socketPath = runtime_root_ / "outer.sock"; outerConfig.pidFile = runtime_root_ / "outer.pid"; auto innerConfig = config_; innerConfig.dataDir = runtime_root_ / "inner-data"; innerConfig.configFilePath = runtime_root_ / "inner.toml"; innerConfig.socketPath = runtime_root_ / "inner.sock"; innerConfig.pidFile = runtime_root_ / "inner.pid"; const auto checkInstalled = [](const DaemonConfig& expected) { CHECK((yams::config::getenv_copy("YAMS_IN_DAEMON") == "1")); CHECK((yams::config::getenv_copy("YAMS_STORAGE") == expected.dataDir.string())); CHECK((yams::config::getenv_copy("YAMS_DATA_DIR") == expected.dataDir.string())); CHECK((yams::config::getenv_copy("YAMS_CONFIG") == expected.configFilePath.string())); CHECK((yams::config::getenv_copy("YAMS_CONFIG_PATH") == expected.configFilePath.string())); CHECK((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == expected.socketPath.string())); }; const auto checkOriginal = [] { CHECK((yams::config::getenv_copy("YAMS_IN_DAEMON") == "before-daemon")); CHECK((yams::config::getenv_copy("YAMS_STORAGE") == "/before/storage")); CHECK((yams::config::getenv_copy("YAMS_DATA_DIR") == "/before/data")); CHECK((yams::config::getenv_copy("YAMS_CONFIG") == "/before/config")); CHECK((yams::config::getenv_copy("YAMS_CONFIG_PATH") == "/before/config-path")); CHECK((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == "/before/socket")); }; SECTION("LIFO destruction resumes the outer daemon") { auto outer = std::make_unique(outerConfig); checkInstalled(outerConfig); auto inner = std::make_unique(innerConfig); checkInstalled(innerConfig); inner.reset(); checkInstalled(outerConfig); outer.reset(); checkOriginal(); } SECTION("non-LIFO destruction rebases the inner daemon onto the original state") { auto outer = std::make_unique(outerConfig); auto inner = std::make_unique(innerConfig); checkInstalled(innerConfig); outer.reset(); checkInstalled(innerConfig); inner.reset(); checkOriginal(); } } TEST_CASE_METHOD(DaemonFixture, "Daemon construction failure releases acquired environment leases", "[daemon][lifecycle][environment][ownership][failure]") { std::vector original; original.reserve(6); original.emplace_back("YAMS_IN_DAEMON", std::string{"before-daemon"}); original.emplace_back("YAMS_STORAGE", std::string{"/before/storage"}); original.emplace_back("YAMS_DATA_DIR", std::string{"/before/data"}); original.emplace_back("YAMS_CONFIG", std::string{"/before/config"}); original.emplace_back("YAMS_CONFIG_PATH", std::string{"/before/config-path"}); original.emplace_back("YAMS_DAEMON_SOCKET", std::string{"/before/socket"}); config_.configFilePath = runtime_root_ / "failure.toml"; yams::config::testing_fail_owned_environment_lease_after(2); try { auto unexpected = std::make_unique(config_); FAIL("YamsDaemon construction unexpectedly succeeded"); } catch (const std::runtime_error& error) { CHECK(std::string_view{error.what()}.starts_with( "YamsDaemon failed to lease environment key YAMS_DATA_DIR")); } CHECK((yams::config::getenv_copy("YAMS_IN_DAEMON") == "before-daemon")); CHECK((yams::config::getenv_copy("YAMS_STORAGE") == "/before/storage")); CHECK((yams::config::getenv_copy("YAMS_DATA_DIR") == "/before/data")); CHECK((yams::config::getenv_copy("YAMS_CONFIG") == "/before/config")); CHECK((yams::config::getenv_copy("YAMS_CONFIG_PATH") == "/before/config-path")); CHECK((yams::config::getenv_copy("YAMS_DAEMON_SOCKET") == "/before/socket")); } TEST_CASE_METHOD(DaemonFixture, "Daemon tuning reload revokes removed overrides coherently", "[daemon][tuning][reload]") { SKIP_ON_WINDOWS(); yams::test::ScopedEnvVar ipcCompatibility{"YAMS_IPC_TIMEOUT_MS", std::nullopt}; yams::test::ScopedEnvVar admissionCompatibility{"YAMS_ADMISSION_CONTROL", std::nullopt}; yams::test::ScopedEnvVar memoryCompatibility{"YAMS_MEMORY_WARNING_PCT", std::nullopt}; yams::test::ScopedEnvVar postIngestCompatibility{"YAMS_POST_INGEST_RPC_QUEUE_MAX", std::nullopt}; const auto configPath = runtime_root_ / "config.toml"; const auto writeOverrides = [&](bool enabled) { std::ofstream out(configPath, std::ios::trunc); REQUIRE(out.is_open()); if (!enabled) { out << "# runtime tuning overrides removed\n"; return; } out << "[tuning]\n"; out << "target_cpu_percent = 321\n"; out << "[tuning.ipc]\n"; out << "timeout_ms = 4321\n"; out << "[tuning.resource]\n"; out << "admission_control = false\n"; out << "memory_warning_threshold = 0.91\n"; out << "[tuning.post_ingest]\n"; out << "rpc_queue_max = 333\n"; }; writeOverrides(true); config_.configFilePath = configPath; daemon_ = std::make_unique(config_); REQUIRE(daemon_ != nullptr); auto activeTuning = daemon_->serviceManager_->getTuningConfig(); activeTuning.topologyAlgorithm = "exact"; daemon_->serviceManager_->setTuningConfig(activeTuning); daemon_->reloadTuningConfig(); CHECK((daemon_->config_.tuning.targetCpuPercent == 321u)); CHECK((TuneAdvisor::ipcTimeoutMs() == 4321u)); CHECK_FALSE(TuneAdvisor::enableAdmissionControl()); CHECK((TuneAdvisor::memoryWarningThreshold() == 0.91)); CHECK((TuneAdvisor::postIngestRpcQueueMax() == 333u)); CHECK((daemon_->config_.tuning.provenance.at("tuning.ipc.timeout_ms") == "config:tuning.ipc.timeout_ms")); writeOverrides(false); const auto versionBeforeRemoval = TuneAdvisor::configuredOverridesVersion(); daemon_->reloadTuningConfig(); CHECK((TuneAdvisor::configuredOverridesVersion() == versionBeforeRemoval + 2)); CHECK((daemon_->config_.tuning.targetCpuPercent == 200u)); CHECK((daemon_->config_.tuning.topologyAlgorithm == "exact")); CHECK(daemon_->config_.tuning.provenance.empty()); CHECK((TuneAdvisor::ipcTimeoutMs() == 15000u)); CHECK(TuneAdvisor::enableAdmissionControl()); CHECK((TuneAdvisor::memoryWarningThreshold() == 0.75)); CHECK((TuneAdvisor::postIngestRpcQueueMax() == 256u)); const auto status = daemon_->serviceManager_->getRuntimeTuningStatus(); CHECK((status.at("ipc.timeout_ms") == "15000")); CHECK((status.at("ipc.timeout_ms.source") == "default")); CHECK((status.at("resource.admission_control") == "1")); CHECK((status.at("resource.admission_control.source") == "default")); CHECK((status.at("resource.memory_warning_threshold") == "0.750000")); CHECK((status.at("resource.memory_warning_threshold.source") == "default")); CHECK((status.at("post_ingest.rpc_queue_max") == "256")); CHECK((status.at("post_ingest.rpc_queue_max.source") == "default")); } TEST_CASE_METHOD(DaemonFixture, "Daemon tuning reload rejects multiple active lifecycles", "[daemon][tuning][reload][lifecycle]") { SKIP_ON_WINDOWS(); yams::test::ScopedEnvVar ipcCompatibility{"YAMS_IPC_TIMEOUT_MS", std::nullopt}; const auto configPath = runtime_root_ / "config.toml"; config_.configFilePath = configPath; daemon_ = std::make_unique(config_); REQUIRE(daemon_ != nullptr); REQUIRE((TuneAdvisor::ipcTimeoutMs() == 15000u)); auto nestedConfig = config_; nestedConfig.dataDir = runtime_root_ / "nested-data"; nestedConfig.socketPath = runtime_root_ / "nested.sock"; nestedConfig.pidFile = runtime_root_ / "nested.pid"; nestedConfig.logFile = runtime_root_ / "nested.log"; YamsDaemon nested(nestedConfig); { std::ofstream out(configPath, std::ios::trunc); REQUIRE(out.is_open()); out << "[tuning.ipc]\n"; out << "timeout_ms = 4321\n"; } const auto versionBefore = TuneAdvisor::configuredOverridesVersion(); daemon_->reloadTuningConfig(); CHECK((TuneAdvisor::configuredOverridesVersion() == versionBefore)); CHECK((TuneAdvisor::ipcTimeoutMs() == 15000u)); CHECK((daemon_->config_.tuning.provenance.count("tuning.ipc.timeout_ms") == 0)); CHECK((daemon_->serviceManager_->getRuntimeTuningStatus().at("ipc.timeout_ms") == "15000")); } TEST_CASE_METHOD(DaemonFixture, "Lifecycle shutdown waits for owner-thread stop", "[daemon][lifecycle][shutdown-request]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto startResult = daemon_->start(); if (!startResult && isSocketPermissionDenied(startResult.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(startResult); startRunLoop(); REQUIRE(waitForCondition( 2s, [&] { return daemon_->runLoopStarted_.load(std::memory_order_acquire); })); DaemonLifecycleAdapter lifecycle(daemon_.get()); lifecycle.requestShutdown(true, true); REQUIRE(waitForCondition(2s, [&] { return daemon_->stopRequested_.load(std::memory_order_acquire) && daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); std::this_thread::sleep_for(100ms); CHECK(daemon_->shutdownThreadActive_.load(std::memory_order_acquire)); stopRunLoop(); auto stopResult = daemon_->stop(); REQUIRE(stopResult); REQUIRE(waitForCondition( 2s, [&] { return !daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); daemon_->reapCompletedShutdownThread(); } TEST_CASE_METHOD(DaemonFixture, "Lifecycle shutdown request does not cancel an in-flight worker task", "[daemon][lifecycle][shutdown-request][cancellation]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto startResult = daemon_->start(); if (!startResult && isSocketPermissionDenied(startResult.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(startResult); startRunLoop(); REQUIRE(waitForCondition( 2s, [&] { return daemon_->runLoopStarted_.load(std::memory_order_acquire); })); auto* serviceManager = daemon_->getServiceManager(); REQUIRE((serviceManager != nullptr)); std::mutex gateMutex; std::condition_variable gateCv; bool allowExit = false; std::atomic workerEntered{false}; std::atomic workerFinished{false}; boost::asio::post(serviceManager->getWorkerExecutor(), [&]() { { std::lock_guard lk(gateMutex); workerEntered.store(true, std::memory_order_release); } gateCv.notify_all(); std::unique_lock lk(gateMutex); gateCv.wait(lk, [&] { return allowExit; }); workerFinished.store(true, std::memory_order_release); }); REQUIRE(waitForCondition(2s, [&] { return workerEntered.load(std::memory_order_acquire); })); DaemonLifecycleAdapter lifecycle(daemon_.get()); lifecycle.requestShutdown(true, true); REQUIRE(waitForCondition(2s, [&] { return daemon_->stopRequested_.load(std::memory_order_acquire) && daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); std::this_thread::sleep_for(150ms); CHECK(daemon_->shutdownThreadActive_.load(std::memory_order_acquire)); CHECK_FALSE(workerFinished.load(std::memory_order_acquire)); { std::lock_guard lk(gateMutex); allowExit = true; } gateCv.notify_all(); REQUIRE(waitForCondition(2s, [&] { return workerFinished.load(std::memory_order_acquire); })); stopRunLoop(); auto stopResult = daemon_->stop(); REQUIRE(stopResult); REQUIRE(waitForCondition( 2s, [&] { return !daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); daemon_->reapCompletedShutdownThread(); } TEST_CASE_METHOD(DaemonFixture, "Lifecycle shutdown completion stays pending while worker teardown is blocked", "[daemon][lifecycle][shutdown-request][budget]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto startResult = daemon_->start(); if (!startResult && isSocketPermissionDenied(startResult.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(startResult); startRunLoop(); REQUIRE(waitForCondition( 2s, [&] { return daemon_->runLoopStarted_.load(std::memory_order_acquire); })); auto* serviceManager = daemon_->getServiceManager(); REQUIRE((serviceManager != nullptr)); std::mutex gateMutex; std::condition_variable gateCv; bool allowExit = false; std::atomic workerEntered{false}; boost::asio::post(serviceManager->getWorkerExecutor(), [&]() { { std::lock_guard lk(gateMutex); workerEntered.store(true, std::memory_order_release); } gateCv.notify_all(); std::unique_lock lk(gateMutex); gateCv.wait(lk, [&] { return allowExit; }); }); REQUIRE(waitForCondition(2s, [&] { return workerEntered.load(std::memory_order_acquire); })); DaemonLifecycleAdapter lifecycle(daemon_.get()); lifecycle.requestShutdown(true, true); REQUIRE(waitForCondition(2s, [&] { return daemon_->stopRequested_.load(std::memory_order_acquire) && daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); CHECK_FALSE(daemon_->waitForStopCompletion(100ms)); std::this_thread::sleep_for(150ms); CHECK(daemon_->shutdownThreadActive_.load(std::memory_order_acquire)); CHECK_FALSE(daemon_->waitForStopCompletion(100ms)); { std::lock_guard lk(gateMutex); allowExit = true; } gateCv.notify_all(); stopRunLoop(); auto stopResult = daemon_->stop(); REQUIRE(stopResult); REQUIRE(daemon_->waitForStopCompletion(2s)); daemon_->reapCompletedShutdownThread(); } TEST_CASE_METHOD(DaemonFixture, "Lifecycle shutdown timeout floor covers work coordinator join budget", "[daemon][lifecycle][shutdown-request][budget]") { SKIP_ON_WINDOWS(); yams::test::ScopedEnvVar shutdownBudget("YAMS_SHUTDOWN_FORCE_EXIT_MS", std::string("1000")); daemon_ = std::make_unique(config_); auto startResult = daemon_->start(); if (!startResult && isSocketPermissionDenied(startResult.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(startResult); startRunLoop(); REQUIRE(waitForCondition( 2s, [&] { return daemon_->runLoopStarted_.load(std::memory_order_acquire); })); auto* serviceManager = daemon_->getServiceManager(); REQUIRE((serviceManager != nullptr)); std::mutex gateMutex; std::condition_variable gateCv; bool allowExit = false; std::atomic workerEntered{false}; boost::asio::post(serviceManager->getWorkerExecutor(), [&]() { { std::lock_guard lk(gateMutex); workerEntered.store(true, std::memory_order_release); } gateCv.notify_all(); std::unique_lock lk(gateMutex); gateCv.wait(lk, [&] { return allowExit; }); }); REQUIRE(waitForCondition(2s, [&] { return workerEntered.load(std::memory_order_acquire); })); DaemonLifecycleAdapter lifecycle(daemon_.get()); lifecycle.requestShutdown(true, true); REQUIRE(waitForCondition(2s, [&] { return daemon_->stopRequested_.load(std::memory_order_acquire) && daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); std::this_thread::sleep_for(1200ms); CHECK(daemon_->shutdownThreadActive_.load(std::memory_order_acquire)); CHECK_FALSE(daemon_->waitForStopCompletion(100ms)); { std::lock_guard lk(gateMutex); allowExit = true; } gateCv.notify_all(); stopRunLoop(); auto stopResult = daemon_->stop(); REQUIRE(stopResult); REQUIRE(daemon_->waitForStopCompletion(2s)); daemon_->reapCompletedShutdownThread(); } TEST_CASE_METHOD(DaemonFixture, "Lifecycle shutdown falls back to direct stop without run loop", "[daemon][lifecycle][shutdown-request][fallback]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto startResult = daemon_->start(); if (!startResult && isSocketPermissionDenied(startResult.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(startResult); REQUIRE_FALSE(daemon_->runLoopStarted_.load(std::memory_order_acquire)); DaemonLifecycleAdapter lifecycle(daemon_.get()); lifecycle.requestShutdown(true, true); REQUIRE(waitForCondition(2s, [&] { return !daemon_->isRunning() && !daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); CHECK(daemon_->getLifecycle().snapshot().state == LifecycleState::Stopped); if (const auto* serviceManager = daemon_->getServiceManager()) { CHECK(serviceManager->getServiceManagerFsmSnapshot().state == ServiceManagerState::Stopped); } daemon_->reapCompletedShutdownThread(); } TEST_CASE_METHOD(DaemonFixture, "Lifecycle shutdown avoids forced exit for foreground-managed daemon", "[daemon][lifecycle][shutdown-request][foreground-managed]") { SKIP_ON_WINDOWS(); yams::test::ScopedEnvVar foregroundManaged("YAMS_DAEMON_FOREGROUND", std::string("1")); daemon_ = std::make_unique(config_); auto startResult = daemon_->start(); if (!startResult && isSocketPermissionDenied(startResult.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(startResult); startRunLoop(); REQUIRE(waitForCondition( 2s, [&] { return daemon_->runLoopStarted_.load(std::memory_order_acquire); })); DaemonLifecycleAdapter lifecycle(daemon_.get()); lifecycle.requestShutdown(true, false); REQUIRE(waitForCondition(2s, [&] { return daemon_->stopRequested_.load(std::memory_order_acquire) && daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); stopRunLoop(); auto stopResult = daemon_->stop(); REQUIRE(stopResult); REQUIRE(waitForCondition( 2s, [&] { return !daemon_->shutdownThreadActive_.load(std::memory_order_acquire); })); daemon_->reapCompletedShutdownThread(); REQUIRE_FALSE(daemon_->isRunning()); } TEST_CASE_METHOD(DaemonFixture, "Daemon start and stop", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto startResult = daemon_->start(); if (!startResult && isSocketPermissionDenied(startResult.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(startResult); REQUIRE(daemon_->isRunning()); REQUIRE(fs::exists(config_.pidFile)); startRunLoop(); REQUIRE(waitForDaemonReady()); stopRunLoop(); auto result = daemon_->stop(); REQUIRE(result); REQUIRE_FALSE(daemon_->isRunning()); REQUIRE(daemon_->getLifecycle().snapshot().state == LifecycleState::Stopped); if (const auto* serviceManager = daemon_->getServiceManager()) { REQUIRE(serviceManager->getServiceManagerFsmSnapshot().state == ServiceManagerState::Stopped); } REQUIRE_FALSE(fs::exists(config_.pidFile)); } TEST_CASE_METHOD(DaemonFixture, "Daemon single instance enforcement", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); auto daemon2 = std::make_unique(config_); auto result2 = daemon2->start(); REQUIRE_FALSE(result2); REQUIRE(result2.error().code == ErrorCode::InvalidState); daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "Daemon rejects pid file pointing to current process", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); json payload = json::object(); payload["pid"] = static_cast(getpid()); payload["start_ns"] = 1; payload["token"] = "test"; std::ofstream pidFile(config_.pidFile); pidFile << payload.dump(); pidFile.close(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); REQUIRE_FALSE(result); REQUIRE(result.error().code == ErrorCode::InvalidState); } TEST_CASE_METHOD(DaemonFixture, "Daemon refuses pid file pointing to unrelated running process", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); #ifndef _WIN32 pid_t child = fork(); if (child < 0) { SKIP("fork not available"); } if (child == 0) { for (;;) { ::pause(); } } json payload = json::object(); payload["pid"] = static_cast(child); payload["start_ns"] = 1; payload["token"] = "test"; std::ofstream pidFile(config_.pidFile); pidFile << payload.dump(); pidFile.close(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); REQUIRE_FALSE(result); REQUIRE(result.error().code == ErrorCode::InvalidState); ::kill(child, SIGKILL); int status = 0; (void)::waitpid(child, &status, 0); #else SKIP("POSIX process APIs not available on Windows"); #endif } TEST_CASE_METHOD(DaemonFixture, "Daemon does not terminate unverified data-dir lock holder", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); #ifndef _WIN32 // Pre-compute fork-safe inputs in the parent: the child must not allocate // (a fork in a multithreaded process can leave the malloc lock held by a // vanished thread), so build the lock path here and format the payload with // snprintf into a stack buffer in the child. const std::string lockPathStr = (config_.dataDir / ".yams-lock").string(); int readyPipe[2] = {-1, -1}; REQUIRE(::pipe(readyPipe) == 0); const pid_t child = ::fork(); REQUIRE(child >= 0); if (child == 0) { ::close(readyPipe[0]); const int lockFd = ::open(lockPathStr.c_str(), O_CREAT | O_RDWR, 0644); if (lockFd < 0 || ::flock(lockFd, LOCK_EX) != 0) { const char failed = '2'; (void)::write(readyPipe[1], &failed, 1); _exit(2); } char payload[128]; const int payloadLen = ::snprintf(payload, sizeof(payload), "{\"pid\":%d,\"socket\":\"/tmp/not-a-daemon.sock\"}", static_cast(::getpid())); if (payloadLen > 0 && static_cast(payloadLen) < sizeof(payload)) { (void)::ftruncate(lockFd, 0); (void)::write(lockFd, payload, static_cast(payloadLen)); } const char ready = '1'; (void)::write(readyPipe[1], &ready, 1); for (;;) { ::pause(); } } ::close(readyPipe[1]); struct ChildGuard { pid_t pid; int readyFd; ~ChildGuard() { ::close(readyFd); (void)::kill(pid, SIGKILL); int status = 0; (void)::waitpid(pid, &status, 0); } } childGuard{child, readyPipe[0]}; char ready = 0; ssize_t readyRead = -1; do { readyRead = ::read(readyPipe[0], &ready, 1); } while (readyRead < 0 && errno == EINTR); INFO("lock holder child status byte=" << static_cast(ready) << " read=" << readyRead); REQUIRE(readyRead == 1); REQUIRE(ready == '1'); daemon_ = std::make_unique(config_); const auto result = daemon_->start(); REQUIRE_FALSE(result); CHECK(result.error().code == ErrorCode::InvalidState); CHECK(::kill(child, 0) == 0); #else SKIP("POSIX process APIs not available on Windows"); #endif } TEST_CASE_METHOD(DaemonFixture, "Daemon restart", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); result = daemon_->stop(); REQUIRE(result); result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted on restart"); } REQUIRE(result); REQUIRE(daemon_->isRunning()); daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "Daemon path resolution with empty config", "[daemon][config]") { SKIP_ON_WINDOWS(); DaemonConfig autoConfig; daemon_ = std::make_unique(autoConfig); REQUIRE(daemon_ != nullptr); } TEST_CASE_METHOD(DaemonFixture, "Daemon signal handling and PID file", "[daemon][signals]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); #ifndef _WIN32 std::ifstream pidFile(config_.pidFile); std::string content; std::getline(pidFile, content, '\0'); pidFile.close(); auto parsed = json::parse(content, nullptr, false); REQUIRE_FALSE(parsed.is_discarded()); REQUIRE(parsed.value("pid", 0) == getpid()); #endif daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "Daemon stale PID file cleanup", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); std::ofstream pidFile(config_.pidFile); pidFile << "99999999\n"; pidFile.close(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); REQUIRE(daemon_->isRunning()); daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "Daemon invalid socket parent configuration", "[daemon][config]") { SKIP_ON_WINDOWS(); const auto notADirectory = runtime_root_ / "socket-parent-file"; fs::create_directories(runtime_root_); std::ofstream(notADirectory) << "not a directory\n"; config_.socketPath = notADirectory / "cannot-create.sock"; daemon_ = std::make_unique(config_); auto result = daemon_->start(); REQUIRE_FALSE(result); } TEST_CASE_METHOD(DaemonFixture, "Daemon socket preflight does not clean unrelated stale PID artifacts", "[daemon][config][lifecycle]") { SKIP_ON_WINDOWS(); // Seed a stale PID artifact that this process does not own. { std::ofstream pidFile(config_.pidFile); pidFile << "99999999\n"; } REQUIRE(fs::exists(config_.pidFile)); // Force an AF_UNIX-overlong socket path so start fails in preflight, before lifecycle init. config_.socketPath = runtime_root_ / std::string(140, 'x') / "daemon.sock"; daemon_ = std::make_unique(config_); auto result = daemon_->start(); REQUIRE_FALSE(result); CHECK(result.error().message.find("Socket path too long") != std::string::npos); // Preflight failure must not mutate stale PID artifacts we did not create. CHECK(fs::exists(config_.pidFile)); } TEST_CASE_METHOD(DaemonFixture, "Daemon concurrent start attempts", "[daemon][concurrency]") { SKIP_ON_WINDOWS(); { YamsDaemon probe(config_); auto result = probe.start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); probe.stop(); } std::atomic successCount{0}; std::atomic failCount{0}; std::once_flag startedFlag; std::promise startedPromise; auto startedFuture = startedPromise.get_future().share(); std::promise releasePromise; auto releaseFuture = releasePromise.get_future().share(); std::promise attemptGatePromise; auto attemptGateFuture = attemptGatePromise.get_future().share(); std::mutex readyMutex; std::condition_variable readyCv; int readyCount = 0; std::vector threads; constexpr int kAttempts = 5; threads.reserve(kAttempts); for (int i = 0; i < kAttempts; ++i) { threads.emplace_back([this, &successCount, &failCount, &startedFlag, startedFuture, &startedPromise, releaseFuture, attemptGateFuture, &readyMutex, &readyCv, &readyCount]() { { std::lock_guard lock(readyMutex); ++readyCount; } readyCv.notify_one(); attemptGateFuture.wait(); YamsDaemon local(config_); auto result = local.start(); if (result) { successCount.fetch_add(1, std::memory_order_relaxed); std::call_once(startedFlag, [&]() { startedPromise.set_value(); }); releaseFuture.wait(); local.stop(); } else { startedFuture.wait(); failCount.fetch_add(1, std::memory_order_relaxed); } }); } { std::unique_lock lock(readyMutex); REQUIRE(readyCv.wait_for(lock, std::chrono::seconds(1), [&] { return readyCount == kAttempts; })); } attemptGatePromise.set_value(); if (startedFuture.wait_for(std::chrono::seconds(1)) != std::future_status::ready) { std::call_once(startedFlag, [&]() { startedPromise.set_value(); }); } const auto failDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (failCount.load(std::memory_order_relaxed) < kAttempts - 1 && std::chrono::steady_clock::now() < failDeadline) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } releasePromise.set_value(); for (auto& t : threads) { t.join(); } REQUIRE(successCount.load() == 1); REQUIRE(failCount.load() == kAttempts - 1); } TEST_CASE_METHOD(DaemonFixture, "Daemon stats tracking", "[daemon][stats]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); [[maybe_unused]] const auto& state = daemon_->getState(); daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "Daemon hybrid search smoke test", "[daemon][search]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); SearchRequest req; req.query = "seed"; req.limit = 5; req.fuzzy = false; req.literalText = false; req.similarity = 0.7; Message msg; msg.version = 1; msg.requestId = 42; msg.payload = Request{req}; auto enc = ProtoSerializer::encode_payload(msg); REQUIRE(enc); auto parsed = ProtoSerializer::decode_payload(enc.value()); REQUIRE(parsed); REQUIRE(daemon_->isRunning()); daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "Daemon fallback search when hybrid unavailable", "[daemon][search]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); SearchRequest req; req.query = "fallback"; req.limit = 3; req.fuzzy = true; req.similarity = 0.6; Message msg; msg.version = 1; msg.requestId = 7; msg.payload = Request{req}; auto enc = ProtoSerializer::encode_payload(msg); REQUIRE(enc); auto parsed = ProtoSerializer::decode_payload(enc.value()); REQUIRE(parsed); REQUIRE(daemon_->isRunning()); daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "StatusResponse serialization includes ready field", "[daemon][serialization]") { StatusResponse sr; sr.running = true; sr.ready = true; sr.uptimeSeconds = 0; sr.requestsProcessed = 0; sr.activeConnections = 0; sr.memoryUsageMb = 0.0; sr.cpuUsagePercent = 0.0; sr.version = "test"; Message msg; msg.version = 1; msg.requestId = 0; msg.payload = Response{sr}; auto enc = ProtoSerializer::encode_payload(msg); REQUIRE(enc); auto des = ProtoSerializer::decode_payload(enc.value()); REQUIRE(des); Message out = des.value(); auto respVariant = std::get(out.payload); REQUIRE(std::holds_alternative(respVariant)); auto outSr = std::get(respVariant); REQUIRE(outSr.ready == true); REQUIRE(outSr.running == true); REQUIRE(outSr.version == "test"); } TEST_CASE_METHOD(DaemonFixture, "Daemon start fails when data directory path is a file", "[daemon][config]") { SKIP_ON_WINDOWS(); const auto notADirectory = runtime_root_ / "data-file"; fs::create_directories(runtime_root_); std::ofstream(notADirectory) << "not a directory\n"; DaemonConfig bad = config_; bad.dataDir = notADirectory; daemon_ = std::make_unique(bad); auto result = daemon_->start(); REQUIRE_FALSE(result); } TEST_CASE_METHOD(DaemonFixture, "Daemon PID file cannot be created below a file path", "[daemon][errors]") { SKIP_ON_WINDOWS(); const auto notADirectory = runtime_root_ / "pid-parent-file"; fs::create_directories(runtime_root_); std::ofstream(notADirectory) << "not a directory\n"; DaemonConfig bad_config = config_; bad_config.pidFile = notADirectory / "yams.pid"; daemon_ = std::make_unique(bad_config); auto result = daemon_->start(); REQUIRE_FALSE(result); if (!result) { REQUIRE_FALSE(result.error().message.empty()); } } TEST_CASE_METHOD(DaemonFixture, "Daemon PID file cleaned up on start failure", "[daemon][errors]") { SKIP_ON_WINDOWS(); const auto notADirectory = runtime_root_ / "data-file-for-pid-cleanup"; fs::create_directories(runtime_root_); std::ofstream(notADirectory) << "not a directory\n"; DaemonConfig bad_config = config_; bad_config.dataDir = notADirectory; daemon_ = std::make_unique(bad_config); auto result = daemon_->start(); REQUIRE_FALSE(result); REQUIRE_FALSE(fs::exists(bad_config.pidFile)); } TEST_CASE_METHOD(DaemonFixture, "Daemon stop when not started is safe", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); REQUIRE_NOTHROW(daemon_->stop()); REQUIRE_NOTHROW(daemon_->stop()); } TEST_CASE_METHOD(DaemonFixture, "Daemon double stop is idempotent", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); daemon_->stop(); REQUIRE_NOTHROW(daemon_->stop()); } TEST_CASE_METHOD(DaemonFixture, "Daemon fails when socket parent is a file", "[daemon][config]") { SKIP_ON_WINDOWS(); const auto notADirectory = runtime_root_ / "socket-parent-file-2"; fs::create_directories(runtime_root_); std::ofstream(notADirectory) << "not a directory\n"; DaemonConfig bad_config = config_; bad_config.socketPath = notADirectory / "subdir" / "yams.sock"; daemon_ = std::make_unique(bad_config); auto result = daemon_->start(); REQUIRE_FALSE(result); } TEST_CASE_METHOD(DaemonFixture, "Daemon socket cleanup after stop", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); REQUIRE(fs::exists(config_.socketPath)); daemon_->stop(); REQUIRE_FALSE(fs::exists(config_.socketPath)); } TEST_CASE_METHOD(DaemonFixture, "Daemon restart with existing socket", "[daemon][lifecycle]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result1 = daemon_->start(); if (!result1 && isSocketPermissionDenied(result1.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result1); daemon_->stop(); auto result2 = daemon_->start(); REQUIRE(result2); daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "Daemon empty data directory handling", "[daemon][config]") { SKIP_ON_WINDOWS(); DaemonConfig bad_config = config_; bad_config.dataDir = ""; daemon_ = std::make_unique(bad_config); auto result = daemon_->start(); if (result) { daemon_->stop(); } else { REQUIRE_FALSE(result.error().message.empty()); } } TEST_CASE_METHOD(DaemonFixture, "Daemon status query on stopped daemon", "[daemon][state]") { SKIP_ON_WINDOWS(); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); const auto& state_running = daemon_->getState(); REQUIRE(state_running.readiness.ipcServerReady.load()); daemon_->stop(); const auto& state_stopped = daemon_->getState(); REQUIRE_FALSE(state_stopped.readiness.ipcServerReady.load()); } TEST_CASE_METHOD(DaemonFixture, "Daemon async init barrier prevents race condition", "[daemon][concurrency]") { SKIP_ON_WINDOWS(); if (const char* disable_vectors = std::getenv("YAMS_DISABLE_VECTORS")) { if (std::string_view(disable_vectors) == "1") { SKIP("Vector DB disabled via YAMS_DISABLE_VECTORS=1"); } } daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); REQUIRE(daemon_->isRunning()); std::atomic runLoopEntered{false}; std::atomic runLoopExited{false}; std::thread runLoopThread([this, &runLoopEntered, &runLoopExited]() { runLoopEntered.store(true); daemon_->runLoop(); runLoopExited.store(true); }); std::this_thread::sleep_for(100ms); REQUIRE(runLoopEntered.load()); daemon_->requestStop(); if (runLoopThread.joinable()) { runLoopThread.join(); } REQUIRE(runLoopExited.load()); daemon_->stop(); } TEST_CASE_METHOD(DaemonFixture, "Daemon async init barrier stress test", "[daemon][concurrency][.slow]") { SKIP_ON_WINDOWS(); constexpr int kIterations = 5; for (int i = 0; i < kIterations; ++i) { INFO("Iteration " << i); daemon_ = std::make_unique(config_); auto result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } if (!result) { std::this_thread::sleep_for(100ms); result = daemon_->start(); if (!result && isSocketPermissionDenied(result.error())) { SKIP("UNIX domain sockets not permitted"); } REQUIRE(result); } REQUIRE(daemon_->isRunning()); std::atomic crashed{false}; std::thread runLoopThread([this, &crashed]() { try { daemon_->runLoop(); } catch (...) { crashed.store(true); } }); std::this_thread::sleep_for(50ms); daemon_->requestStop(); if (runLoopThread.joinable()) { runLoopThread.join(); } REQUIRE_FALSE(crashed.load()); daemon_->stop(); daemon_.reset(); std::this_thread::sleep_for(50ms); } } } // namespace yams::daemon::test