use converge_core::{Budget, ContextKey, ContextState, ConvergeResult, Engine};
use converge_optimization::packs::constraint_programming::ConstraintProgrammingPack;
use converge_optimization::packs::graph_partitioning::GraphPartitioningPack;
use converge_optimization::packs::job_shop_scheduling::JobShopSchedulingPack;
use converge_optimization::packs::staff_rostering::StaffRosteringPack;
use converge_optimization::packs::traveling_salesman::TravelingSalesmanPack;
use converge_pack::{PackInputPayload, PackPlanPayload, PackSuggestor, ProposedFact};
fn budget() -> Budget {
Budget {
max_cycles: 5,
max_facts: 100,
}
}
async fn run_with_input<P: converge_pack::Pack + 'static>(
pack: P,
input: serde_json::Value,
) -> ConvergeResult {
let pack_name = pack.name();
let mut engine = Engine::with_budget(budget());
engine.register_suggestor(PackSuggestor::new(
pack,
ContextKey::Seeds,
ContextKey::Strategies,
));
let mut ctx = ContextState::new();
let _ = ctx.add_proposal(ProposedFact::new(
ContextKey::Seeds,
"input-1",
PackInputPayload::new(pack_name, input),
"test",
));
engine.run(ctx).await.expect("should converge")
}
fn strategy_value(result: &ConvergeResult) -> &serde_json::Value {
let payload = result.context.get(ContextKey::Strategies)[0]
.require_payload::<PackPlanPayload>()
.expect("PackSuggestor emits typed pack plan payload");
&payload.plan
}
#[tokio::test]
async fn test_constraint_programming_formation() {
let result = run_with_input(
ConstraintProgrammingPack,
serde_json::json!({
"variables": [
{"name": "x", "min": 1, "max": 5},
{"name": "y", "min": 1, "max": 5}
],
"constraints": [
{"type": "not_equal", "args": {"var1": "x", "var2": "y"}}
],
"objective": {"variable": "x", "maximize": true}
}),
)
.await;
assert!(result.converged);
let strategies = result.context.get(ContextKey::Strategies);
assert_eq!(strategies.len(), 1);
let plan = strategy_value(&result);
assert!(plan["feasible"].as_bool().unwrap());
let assignments = plan["assignments"].as_array().unwrap();
let x_val = assignments.iter().find(|a| a["name"] == "x").unwrap()["value"]
.as_i64()
.unwrap();
let y_val = assignments.iter().find(|a| a["name"] == "y").unwrap()["value"]
.as_i64()
.unwrap();
assert_ne!(x_val, y_val, "x and y must differ (not_equal constraint)");
assert_eq!(x_val, 5, "x should be maximized to 5");
}
#[tokio::test]
async fn test_job_shop_scheduling_formation() {
let result = run_with_input(
JobShopSchedulingPack,
serde_json::json!({
"jobs": [
{"operations": [{"machine": 0, "duration": 3}, {"machine": 1, "duration": 2}]},
{"operations": [{"machine": 1, "duration": 4}, {"machine": 0, "duration": 1}]}
],
"machines": 2
}),
)
.await;
assert!(result.converged);
let strategies = result.context.get(ContextKey::Strategies);
assert_eq!(strategies.len(), 1);
let payload = strategy_value(&result);
let makespan = payload["makespan"].as_u64().unwrap();
assert!(makespan > 0, "makespan must be positive");
let schedule = payload["schedule"].as_array().unwrap();
assert_eq!(schedule.len(), 4, "should schedule all 4 operations");
}
#[tokio::test]
async fn test_staff_rostering_formation() {
let result = run_with_input(
StaffRosteringPack,
serde_json::json!({
"staff": [
{"id": "alice", "skills": ["nursing"], "max_hours": 40},
{"id": "bob", "skills": ["nursing", "admin"], "max_hours": 40}
],
"shifts": [
{"id": "morning", "required_skill": "nursing", "hours": 8, "period": "monday"},
{"id": "afternoon", "required_skill": "admin", "hours": 8, "period": "monday"}
]
}),
)
.await;
assert!(result.converged);
let strategies = result.context.get(ContextKey::Strategies);
assert_eq!(strategies.len(), 1);
let payload = strategy_value(&result);
let assignments = payload["assignments"].as_array().unwrap();
let unassigned = payload["unassigned_shifts"].as_array().unwrap();
assert!(
unassigned.is_empty(),
"all shifts should be assigned: {unassigned:?}"
);
assert_eq!(assignments.len(), 2, "both shifts should have assignments");
}
#[tokio::test]
async fn test_traveling_salesman_formation() {
let result = run_with_input(
TravelingSalesmanPack,
serde_json::json!({
"cities": [
{"x": 0.0, "y": 0.0},
{"x": 1.0, "y": 0.0},
{"x": 1.0, "y": 1.0},
{"x": 0.0, "y": 1.0}
]
}),
)
.await;
assert!(result.converged);
let strategies = result.context.get(ContextKey::Strategies);
assert_eq!(strategies.len(), 1);
let payload = strategy_value(&result);
let tour = payload["tour"].as_array().unwrap();
let distance = payload["total_distance"].as_f64().unwrap();
assert_eq!(tour.len(), 4, "tour should visit all 4 cities");
assert!(distance > 0.0, "total distance must be positive");
let mut visited: Vec<u64> = tour.iter().map(|c| c.as_u64().unwrap()).collect();
visited.sort_unstable();
assert_eq!(visited, vec![0, 1, 2, 3], "tour must visit every city");
}
#[tokio::test]
async fn test_graph_partitioning_formation() {
let result = run_with_input(
GraphPartitioningPack,
serde_json::json!({
"num_nodes": 6,
"edges": [
{"from": 0, "to": 1, "weight": 1.0},
{"from": 1, "to": 2, "weight": 1.0},
{"from": 3, "to": 4, "weight": 1.0},
{"from": 4, "to": 5, "weight": 1.0},
{"from": 2, "to": 3, "weight": 0.1}
],
"num_partitions": 2
}),
)
.await;
assert!(result.converged);
let strategies = result.context.get(ContextKey::Strategies);
assert_eq!(strategies.len(), 1);
let payload = strategy_value(&result);
let partition = payload["partition"].as_array().unwrap();
let cut_weight = payload["cut_weight"].as_f64().unwrap();
assert_eq!(
partition.len(),
6,
"every node must be assigned a partition"
);
let partitions_used: std::collections::HashSet<u64> =
partition.iter().map(|p| p.as_u64().unwrap()).collect();
assert_eq!(
partitions_used.len(),
2,
"should produce exactly 2 partitions"
);
assert!(
cut_weight <= 1.0,
"cut weight {cut_weight} should be small (weak edge is the natural cut)"
);
}