use autoany_core::budget::BudgetController;
use autoany_core::evaluator::Evaluator;
use autoany_core::executor::Executor;
use autoany_core::ledger::Ledger;
use autoany_core::loop_engine::EgriLoop;
use autoany_core::proposer::Proposer;
use autoany_core::selector::DefaultSelector;
use autoany_core::spec::PromotionPolicy;
use autoany_core::types::*;
use autoany_core::{EgriError, Result};
#[derive(Clone, Debug)]
struct SimpleArtifact {
x: f64,
}
struct SimpleProposer {
perturbations: Vec<f64>,
index: std::cell::Cell<usize>,
}
impl Proposer for SimpleProposer {
type Artifact = SimpleArtifact;
fn propose(
&self,
artifact: &SimpleArtifact,
_ledger: &Ledger,
) -> Result<(Mutation, SimpleArtifact)> {
let idx = self.index.get();
let delta = self.perturbations.get(idx).copied().unwrap_or(0.1);
self.index.set(idx + 1);
let new_x = artifact.x + delta;
let mutation = Mutation {
operator: "perturb".into(),
description: format!("x += {delta:.2} -> {new_x:.2}"),
diff: Some(format!("{:.4} -> {:.4}", artifact.x, new_x)),
hypothesis: Some(format!("moving x by {delta:.2} may improve score")),
};
Ok((mutation, SimpleArtifact { x: new_x }))
}
}
struct SimpleExecutor;
impl Executor for SimpleExecutor {
type Artifact = SimpleArtifact;
fn execute(&self, artifact: &SimpleArtifact) -> Result<ExecutionResult> {
let score = -(artifact.x - 3.0).powi(2) + 10.0;
Ok(ExecutionResult {
duration_secs: 0.001,
exit_code: 0,
error: None,
output: Some(serde_json::json!({ "score": score })),
})
}
}
struct SimpleEvaluator;
impl Evaluator for SimpleEvaluator {
type Artifact = SimpleArtifact;
fn evaluate(&self, _artifact: &SimpleArtifact, execution: &ExecutionResult) -> Result<Outcome> {
let score = execution
.output
.as_ref()
.and_then(|o| o.get("score"))
.and_then(|v| v.as_f64())
.ok_or_else(|| EgriError::EvaluationFailed("no score in output".into()))?;
Ok(Outcome {
score: Score::Scalar(score),
constraints_passed: true,
constraint_violations: vec![],
evaluator_metadata: None,
})
}
}
#[test]
fn test_full_egri_loop() {
let proposer = SimpleProposer {
perturbations: vec![1.0, 1.0, 1.0, 0.5, -0.5, 0.1, -0.1],
index: std::cell::Cell::new(0),
};
let executor = SimpleExecutor;
let evaluator = SimpleEvaluator;
let selector = DefaultSelector::new(PromotionPolicy::KeepIfImproves, Direction::Maximize, None);
let budget = BudgetController::new(7, None);
let ledger = Ledger::in_memory();
let mut egri = EgriLoop::new(proposer, executor, evaluator, selector, budget, ledger);
let baseline = egri.baseline(SimpleArtifact { x: 0.0 }).unwrap();
assert_eq!(baseline.score.as_scalar().unwrap(), 1.0);
let summary = egri.run().unwrap();
println!("Total trials: {}", summary.total_trials);
println!("Promoted: {}", summary.promoted_count);
println!("Discarded: {}", summary.discarded_count);
println!("Baseline: {:?}", summary.baseline_score);
println!("Final: {:?}", summary.final_score);
let final_score = summary.final_score.as_ref().unwrap().as_scalar().unwrap();
assert!(final_score > 1.0, "should improve from baseline of 1.0");
let best = egri.best().unwrap();
assert!(
(best.x - 3.0).abs() < 1.0,
"best x={:.2} should be near 3.0",
best.x
);
assert!(egri.ledger().records().len() > 1);
println!("\n--- Ledger ---");
for r in egri.ledger().records() {
println!(
"{}: score={:?} action={} reason={}",
r.trial_id, r.outcome.score, r.decision.action, r.decision.reason
);
}
}
#[test]
fn test_budget_enforcement() {
let proposer = SimpleProposer {
perturbations: vec![0.1; 100],
index: std::cell::Cell::new(0),
};
let selector = DefaultSelector::new(PromotionPolicy::KeepIfImproves, Direction::Maximize, None);
let budget = BudgetController::new(3, None);
let ledger = Ledger::in_memory();
let mut egri = EgriLoop::new(
proposer,
SimpleExecutor,
SimpleEvaluator,
selector,
budget,
ledger,
);
egri.baseline(SimpleArtifact { x: 0.0 }).unwrap();
let summary = egri.run().unwrap();
assert_eq!(summary.total_trials, 3, "should stop after budget of 3");
}
#[test]
fn test_rollback() {
let proposer = SimpleProposer {
perturbations: vec![1.0],
index: std::cell::Cell::new(0),
};
let selector = DefaultSelector::new(PromotionPolicy::KeepIfImproves, Direction::Maximize, None);
let budget = BudgetController::new(5, None);
let ledger = Ledger::in_memory();
let mut egri = EgriLoop::new(
proposer,
SimpleExecutor,
SimpleEvaluator,
selector,
budget,
ledger,
);
egri.baseline(SimpleArtifact { x: 0.0 }).unwrap();
egri.step().unwrap();
let rolled_back = egri.rollback().unwrap();
assert!(
(rolled_back.x - 1.0).abs() < 0.001,
"rollback should return to last promoted state (x=1.0)"
);
}
#[test]
fn test_constraint_violation_discards() {
struct StrictEvaluator;
impl Evaluator for StrictEvaluator {
type Artifact = SimpleArtifact;
fn evaluate(&self, _a: &SimpleArtifact, exec: &ExecutionResult) -> Result<Outcome> {
let score = exec.output.as_ref().unwrap()["score"].as_f64().unwrap();
let passes = score <= 9.0;
Ok(Outcome {
score: Score::Scalar(score),
constraints_passed: passes,
constraint_violations: if passes {
vec![]
} else {
vec!["score exceeds safety limit of 9.0".into()]
},
evaluator_metadata: None,
})
}
}
let proposer = SimpleProposer {
perturbations: vec![1.0, 1.0, 1.0], index: std::cell::Cell::new(0),
};
let selector = DefaultSelector::new(PromotionPolicy::KeepIfImproves, Direction::Maximize, None);
let budget = BudgetController::new(3, None);
let ledger = Ledger::in_memory();
let mut egri = EgriLoop::new(
proposer,
SimpleExecutor,
StrictEvaluator,
selector,
budget,
ledger,
);
egri.baseline(SimpleArtifact { x: 0.0 }).unwrap();
let summary = egri.run().unwrap();
assert!(summary.discarded_count > 0);
for r in egri.ledger().records() {
println!(
"{}: score={:?} action={} violations={:?}",
r.trial_id, r.outcome.score, r.decision.action, r.outcome.constraint_violations
);
}
}