solverforge-solver 0.18.0

Solver engine for SolverForge
Documentation
use std::time::Duration;

use super::*;
use std::cmp::Ordering;

#[test]
fn solver_snapshot_preserves_exact_counts_and_durations() {
    let mut stats = SolverStats::default();
    stats.start();
    stats.record_step();
    stats.record_generated_batch(3, Duration::from_millis(4));
    stats.record_evaluated_move(Duration::from_millis(5));
    stats.record_move_accepted();
    stats.record_score_calculation();

    let snapshot = stats.snapshot();

    assert_eq!(snapshot.step_count, 1);
    assert_eq!(snapshot.moves_generated, 3);
    assert_eq!(snapshot.moves_evaluated, 1);
    assert_eq!(snapshot.moves_accepted, 1);
    assert_eq!(snapshot.score_calculations, 1);
    assert_eq!(snapshot.generation_time, Duration::from_millis(4));
    assert_eq!(snapshot.evaluation_time, Duration::from_millis(5));
    assert!(snapshot.phase.is_none());
}

#[test]
fn phase_stats_track_generation_and_evaluation_separately() {
    let mut stats = PhaseStats::new(2, "LocalSearch");
    stats.record_step();
    stats.record_generated_batch(7, Duration::from_millis(8));
    stats.record_evaluated_move(Duration::from_millis(9));
    stats.record_move_accepted();
    stats.record_score_calculation();

    assert_eq!(stats.phase_index, 2);
    assert_eq!(stats.phase_type, "LocalSearch");
    assert_eq!(stats.step_count, 1);
    assert_eq!(stats.moves_generated, 7);
    assert_eq!(stats.moves_evaluated, 1);
    assert_eq!(stats.moves_accepted, 1);
    assert_eq!(stats.score_calculations, 1);
    assert_eq!(stats.generation_time(), Duration::from_millis(8));
    assert_eq!(stats.evaluation_time(), Duration::from_millis(9));
    let snapshot = stats.snapshot();
    assert_eq!(snapshot.phase_index, 2);
    assert_eq!(snapshot.phase_type, "LocalSearch");
    assert_eq!(snapshot.step_count, 1);
    assert_eq!(snapshot.moves_generated, 7);
    assert_eq!(snapshot.moves_evaluated, 1);
    assert_eq!(snapshot.moves_accepted, 1);
    assert_eq!(snapshot.score_calculations, 1);
    assert_eq!(snapshot.generation_time, Duration::from_millis(8));
    assert_eq!(snapshot.evaluation_time, Duration::from_millis(9));
}

#[test]
fn solver_snapshot_includes_selector_level_telemetry() {
    let mut stats = SolverStats::default();
    stats.start();
    stats.record_selector_generated(2, 3, Duration::from_millis(4));
    stats.record_selector_evaluated(2, Duration::from_millis(5));
    stats.record_selector_accepted(2);
    stats.record_selector_applied(2);

    let snapshot = stats.snapshot();

    assert_eq!(snapshot.moves_generated, 3);
    assert_eq!(snapshot.moves_evaluated, 1);
    assert_eq!(snapshot.moves_accepted, 1);
    assert_eq!(snapshot.moves_applied, 1);
    assert_eq!(snapshot.selector_telemetry.len(), 1);
    assert_eq!(snapshot.selector_telemetry[0].selector_index, 2);
    assert_eq!(snapshot.selector_telemetry[0].selector_label, "selector-2");
    assert_eq!(snapshot.selector_telemetry[0].moves_generated, 3);
    assert_eq!(snapshot.selector_telemetry[0].moves_evaluated, 1);
    assert_eq!(snapshot.selector_telemetry[0].moves_accepted, 1);
    assert_eq!(snapshot.selector_telemetry[0].moves_applied, 1);
}

#[test]
fn solver_snapshot_includes_move_level_telemetry() {
    let mut stats = SolverStats::default();

    stats.record_move_kind_generated("scalar_assignment_value_run_gap_swap");
    stats.record_move_kind_evaluated("scalar_assignment_value_run_gap_swap", Ordering::Greater);
    stats.record_move_kind_accepted("scalar_assignment_value_run_gap_swap");
    stats.record_move_kind_applied("scalar_assignment_value_run_gap_swap", 25.0);
    stats.record_move_kind_forager_ignored("scalar_assignment_value_run_gap_swap", 2);

    let snapshot = stats.snapshot();

    assert_eq!(snapshot.move_telemetry.len(), 1);
    assert_eq!(
        snapshot.move_telemetry[0].move_label,
        "scalar_assignment_value_run_gap_swap"
    );
    assert_eq!(snapshot.move_telemetry[0].moves_generated, 1);
    assert_eq!(snapshot.move_telemetry[0].moves_evaluated, 1);
    assert_eq!(snapshot.move_telemetry[0].moves_accepted, 1);
    assert_eq!(snapshot.move_telemetry[0].moves_applied, 1);
    assert_eq!(snapshot.move_telemetry[0].moves_score_improving, 1);
    assert_eq!(snapshot.move_telemetry[0].moves_forager_ignored, 2);
    assert_eq!(snapshot.move_telemetry[0].applied_score_improvement, 25.0);
}

#[test]
fn solver_snapshot_includes_bounded_applied_move_trace() {
    let mut stats = SolverStats::default();

    stats.record_applied_move_trace(AppliedMoveTelemetry {
        step_index: 7,
        move_label: "scalar_assignment_value_window_swap",
        selected_candidate_index: 42,
        moves_generated_this_step: 43,
        moves_evaluated_this_step: 43,
        moves_accepted_this_step: 1,
        moves_forager_ignored_this_step: 0,
        score_before: -100.0,
        score_after: -80.0,
        score_delta: 20.0,
        hard_feasible_before: true,
        hard_feasible_after: true,
    });

    let snapshot = stats.snapshot();

    assert_eq!(snapshot.applied_move_trace.len(), 1);
    assert_eq!(snapshot.applied_move_trace[0].step_index, 7);
    assert_eq!(
        snapshot.applied_move_trace[0].move_label,
        "scalar_assignment_value_window_swap"
    );
    assert_eq!(snapshot.applied_move_trace[0].selected_candidate_index, 42);
    assert_eq!(snapshot.applied_move_trace[0].score_delta, 20.0);
    assert!(snapshot.applied_move_trace[0].hard_feasible_after);
}

#[test]
fn compact_solver_snapshot_omits_applied_move_trace() {
    let mut stats = SolverStats::default();

    stats.record_applied_move_trace(AppliedMoveTelemetry {
        step_index: 1,
        move_label: "scalar_assignment_value_window_swap",
        selected_candidate_index: 0,
        hard_feasible_before: true,
        hard_feasible_after: true,
        ..AppliedMoveTelemetry::default()
    });

    assert_eq!(stats.snapshot().applied_move_trace.len(), 1);
    assert!(stats
        .snapshot_without_applied_move_trace()
        .applied_move_trace
        .is_empty());
}

#[test]
fn solver_snapshot_prefers_observed_selector_label() {
    let mut stats = SolverStats::default();
    stats.record_selector_generated(2, 1, Duration::from_millis(1));
    stats.record_selector_generated_with_label(2, "conflict_repair", 1, Duration::from_millis(2));

    let snapshot = stats.snapshot();

    assert_eq!(
        snapshot.selector_telemetry[0].selector_label,
        "conflict_repair"
    );
    assert_eq!(snapshot.selector_telemetry[0].moves_generated, 2);
}

#[test]
fn solver_scalar_assignment_remaining_aggregates_by_group() {
    let mut stats = SolverStats::default();

    stats.record_scalar_assignment_required_remaining("group_a", 3);
    stats.record_scalar_assignment_required_remaining("group_b", 0);
    assert_eq!(stats.scalar_assignment_required_remaining, 3);
    assert_eq!(stats.snapshot().scalar_assignment_required_remaining, 3);

    stats.record_scalar_assignment_required_remaining("group_a", 0);
    assert_eq!(stats.scalar_assignment_required_remaining, 0);
    assert_eq!(stats.snapshot().scalar_assignment_required_remaining, 0);
}

#[test]
fn unattributed_applied_moves_do_not_create_selector_zero_telemetry() {
    let mut stats = SolverStats::default();
    stats.record_generated_move(Duration::from_millis(1));
    stats.record_evaluated_move(Duration::from_millis(2));
    stats.record_move_accepted();
    stats.record_move_applied();

    let snapshot = stats.snapshot();

    assert_eq!(snapshot.moves_generated, 1);
    assert_eq!(snapshot.moves_evaluated, 1);
    assert_eq!(snapshot.moves_accepted, 1);
    assert_eq!(snapshot.moves_applied, 1);
    assert!(snapshot.selector_telemetry.is_empty());
}

#[test]
fn throughput_helpers_use_stage_specific_durations() {
    let mut solver_stats = SolverStats::default();
    solver_stats.start();
    solver_stats.record_generated_batch(5, Duration::from_millis(7));
    solver_stats.record_evaluated_move(Duration::from_millis(11));

    let mut phase_stats = PhaseStats::new(1, "LocalSearch");
    phase_stats.record_generated_batch(3, Duration::from_millis(13));
    phase_stats.record_evaluated_move(Duration::from_millis(17));

    assert_eq!(
        solver_stats.generated_throughput(),
        Throughput {
            count: 5,
            elapsed: Duration::from_millis(7),
        }
    );
    assert_eq!(
        solver_stats.evaluated_throughput(),
        Throughput {
            count: 1,
            elapsed: Duration::from_millis(11),
        }
    );
    assert_eq!(
        phase_stats.generated_throughput(),
        Throughput {
            count: 3,
            elapsed: Duration::from_millis(13),
        }
    );
    assert_eq!(
        phase_stats.evaluated_throughput(),
        Throughput {
            count: 1,
            elapsed: Duration::from_millis(17),
        }
    );
}

#[test]
fn whole_units_per_second_uses_integer_rate_math() {
    assert_eq!(whole_units_per_second(3, Duration::from_millis(2_000)), 1);
    assert_eq!(whole_units_per_second(9, Duration::from_secs(2)), 4);
    assert_eq!(whole_units_per_second(5, Duration::ZERO), 0);
}

#[test]
fn format_duration_uses_exact_integer_units() {
    assert_eq!(format_duration(Duration::from_millis(750)), "750ms");
    assert_eq!(format_duration(Duration::from_millis(2_500)), "2s 500ms");
    assert_eq!(format_duration(Duration::from_secs(125)), "2m 5s");
    assert_eq!(format_duration(Duration::from_micros(42)), "42us");
}

fn provisional_candidate_trace() -> CandidateTraceTelemetry {
    CandidateTraceTelemetry::new(
        CandidateTraceHeader::new(
            "[candidate_trace]\nmax_entries = 4\n".to_string(),
            CandidateTraceExecutionPolicy::known(
                "test.execution_policy",
                std::iter::empty::<(String, String)>(),
            ),
            CandidateTracePhasePlan::opaque("test.runtime.default.pending"),
            None,
        ),
        4,
    )
}

#[test]
fn candidate_trace_plan_stays_provisional_across_pause_and_resume_until_finalization() {
    let mut stats = SolverStats::default();
    stats.start();
    stats.enable_candidate_trace(provisional_candidate_trace());

    let before_pause = stats
        .snapshot()
        .candidate_trace
        .expect("enabled candidate trace");
    assert!(!before_pause.has_complete_execution_provenance());
    assert!(!before_pause.header.resolved_phase_plan_complete);
    let provisional_digest = before_pause.header.resolved_phase_plan_digest;

    stats.pause();
    let paused = stats
        .snapshot()
        .candidate_trace
        .expect("paused candidate trace");
    stats.resume();
    let resumed = stats
        .snapshot()
        .candidate_trace
        .expect("resumed candidate trace");

    assert_eq!(
        paused.header.resolved_phase_plan,
        before_pause.header.resolved_phase_plan
    );
    assert_eq!(paused.header.resolved_phase_plan_digest, provisional_digest);
    assert_eq!(
        resumed.header.resolved_phase_plan,
        before_pause.header.resolved_phase_plan
    );
    assert_eq!(
        resumed.header.resolved_phase_plan_digest,
        provisional_digest
    );
    assert!(!resumed.has_complete_execution_provenance());

    let terminal_plan = CandidateTracePhasePlan::known(
        "test.runtime.default",
        [("terminal", "completed")],
        vec![CandidateTracePhasePlan::known(
            "test.runtime.list_clarke_wright",
            std::iter::empty::<(String, String)>(),
            Vec::new(),
        )],
    );
    let expected_digest = CandidateTraceDigest::of_bytes(&terminal_plan.canonical_bytes());
    stats.finalize_candidate_trace_resolved_phase_plan(terminal_plan.clone());

    let terminal = stats
        .snapshot()
        .candidate_trace
        .expect("terminal candidate trace");
    assert_eq!(terminal.header.resolved_phase_plan, terminal_plan);
    assert_eq!(terminal.header.resolved_phase_plan_digest, expected_digest);
    assert!(terminal.header.resolved_phase_plan_complete);
    assert!(terminal.has_complete_execution_provenance());
    assert_eq!(
        terminal.provenance_status().qualification,
        CandidateTraceQualificationStatus::NotRequested
    );

    assert!(!paused.header.resolved_phase_plan_complete);
    assert_eq!(paused.header.resolved_phase_plan_digest, provisional_digest);
}

#[test]
#[should_panic(expected = "candidate trace resolved phase plan may be finalized only once")]
fn candidate_trace_plan_finalization_is_one_way() {
    let mut trace = provisional_candidate_trace();
    let plan = CandidateTracePhasePlan::known(
        "test.runtime.final",
        std::iter::empty::<(String, String)>(),
        Vec::new(),
    );
    trace.finalize_resolved_phase_plan(plan.clone());
    trace.finalize_resolved_phase_plan(plan);
}