#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "catalog_fixture.hpp" namespace { struct temporary_directory { std::filesystem::path path; temporary_directory() { auto pattern = (std::filesystem::temp_directory_path() / "vamp-health-XXXXXX").string(); BOOST_REQUIRE(::mkdtemp(pattern.data()) != nullptr); path = pattern; } ~temporary_directory() { auto ignored = std::error_code{}; std::filesystem::remove_all(path, ignored); } }; auto write_file(std::filesystem::path const &path, std::string const &bytes) -> void { std::filesystem::create_directories(path.parent_path()); auto output = std::ofstream{path, std::ios::binary | std::ios::trunc}; output << bytes; output.close(); BOOST_REQUIRE(output.good()); } auto read_file(std::filesystem::path const &path) -> std::string { auto input = std::ifstream{path, std::ios::binary}; BOOST_REQUIRE(input.good()); return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; } auto healthy_payload() -> boost::json::value { return boost::json::object{{"device", "zero"}, {"status", "ready"}, {"product", "product-vamp"}}; } auto health_router(boost::json::value payload) -> ligo::service_router { auto router = ligo::service_router{}; router.add_route("GET", "/api/health", [payload = std::move(payload)](ligo::request_context, boost::json::value) -> boost::asio::awaitable> { co_return payload; }); return router; } auto ipv6_loopback_available(boost::unit_test::test_unit_id) -> boost::test_tools::assertion_result { auto io = boost::asio::io_context{}; auto acceptor = boost::asio::ip::tcp::acceptor{io}; auto error = boost::system::error_code{}; acceptor.open(boost::asio::ip::tcp::v6(), error); if (!error) { acceptor.bind({boost::asio::ip::address_v6::loopback(), 0}, error); } // Other setup failures must still be exercised by the HTTP server test. auto unavailable = error == boost::system::errc::address_family_not_supported || error == boost::system::errc::address_not_available || error == boost::system::errc::protocol_not_supported; auto result = boost::test_tools::assertion_result{!unavailable}; if (unavailable) { result.message() << "IPv6 loopback is unavailable: " << error.message(); } return result; } struct http_service { boost::asio::io_context io; ligo::http::server server; std::thread thread; explicit http_service(ligo::service_router router, std::string host = "127.0.0.1") : server{io, {.host = std::move(host)}, std::move(router), {.timeout = std::chrono::milliseconds{250}}} { auto started = server.start(); BOOST_REQUIRE_MESSAGE(started, (started ? "" : started.error().describe())); thread = std::thread{[this] { io.run(); }}; } ~http_service() { server.stop(); io.stop(); thread.join(); } auto config(std::string host = "127.0.0.1") const -> vamp_zero::config { auto result = vamp_zero::config{}; result.control.endpoint.bind = host + ":" + std::to_string(server.selected_port()); result.control.endpoint.timeout = std::chrono::milliseconds{250}; result.control.zero_ip = "192.0.2.1"; result.control.endpoint.connect = "192.0.2.2:1"; return result; } }; auto unused_port() -> std::uint16_t { auto io = boost::asio::io_context{}; auto acceptor = boost::asio::ip::tcp::acceptor{io, {boost::asio::ip::make_address("127.0.0.1"), 0}}; return acceptor.local_endpoint().port(); } auto fixture_config(std::filesystem::path const &root) -> vamp_zero::config { auto config = vamp_zero::config{}; config.runtime.assets_path = (root / "assets").string(); config.runtime.heartbeat_path = (root / "run" / "heartbeat.json").string(); config.database.path = (root / "db" / "vamp.db").string(); config.control.endpoint.bind = "127.0.0.1:" + std::to_string(unused_port()); config.control.endpoint.timeout = std::chrono::milliseconds{250}; return config; } auto config_file(std::filesystem::path const &root, vamp_zero::config const &config) -> std::string { auto json = boost::json::object{ {"modulus", modulus::to_json(config.modulus)}, {"runtime", {{"assets_path", config.runtime.assets_path}, {"heartbeat_path", config.runtime.heartbeat_path}, {"headless", false}, {"exit_after_startup", config.runtime.exit_after_startup}}}, {"database", {{"path", config.database.path}}}, {"control", {{"endpoint", ligo::to_json(config.control.endpoint)}, {"zero_ip", config.control.zero_ip}}}, {"audio", {{"enabled", true}, {"playback_enabled", true}, {"metronome_enabled", true}}}, }; auto path = root / "config.json"; write_file(path, boost::json::serialize(json)); return path.string(); } auto fixture_page(std::int32_t width = 976, std::int32_t height = 484) -> std::string { auto image = GenImageColor(width, height, WHITE); auto size = std::int32_t{}; auto bytes = ExportImageToMemory(image, ".png", &size); UnloadImage(image); BOOST_REQUIRE(bytes != nullptr); auto result = std::string{reinterpret_cast(bytes), static_cast(size)}; MemFree(bytes); return result; } auto checksum(std::string const &bytes) -> std::string { auto digest = std::array{}; SHA256(reinterpret_cast(bytes.data()), bytes.size(), digest.data()); auto result = std::string{}; constexpr auto digits = std::string_view{"0123456789abcdef"}; for (std::uchar_t byte : digest) { result.push_back(digits[byte >> 4U]); result.push_back(digits[byte & 15U]); } return result; } auto write_score(std::filesystem::path const &path, std::string part = "Lead") -> void { auto score = boost::json::parse(R"({"music":{"metadata":{"title":"Fixture","composer":"Composer", "key_signature":"C","time_signature":"4/4"},"staves":[{"name":"Lead","measures":[{"voices":[]}]}]}, "presentation":{"width":976,"height":484,"pages":[]}})"); score.at("music").at("staves").as_array().front().as_object()["name"] = part; auto &pages = score.at("presentation").at("pages").as_array(); for (auto index = std::uint32_t{1}; index <= 2U; ++index) { auto relative = path.stem().string() + ".pages/" + std::to_string(index) + ".png"; auto bytes = fixture_page(); write_file(path.parent_path() / relative, bytes); pages.push_back(boost::json::object{{"path", relative}, {"sha256", checksum(bytes)}}); } write_file(path, boost::json::serialize(score)); } auto make_assets(vamp_zero::config const &config) -> void { auto root = std::filesystem::path{config.runtime.assets_path}; write_score(root / "songs" / "first.json"); write_score(root / "songs" / "second.json"); for (auto part_name : vamp_zero::practice_part_names()) { auto name = std::string{part_name}; name.front() = static_cast(name.front() - 'A' + 'a'); write_score(root / "practice" / "001" / (name + ".json"), std::string{part_name}); } BOOST_REQUIRE(vamp_test::build_catalog(root, config.database.path)); } auto run_once(std::filesystem::path const &root, vamp_zero::config const &config) -> std::expected { return vamp_zero::run({.config_path = config_file(root, config), .exit_after_startup = true}); } auto require_no_probe(vamp_zero::config const &config, std::bool_t heartbeat_exists = false) -> void { auto directory = std::filesystem::path{config.runtime.heartbeat_path}.parent_path(); if (!std::filesystem::exists(directory)) { return; } auto count = std::uint32_t{}; for (auto const &entry : std::filesystem::directory_iterator{directory}) { BOOST_TEST(entry.path().filename().string() == "heartbeat.json"); ++count; } BOOST_TEST(count == (heartbeat_exists ? 1U : 0U)); } } BOOST_AUTO_TEST_CASE(health_uses_configured_bind_and_normalizes_wildcards) { auto service = http_service{health_router(healthy_payload())}; BOOST_TEST(vamp_zero::check_health(service.config()).has_value()); BOOST_TEST(vamp_zero::check_health(service.config("0.0.0.0")).has_value()); auto other = http_service{health_router(healthy_payload()), "127.0.0.2"}; BOOST_TEST(vamp_zero::check_health(other.config("127.0.0.2")).has_value()); } BOOST_AUTO_TEST_CASE(health_normalizes_ipv6_wildcards, *boost::unit_test::precondition(ipv6_loopback_available)) { auto ipv6 = http_service{health_router(healthy_payload()), "::1"}; BOOST_TEST(vamp_zero::check_health(ipv6.config("[::1]")).has_value()); BOOST_TEST(vamp_zero::check_health(ipv6.config("[::]")).has_value()); BOOST_TEST(vamp_zero::check_health(ipv6.config("[0:0:0:0:0:0:0:0]")).has_value()); } BOOST_AUTO_TEST_CASE(health_rejects_wrong_device_product_status_and_payload_types) { for (std::char_t const *field : {"product", "device", "status"}) { for (auto value : {boost::json::value{"wrong"}, boost::json::value{42}, boost::json::value{}}) { auto payload = healthy_payload(); payload.as_object()[field] = value; auto service = http_service{health_router(std::move(payload))}; BOOST_TEST(!vamp_zero::check_health(service.config())); } auto payload = healthy_payload(); payload.as_object().erase(field); auto service = http_service{health_router(std::move(payload))}; BOOST_TEST(!vamp_zero::check_health(service.config())); } for (auto payload : {boost::json::value{}, boost::json::value{true}, boost::json::value{boost::json::array{}}}) { auto service = http_service{health_router(std::move(payload))}; BOOST_TEST(!vamp_zero::check_health(service.config())); } } BOOST_AUTO_TEST_CASE(health_rejects_malformed_or_failed_envelopes_and_missing_routes) { for (std::char_t const *body : {"not JSON", "{}", "[]", "{\"ok\":true}", "{\"ok\":false}", "{\"device\":\"zero\",\"product\":\"product-vamp\",\"status\":\"ready\"}"}) { auto router = ligo::service_router{}; router.add_response_route("GET", "/api/health", [body](ligo::request_context, boost::json::value) -> boost::asio::awaitable> { co_return ligo::text_response("application/json", body); }); auto service = http_service{std::move(router)}; BOOST_TEST(!vamp_zero::check_health(service.config())); } auto service = http_service{ligo::service_router{}}; BOOST_TEST(!vamp_zero::check_health(service.config())); } BOOST_AUTO_TEST_CASE(health_fails_when_stopped_and_obeys_timeout) { auto config = vamp_zero::config{}; config.control.endpoint.bind = "127.0.0.1:" + std::to_string(unused_port()); config.control.endpoint.timeout = std::chrono::milliseconds{50}; BOOST_TEST(!vamp_zero::check_health(config)); auto io = boost::asio::io_context{}; auto stalled = boost::asio::ip::tcp::acceptor{io, {boost::asio::ip::make_address("127.0.0.1"), 0}}; config.control.endpoint.bind = "127.0.0.1:" + std::to_string(stalled.local_endpoint().port()); auto started = std::chrono::steady_clock::now(); BOOST_TEST(!vamp_zero::check_health(config)); auto elapsed = std::chrono::steady_clock::now() - started; BOOST_TEST((elapsed < std::chrono::seconds{2})); } BOOST_AUTO_TEST_CASE(health_command_never_mutates_database_assets_or_runtime) { auto root = temporary_directory{}; auto service = http_service{health_router(healthy_payload())}; auto config = fixture_config(root.path); config.control = service.config().control; config.runtime.exit_after_startup = true; auto log_level = modulus::default_logger().level(); auto error_config = modulus::default_error_config(); config.modulus.logging.level = log_level == modulus::log_level::DEBUG ? modulus::log_level::ERROR : modulus::log_level::DEBUG; config.modulus.errors.include_location = !error_config.include_location; auto command = vamp_zero::command_line{.config_path = config_file(root.path, config), .health_check = true}; BOOST_TEST(vamp_zero::run(command).has_value()); BOOST_TEST((modulus::default_logger().level() == log_level)); BOOST_TEST(modulus::default_error_config().include_location == error_config.include_location); BOOST_TEST(!std::filesystem::exists(root.path / "db")); BOOST_TEST(!std::filesystem::exists(root.path / "assets")); BOOST_TEST(!std::filesystem::exists(root.path / "run")); write_file(config.database.path, "not a database"); write_file(std::filesystem::path{config.runtime.assets_path} / "songs" / "invalid.json", "not JSON"); write_file(config.runtime.heartbeat_path, "stale heartbeat"); auto database_time = std::filesystem::last_write_time(config.database.path); auto heartbeat_time = std::filesystem::last_write_time(config.runtime.heartbeat_path); BOOST_TEST(vamp_zero::run(command).has_value()); config.control.endpoint.bind = "127.0.0.1:" + std::to_string(unused_port()); command.config_path = config_file(root.path, config); BOOST_TEST(!vamp_zero::run(command)); BOOST_TEST((modulus::default_logger().level() == log_level)); BOOST_TEST(modulus::default_error_config().include_location == error_config.include_location); BOOST_TEST(read_file(config.database.path) == "not a database"); BOOST_TEST(read_file(std::filesystem::path{config.runtime.assets_path} / "songs" / "invalid.json") == "not JSON"); BOOST_TEST(read_file(config.runtime.heartbeat_path) == "stale heartbeat"); BOOST_TEST((std::filesystem::last_write_time(config.database.path) == database_time)); BOOST_TEST((std::filesystem::last_write_time(config.runtime.heartbeat_path) == heartbeat_time)); BOOST_TEST(!std::filesystem::exists(config.database.path + "-wal")); require_no_probe(config, true); } BOOST_AUTO_TEST_CASE(portal_health_route_reports_readiness_without_changing_control_state) { auto state = vamp_zero::control_state{vamp_zero::config{}, vamp_zero::app_state{}}; auto service = http_service{vamp_zero::make_router(state)}; BOOST_TEST(vamp_zero::check_health(service.config()).has_value()); auto snapshot = state.snapshot(); BOOST_TEST(snapshot.heartbeat_count == 0U); BOOST_TEST(!snapshot.pico_connected); BOOST_TEST(snapshot.last_command == "startup"); } BOOST_AUTO_TEST_CASE(portal_remote_commands_refresh_connection_without_background_polls) { for (auto const *route : {vamp_protocol::remote_next_path, vamp_protocol::remote_previous_path, vamp_protocol::remote_select_path}) { auto state = vamp_zero::control_state{vamp_zero::config{}, vamp_zero::app_state{}}; auto catalog = state.snapshot().catalog; auto router = vamp_zero::make_router(state); auto io = boost::asio::io_context{}; auto response = boost::asio::co_spawn(io, router.call_response({.method = "POST", .route = route}, {}), boost::asio::use_future); io.run(); BOOST_REQUIRE(response.get()); auto snapshot = state.snapshot(); BOOST_TEST(snapshot.pico_connected); BOOST_TEST(snapshot.heartbeat_count == 1U); BOOST_TEST(snapshot.catalog.get() == catalog.get()); } } BOOST_AUTO_TEST_CASE(readiness_reads_packaged_catalog_without_hardware_or_heartbeat) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); auto database_time = std::filesystem::last_write_time(config.database.path); for (auto const &entry : std::filesystem::recursive_directory_iterator{config.runtime.assets_path}) { if (entry.path().extension() == ".json") { std::filesystem::remove(entry.path()); } } auto ready = run_once(root.path, config); BOOST_REQUIRE_MESSAGE(ready, (ready ? "" : ready.error().describe())); BOOST_TEST(std::filesystem::exists(config.database.path)); BOOST_TEST((std::filesystem::last_write_time(config.database.path) == database_time)); BOOST_TEST(!std::filesystem::exists(config.database.path + "-wal")); BOOST_TEST(!std::filesystem::exists(config.runtime.heartbeat_path)); require_no_probe(config); auto io = boost::asio::io_context{}; auto performance = boost::asio::co_spawn(io, vamp_zero::async_load_performance_library(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future); auto practice = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future); io.run(); auto songs = performance.get(); auto exercises = practice.get(); BOOST_REQUIRE(songs); BOOST_REQUIRE(exercises); BOOST_TEST(songs->parts.size() == 2U); BOOST_REQUIRE_EQUAL(exercises->exercises.size(), 1U); BOOST_TEST(exercises->exercises.front().parts.size() == 3U); write_file(config.runtime.heartbeat_path, "existing heartbeat"); auto heartbeat_time = std::filesystem::last_write_time(config.runtime.heartbeat_path); config.runtime.exit_after_startup = true; ready = vamp_zero::run({.config_path = config_file(root.path, config)}); BOOST_REQUIRE_MESSAGE(ready, (ready ? "" : ready.error().describe())); BOOST_TEST(read_file(config.runtime.heartbeat_path) == "existing heartbeat"); BOOST_TEST((std::filesystem::last_write_time(config.runtime.heartbeat_path) == heartbeat_time)); require_no_probe(config, true); } BOOST_AUTO_TEST_CASE(readiness_validates_later_pages_and_parts) { for (std::char_t const *relative : {"songs/first.pages/2.png", "songs/second.pages/2.png", "practice/001/electric.pages/2.png"}) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); std::filesystem::remove(std::filesystem::path{config.runtime.assets_path} / relative); auto ready = run_once(root.path, config); BOOST_REQUIRE(!ready); BOOST_TEST(std::string{ready.error().message()}.find("page is missing") != std::string::npos); BOOST_TEST(!std::filesystem::exists(config.runtime.heartbeat_path)); } } BOOST_AUTO_TEST_CASE(readiness_validates_generated_audio_using_configured_asset_root) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); auto assets = std::filesystem::path{config.runtime.assets_path}; auto fixture = std::filesystem::path{VAMP_TEST_PLAYBACK_ASSETS_DIR}; std::filesystem::copy(fixture / "audio", assets / "audio", std::filesystem::copy_options::recursive); auto path = assets / "practice/001/acoustic.json"; write_score(path, "Acoustic"); auto score = boost::json::parse(read_file(path)); score.as_object()["playback"] = boost::json::parse(read_file(fixture / "score.json")).at("playback"); write_file(path, boost::json::serialize(score)); BOOST_REQUIRE(vamp_test::build_catalog(assets, config.database.path)); auto ready = run_once(root.path, config); BOOST_REQUIRE_MESSAGE(ready, (ready ? "" : ready.error().describe())); auto &sample = score.at("playback").at("samples").as_array().front().as_object(); auto audio = assets / std::string{sample.at("path").as_string()}; auto corrupt = read_file(audio); corrupt[0] = 'X'; sample["sha256"] = checksum(corrupt); write_file(audio, corrupt); write_file(path, boost::json::serialize(score)); BOOST_REQUIRE(vamp_test::build_catalog(assets, config.database.path)); ready = run_once(root.path, config); BOOST_REQUIRE(!ready); BOOST_TEST(std::string{ready.error().message()}.find("PCM16 WAV") != std::string::npos); } BOOST_AUTO_TEST_CASE(readiness_rejects_corrupt_checksums_and_escaping_pages) { for (std::bool_t escape : {false, true}) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); auto page = std::filesystem::path{config.runtime.assets_path} / "songs" / "second.pages" / "2.png"; if (escape) { write_file(root.path / "outside.png", fixture_page()); std::filesystem::remove(page); std::filesystem::create_symlink(root.path / "outside.png", page); } else { auto bytes = fixture_page(); bytes.back() ^= 1; write_file(page, bytes); } auto ready = run_once(root.path, config); BOOST_REQUIRE(!ready); auto expected = std::string_view{escape ? "escapes its asset directory" : "checksum mismatch"}; BOOST_TEST(std::string{ready.error().message()}.find(expected) != std::string::npos); } } BOOST_AUTO_TEST_CASE(readiness_rejects_missing_or_malformed_catalogs) { auto root = temporary_directory{}; auto config = fixture_config(root.path); BOOST_TEST(!run_once(root.path, config)); make_assets(config); write_file(config.database.path, "not a database"); BOOST_TEST(!run_once(root.path, config)); BOOST_TEST(!std::filesystem::exists(config.runtime.heartbeat_path)); } BOOST_AUTO_TEST_CASE(readiness_rejects_unusable_database_and_runtime_paths) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); write_file(root.path / "blocked", "regular file"); config.database.path = (root.path / "blocked" / "vamp.db").string(); BOOST_TEST(!run_once(root.path, config)); config.database.path = (root.path / "db" / "vamp.db").string(); config.runtime.heartbeat_path = (root.path / "blocked" / "heartbeat.json").string(); BOOST_TEST(!run_once(root.path, config)); BOOST_TEST(read_file(root.path / "blocked") == "regular file"); } BOOST_AUTO_TEST_CASE(readiness_decodes_pages_even_when_checksums_match) { for (auto kind : {"truncated", "missing pixels", "wrong dimensions", "oversized"}) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); auto bytes = fixture_page(std::string_view{kind} == "wrong dimensions" ? 10 : 976); if (std::string_view{kind} == "truncated") { bytes.resize(24U); } else if (std::string_view{kind} == "missing pixels") { bytes.resize(33U); } else if (std::string_view{kind} == "oversized") { bytes[16] = 127; } auto score_path = std::filesystem::path{config.runtime.assets_path} / "songs" / "first.json"; auto score = boost::json::parse(read_file(score_path)); auto &page = score.at("presentation").at("pages").as_array()[0].as_object(); write_file(score_path.parent_path() / std::string{page.at("path").as_string()}, bytes); page["sha256"] = checksum(bytes); write_file(score_path, boost::json::serialize(score)); BOOST_REQUIRE(vamp_test::build_catalog(config.runtime.assets_path, config.database.path)); auto ready = run_once(root.path, config); BOOST_REQUIRE_MESSAGE(!ready, kind); BOOST_TEST(ready.error().describe().find("first.pages/1.png") != std::string::npos); BOOST_TEST(!std::filesystem::exists(config.runtime.heartbeat_path)); } } BOOST_AUTO_TEST_CASE(readiness_rejects_actual_heartbeat_directories_and_symlinks) { for (auto kind : {"directory", "symlink", "dangling symlink"}) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); std::filesystem::create_directories(std::filesystem::path{config.runtime.heartbeat_path}.parent_path()); if (std::string_view{kind} == "directory") { std::filesystem::create_directory(config.runtime.heartbeat_path); } else { if (std::string_view{kind} == "symlink") { write_file(root.path / "target", "untouched"); } std::filesystem::create_symlink(root.path / "target", config.runtime.heartbeat_path); } auto ready = run_once(root.path, config); BOOST_REQUIRE(!ready); BOOST_TEST(ready.error().describe().find(config.runtime.heartbeat_path) != std::string::npos); require_no_probe(config, true); if (std::string_view{kind} == "symlink") { BOOST_TEST(read_file(root.path / "target") == "untouched"); } } } BOOST_AUTO_TEST_CASE(readiness_rejects_unwritable_parent_without_leaving_probes) { if (::geteuid() == 0) { BOOST_TEST_MESSAGE("Permission check requires an unprivileged test process"); return; } auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); auto parent = std::filesystem::path{config.runtime.heartbeat_path}.parent_path(); std::filesystem::create_directory(parent); std::filesystem::permissions(parent, std::filesystem::perms::owner_read | std::filesystem::perms::owner_exec); auto ready = run_once(root.path, config); std::filesystem::permissions(parent, std::filesystem::perms::owner_all); BOOST_TEST(!ready); require_no_probe(config); } BOOST_AUTO_TEST_CASE(readiness_rejects_occupied_bind_and_releases_successful_probe) { auto root = temporary_directory{}; auto config = fixture_config(root.path); make_assets(config); auto service = http_service{health_router(healthy_payload())}; config.control = service.config().control; write_file(config.runtime.heartbeat_path, "existing heartbeat"); BOOST_TEST(!run_once(root.path, config)); BOOST_TEST(read_file(config.runtime.heartbeat_path) == "existing heartbeat"); require_no_probe(config, true); config.control.endpoint.bind = "127.0.0.1:" + std::to_string(unused_port()); auto ready = run_once(root.path, config); BOOST_REQUIRE_MESSAGE(ready, (ready ? "" : ready.error().describe())); auto io = boost::asio::io_context{}; auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint); BOOST_REQUIRE(endpoint); auto server = ligo::http::server{io, *endpoint, {}}; BOOST_TEST(server.start().has_value()); server.stop(); io.run(); require_no_probe(config, true); }