#include "agent/Agent.h" #include "agent/Codec.h" #include "common/Capability.h" #include "zeroerr/unittest.h" #include #include using namespace eve::agent; TEST_CASE("agent.gpuUnavailableAndTrainingFailure") { struct DeviceFailure final : IGpuPolicyBackend { bool ready = false; int trainingCalls = 0; std::string name() const override { return "injected-device"; } eve::Result available() const override { if (ready) return eve::Result::success(); return eve::Result::failure(eve::Diagnostic::error(eve::DiagnosticCode::Unsupported, "no device")); } eve::Result> evaluate(const Policy& p, const Observation& o) override { return infer(p, o); } eve::Result train(const Policy&, const Observation&, std::uint32_t, double) override { ++trainingCalls; return eve::Result::failure(eve::Diagnostic::error(eve::DiagnosticCode::Cancelled, "device lost")); } } provider; struct Environment final : IEnvironment { int resets = 0; eve::Result reset(std::uint64_t) override { ++resets; Observation o; o.features = {0}; o.legalActions = {0, 1}; return eve::Result::success(std::move(o)); } eve::Result step(std::uint32_t, double) override { Observation o; o.features = {1}; o.outcome = Outcome::Success; return eve::Result::success(std::move(o)); } } environment; Config config; config.backend = Backend::Gpu; config.population = 1; config.elites = 1; config.generations = 1; REQUIRE(!run(config, environment).ok()); REQUIRE_EQ(environment.resets, 0); struct Registration { IGpuPolicyBackend* provider; ~Registration() { eve::cap::revoke(provider); } } registration{&provider}; eve::cap::provide(&provider); REQUIRE(!run(config, environment).ok()); REQUIRE_EQ(environment.resets, 0); provider.ready = true; auto result = run(config, environment); REQUIRE(!result.ok()); REQUIRE(result.status().code() == eve::StatusCode::Cancelled); REQUIRE_EQ(provider.trainingCalls, 1); } namespace { class Domain final : public IEnvironment { public: int resets = 0; int tick = 0; bool failReset = false; bool failStep = false; bool invalidState = false; bool drift = false; eve::Result reset(std::uint64_t) override { ++resets; tick = 0; if (failReset) return eve::Result::failure( eve::Diagnostic::error(eve::DiagnosticCode::Unsupported, "reset unavailable")); return eve::Result::success(state(0)); } eve::Result step(std::uint32_t action, double) override { if (failStep) return eve::Result::failure( eve::Diagnostic::error(eve::DiagnosticCode::Cancelled, "cancelled")); ++tick; auto o = state(action == 1 ? 1 : 0); if (tick == 4 && action == 1) { o.outcome = Outcome::Failure; o.finding = "injected.invariant"; } return eve::Result::success(std::move(o)); } private: Observation state(double reward) const { Observation o; o.features = {float(tick) / 4}; if (invalidState) o.features[0] = std::numeric_limits::quiet_NaN(); if (drift) o.features[0] += 0.1f; o.legalActions = {0, 1}; o.reward = reward; o.coverage = {std::to_string(tick)}; return o; } }; Config smallConfig() { Config c; c.horizon = 4; c.population = 16; c.elites = 4; c.generations = 3; return c; } } // namespace TEST_CASE("agent.findingsReplayAndDeterminism") { Domain a, b; auto first = run(smallConfig(), a); auto second = run(smallConfig(), b); REQUIRE(first.ok()); REQUIRE(second.ok()); REQUIRE(first.value().failures > 0); REQUIRE_EQ(first.value().findings.size(), 1u); REQUIRE_EQ(first.value().episodes, 48u); REQUIRE_EQ(first.value().bestScore, second.value().bestScore); REQUIRE(first.value().policy.weights == second.value().policy.weights); REQUIRE(replay(first.value().findings.front(), b).ok()); b.drift = true; auto divergent = replay(first.value().findings.front(), b); REQUIRE(!divergent.ok()); REQUIRE(divergent.status().code() == eve::StatusCode::Conflict); } TEST_CASE("agent.invalidBeforeMutationAndErrorPropagation") { Domain environment; auto c = smallConfig(); c.dt = std::numeric_limits::quiet_NaN(); REQUIRE(!run(c, environment).ok()); REQUIRE_EQ(environment.resets, 0); c = smallConfig(); environment.failReset = true; auto missing = run(c, environment); REQUIRE(!missing.ok()); REQUIRE(missing.status().code() == eve::StatusCode::Unsupported); environment.failReset = false; environment.failStep = true; auto cancelled = run(c, environment); REQUIRE(!cancelled.ok()); REQUIRE(cancelled.status().code() == eve::StatusCode::Cancelled); environment.failStep = false; environment.invalidState = true; REQUIRE(!run(c, environment).ok()); } TEST_CASE("agent.policyTrainingMaskAndImport") { Domain environment; auto c = smallConfig(); auto result = run(c, environment); REQUIRE(result.ok()); REQUIRE(result.value().trainingSamples > 0); Observation state; state.features = {0}; state.legalActions = {1}; auto masked = infer(result.value().policy, state); REQUIRE(masked.ok()); REQUIRE_EQ(masked.value()[0], 0.0); REQUIRE_EQ(masked.value()[1], 1.0); auto policy = result.value().policy; policy.schemaVersion = 2; REQUIRE(!infer(policy, state).ok()); policy = result.value().policy; policy.weights[0] = std::numeric_limits::infinity(); REQUIRE(!infer(policy, state).ok()); auto badTrace = result.value().best; badTrace.schemaVersion = 2; const auto resets = environment.resets; REQUIRE(!replay(badTrace, environment).ok()); REQUIRE_EQ(environment.resets, resets); badTrace = result.value().best; badTrace.steps[0].action = 55; REQUIRE(!replay(badTrace, environment).ok()); REQUIRE_EQ(environment.resets, resets); } TEST_CASE("agent.randomBaselineAndBoundedFindings") { Domain environment; auto c = smallConfig(); c.strategy = Strategy::Random; c.maxFindings = 0; auto result = run(c, environment); REQUIRE(result.ok()); REQUIRE_EQ(result.value().trainingSamples, 0u); REQUIRE(result.value().failures > 0); REQUIRE(result.value().findings.empty()); REQUIRE_EQ(result.value().episodes, 48u); REQUIRE_EQ(result.value().steps, 192u); } TEST_CASE("agent.learningImprovesRewardedAction") { Domain environment; auto c = smallConfig(); c.generations = 8; c.trainingEpochs = 12; c.failureWeight = 0; c.coverageWeight = 0; c.strategy = Strategy::Random; auto baseline = run(c, environment); REQUIRE(baseline.ok()); c.strategy = Strategy::EvolutionLearning; auto trained = run(c, environment); REQUIRE(trained.ok()); auto observation = environment.reset(c.environmentSeed); REQUIRE(observation.ok()); auto before = infer(baseline.value().policy, observation.value()); auto after = infer(trained.value().policy, observation.value()); REQUIRE(before.ok()); REQUIRE(after.ok()); REQUIRE(after.value()[1] > before.value()[1] + 0.2); REQUIRE(after.value()[1] > 0.8); } TEST_CASE("agent.codecRoundTripAndUnknownFields") { Domain environment; auto c = smallConfig(); c.environmentSeed = std::numeric_limits::max(); auto result = run(c, environment); REQUIRE(result.ok()); auto json = encodePolicy(result.value().policy).toJson(); REQUIRE(json.ok()); auto value = eve::Value::fromJson(json.value()); REQUIRE(value.ok()); auto policy = decodePolicy(value.value()); REQUIRE(policy.ok()); REQUIRE(policy.value().weights == result.value().policy.weights); auto traceJson = encodeTrace(result.value().best).toJson(); REQUIRE(traceJson.ok()); auto traceValue = eve::Value::fromJson(traceJson.value()); REQUIRE(traceValue.ok()); auto trace = decodeTrace(traceValue.value()); REQUIRE(trace.ok()); REQUIRE_EQ(trace.value().environmentSeed, c.environmentSeed); REQUIRE(replay(trace.value(), environment).ok()); value.value().getIf()->emplace("unknown", 1); REQUIRE(!decodePolicy(value.value()).ok()); REQUIRE(!decodeConfig(eve::Value::object({{"population", 1.5}})).ok()); REQUIRE(!decodeConfig(eve::Value::object({{"strategy", "typo"}})).ok()); }