yams/tests/unit/daemon/components/startup_replay_catch2_test.cpp
Trevon 6248c87bcc
Some checks are pending
Fuzz Smoke / libFuzzer smoke (push) Waiting to run
test(daemon): replay startup schedules under a fuzzer (#335)
Startup ordering defects were only observable by waiting minutes on a live
daemon, and the one that aborted it took a live restart loop to find. Give the
startup contracts a deterministic harness that runs in milliseconds.

+ startup_fuzz_support.h drives the real ServiceManagerFsm and the real
  DeferralGate with virtual time: legal phase advances, injected failure,
  shutdown and terminal reset, idle/busy, pressure/calm, starvation deadlines
  and cancellation
+ oracles fail closed on shutdown resurrection without reset, failure-to-ready,
  eligibility disagreeing with fsm.isReady(), admission outside the virtual
  deadline, and ineligible work touching the admission gate; fuzzed IEEE-754
  components must never yield non-finite or out-of-range scores
+ catch2_startup_replay runs named regressions plus 1024 seeded traces per run
  and reports the root seed and action offset for replay
+ fuzz_startup builds the same harness for libFuzzer/AFL, with a seed generator
  that refuses to overwrite different existing files
- the fuzzing lane no longer defaults to TSan: harnesses link
  -fsanitize=fuzzer,address,undefined and the two runtimes cannot share a
  process; combining them now fails with an explanation instead of producing a
  binary that cannot start
- documented the lane invocation, target naming and the coverage limits,
  including that governor call ordering inside the poller is not covered

Scope: contract-valid single-owner schedules. Executor interleavings, late
callbacks, concurrent FSM instances and filesystem/model faults are not modeled.

Signed-off-by: trvon <git@trevon.dev>
2026-10-22 16:48:50 -06:00

195 lines
8.9 KiB
C++

// Copyright (c) 2026 YAMS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#include <catch2/catch_get_random_seed.hpp>
#include <catch2/catch_test_macros.hpp>
#include <array>
#include <chrono>
#include <limits>
#include <thread>
#include <utility>
#include <vector>
#include <yams/daemon/components/startup_phase.h>
#include "../../../../tools/fuzzing/startup_fuzz_support.h"
#include "../../../common/test_helpers_catch2.h"
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t*, std::size_t);
namespace {
using yams::daemon::ServiceManagerState;
using yams::fuzz::StartupAction;
constexpr auto action(StartupAction value) {
return static_cast<std::uint8_t>(value);
}
TEST_CASE("A starvation deadline cannot make incomplete startup maintenance eligible",
"[daemon][startup-replay][pressure]") {
yams::test::SpdlogLevelGuard logging;
spdlog::set_level(spdlog::level::off);
std::vector<std::uint8_t> trace{
action(StartupAction::Pressure), action(StartupAction::Advance),
action(StartupAction::Advance), action(StartupAction::Advance),
action(StartupAction::Advance), action(StartupAction::Deadline),
action(StartupAction::Probe)};
auto incomplete = yams::fuzz::replayStartup(trace);
REQUIRE(incomplete.ok());
CHECK(incomplete.admissions == 0);
CHECK(incomplete.forcedAdmissions == 0);
// Search initialization finishes only AFTER the first five-minute deadline.
trace.push_back(action(StartupAction::Advance));
trace.push_back(action(StartupAction::Advance));
auto justReady = yams::fuzz::replayStartup(trace);
REQUIRE(justReady.ok());
CHECK(justReady.admissions == 0); // ineligible time did not age the pressure gate
trace.push_back(action(StartupAction::Deadline));
auto eventual = yams::fuzz::replayStartup(trace);
REQUIRE(eventual.ok());
CHECK(eventual.admissions == 1);
CHECK(eventual.forcedAdmissions == 1);
trace.push_back(action(StartupAction::Cancel));
trace.push_back(action(StartupAction::Deadline));
auto cancelled = yams::fuzz::replayStartup(trace);
REQUIRE(cancelled.ok());
CHECK(cancelled.admissions == eventual.admissions);
}
TEST_CASE("Startup replay reaches ready with optional vectors enabled or skipped",
"[daemon][startup-replay][readiness]") {
yams::test::SpdlogLevelGuard logging;
spdlog::set_level(spdlog::level::off);
for (const std::uint8_t flags : {std::uint8_t{0}, std::uint8_t{0x80}}) {
const std::vector<std::uint8_t> trace(7, action(StartupAction::Advance) | flags);
const auto replay = yams::fuzz::replayStartup(trace);
REQUIRE(replay.ok());
CHECK(replay.admissions > 0);
CHECK((replay.statesVisited & (1U << static_cast<unsigned>(ServiceManagerState::Ready))) !=
0);
}
}
TEST_CASE("Failure and shutdown cannot be revived by later startup advances",
"[daemon][startup-replay][shutdown]") {
yams::test::SpdlogLevelGuard logging;
spdlog::set_level(spdlog::level::off);
for (std::size_t phase = 0; phase < 6; ++phase) {
CAPTURE(phase);
std::vector<std::uint8_t> trace(phase, action(StartupAction::Advance));
trace.insert(trace.end(),
{action(StartupAction::Fail), action(StartupAction::Deadline),
action(StartupAction::Advance), action(StartupAction::Shutdown),
action(StartupAction::FinishShutdown), action(StartupAction::Advance)});
auto failed = yams::fuzz::replayStartup(trace);
REQUIRE(failed.ok());
CHECK(failed.admissions == 0);
trace.push_back(action(StartupAction::Reset));
trace.insert(trace.end(), 7, action(StartupAction::Advance));
auto restarted = yams::fuzz::replayStartup(trace);
REQUIRE(restarted.ok());
CHECK(restarted.admissions > 0);
}
}
TEST_CASE("Seeded startup schedules and float bit patterns satisfy production contracts",
"[daemon][startup-replay][seeded]") {
yams::test::SpdlogLevelGuard logging;
spdlog::set_level(spdlog::level::off);
const auto rootSeed = Catch::getSeed();
std::uint32_t visited = 0;
for (std::uint32_t round = 0; round < 1024; ++round) {
CAPTURE(rootSeed, round);
auto state = rootSeed ^ (round * 2654435761U);
std::array<std::uint8_t, 256> trace{};
for (auto& byte : trace) {
state = state * 1664525U + 1013904223U;
byte = static_cast<std::uint8_t>(state >> 24);
// Bias enough valid advancement to exercise Ready as well as abort paths.
if ((state & 3U) == 0) {
byte = action(StartupAction::Advance) | (byte & 0x80U);
}
}
const auto replay = yams::fuzz::replayStartup(trace);
CAPTURE(replay.offset, replay.error);
REQUIRE(replay.ok());
REQUIRE(yams::fuzz::semanticScoresBounded(trace));
visited |= replay.statesVisited;
}
for (auto phase : {ServiceManagerState::Ready, ServiceManagerState::Failed,
ServiceManagerState::ShuttingDown, ServiceManagerState::Stopped,
ServiceManagerState::VectorsReady}) {
CAPTURE(phase);
CHECK((visited & (1U << static_cast<unsigned>(phase))) != 0);
}
}
TEST_CASE("Coverage fuzzer entry reuses the startup replay and score contracts",
"[daemon][startup-replay][fuzzer-entry]") {
yams::test::SpdlogLevelGuard logging;
const std::array<std::uint8_t, 64> trace{};
CHECK(LLVMFuzzerTestOneInput(trace.data(), trace.size()) == 0);
}
TEST_CASE("Semantic scores and explicit thresholds reject nonfinite values",
"[daemon][startup-replay][semantic-values]") {
using namespace yams::daemon::embed;
const auto nan = std::numeric_limits<float>::quiet_NaN();
const auto infinity = std::numeric_limits<float>::infinity();
const std::vector<float> good{1.0f}, opposite{-1.0f}, zero{0.0f}, bad{nan}, huge{infinity};
CHECK(semanticInverseNorm(zero) == 0.0f);
CHECK(semanticInverseNorm(bad) == 0.0f);
CHECK(semanticInverseNorm(huge) == 0.0f);
CHECK(semanticCosineSimilarity(good, 1.0f, good, 1.0f) == 1.0f);
CHECK(semanticCosineSimilarity(good, 1.0f, opposite, 1.0f) == -1.0f);
CHECK(semanticCosineSimilarity(bad, 1.0f, good, 1.0f) == 0.0f);
CHECK(semanticCosineSimilarity(good, infinity, good, 1.0f) == 0.0f);
CHECK(semanticCosineSimilarity(good, nan, good, 1.0f) == 0.0f);
CHECK(semanticCosineSimilarity(good, 0.0f, good, 1.0f) == 0.0f);
CHECK(semanticCosineSimilarity(good, 1.0f, std::span<const float>{}, 1.0f) == 0.0f);
CHECK_FALSE(semanticExplicitThreshold(nan).has_value());
CHECK_FALSE(semanticExplicitThreshold(infinity).has_value());
CHECK(semanticExplicitThreshold(-infinity) == std::nullopt);
CHECK(semanticExplicitThreshold(-2.0f) == 0.0f);
CHECK(semanticExplicitThreshold(2.0f) == 1.0f);
CHECK(semanticExplicitThreshold(0.75f) == 0.75f);
}
} // namespace
TEST_CASE("Startup phase reporting names each phase and advances elapsed time",
"[daemon][startup-phase][visibility]") {
// A 50-minute stall was previously reported as a bare state number at debug level. This pins
// the exact label per phase (a swap must fail) and that elapsed time actually advances.
// Marker *placement* in ServiceManager is not unit-tested here: it needs a full init run.
using yams::daemon::StartupPhase;
using yams::daemon::startupPhaseName;
const std::array<std::pair<StartupPhase, std::string_view>, 7> expected{{
{StartupPhase::Idle, "idle"},
{StartupPhase::VectorsInitialized, "vectors_initialized"},
{StartupPhase::EmbeddingReconcile, "embedding_reconcile"},
{StartupPhase::MemorySyncApply, "memory_sync_apply"},
{StartupPhase::DirectP2p, "direct_p2p_recovery"},
{StartupPhase::SearchBuild, "search_build"},
{StartupPhase::AsyncInitComplete, "async_init_complete"},
}};
for (const auto& [phase, name] : expected) {
CAPTURE(static_cast<int>(phase));
CHECK(startupPhaseName(phase) == name);
}
CHECK(startupPhaseName(static_cast<StartupPhase>(200)) == "unknown");
yams::daemon::StartupPhaseTracker tracker;
CHECK(tracker.phase() == StartupPhase::Idle);
CHECK(tracker.elapsedMs() == 0); // idle never reports elapsed time
tracker.begin(StartupPhase::MemorySyncApply);
CHECK(tracker.phase() == StartupPhase::MemorySyncApply);
// Bounded spin, not a sleep loop: prove elapsed time advances rather than merely being >= 0.
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds{200};
while (tracker.elapsedMs() == 0 && std::chrono::steady_clock::now() < deadline) {
std::this_thread::yield();
}
CHECK(tracker.elapsedMs() > 0);
tracker.begin(StartupPhase::SearchBuild);
CHECK(tracker.phase() == StartupPhase::SearchBuild);
CHECK(startupPhaseName(tracker.phase()) == "search_build");
}