use std::path::Path;
use std::sync::Arc;
use proptest::prelude::*;
use crate::service::config::{AppConfig, LogDirSource};
use crate::service::eval_store::{build_meta, extract_strategy_from_id, list_eval_history};
use crate::service::path::{copy_dir, ContainedPath};
use crate::service::resolve::{
display_name, install_scenarios_from_dir, is_package_installed, make_require_code,
resolve_code, resolve_scenario_code, resolve_scenario_source, scenarios_dir,
};
use crate::service::AppService;
proptest! {
#[test]
fn resolve_code_never_panics(
code in proptest::option::of("[a-z]{0,50}"),
file in proptest::option::of("[a-z]{0,50}"),
) {
let _ = resolve_code(code, file);
}
#[test]
fn contained_path_rejects_traversal(
prefix in "[a-z]{0,5}",
suffix in "[a-z]{0,5}",
) {
let dir = tempfile::tempdir().unwrap();
let name = format!("{prefix}/../{suffix}");
let result = ContainedPath::child(dir.path(), &name);
prop_assert!(result.is_err());
}
#[test]
fn contained_path_accepts_simple_names(name in "[a-z][a-z0-9_-]{0,20}\\.json") {
let dir = tempfile::tempdir().unwrap();
let result = ContainedPath::child(dir.path(), &name);
prop_assert!(result.is_ok());
}
#[test]
fn make_require_code_contains_name(name in "[a-z_]{1,20}") {
let code = make_require_code(&name);
let expected = format!("require(\"{}\")", name);
prop_assert!(code.contains(&expected));
prop_assert!(code.contains("pkg.run(ctx)"));
}
#[test]
fn copy_dir_preserves_content(content in "[a-zA-Z0-9 ]{1,200}") {
let src = tempfile::tempdir().unwrap();
let dst = tempfile::tempdir().unwrap();
std::fs::write(src.path().join("test.txt"), &content).unwrap();
let dst_path = dst.path().join("out");
copy_dir(src.path(), &dst_path).unwrap();
let read = std::fs::read_to_string(dst_path.join("test.txt")).unwrap();
prop_assert_eq!(&read, &content);
}
}
#[test]
fn eval_rejects_no_scenario() {
let result = resolve_scenario_code(None, None, None);
assert!(result.is_err());
}
#[test]
fn resolve_scenario_code_inline() {
let result = resolve_scenario_code(Some("return 1".into()), None, None);
assert_eq!(result.unwrap(), "return 1");
}
#[test]
fn resolve_scenario_code_from_file() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut tmp, b"return 42").unwrap();
let result = resolve_scenario_code(None, Some(tmp.path().to_string_lossy().into()), None);
assert_eq!(result.unwrap(), "return 42");
}
#[test]
fn resolve_scenario_code_rejects_multiple() {
let result = resolve_scenario_code(Some("code".into()), Some("file".into()), None);
assert!(result.is_err());
assert!(result.unwrap_err().contains("only one"));
let result2 = resolve_scenario_code(Some("code".into()), None, Some("name".into()));
assert!(result2.is_err());
}
#[test]
fn resolve_scenario_code_by_name_not_found() {
let result = resolve_scenario_code(None, None, Some("nonexistent_test_xyz".into()));
assert!(result.is_err());
assert!(result.unwrap_err().contains("not found"));
}
#[test]
fn scenarios_dir_ends_with_expected_path() {
let dir = scenarios_dir().unwrap();
assert!(
dir.ends_with(".algocline/scenarios"),
"dir: {}",
dir.display()
);
}
#[test]
fn install_scenarios_from_dir_copies_lua_files() {
let source = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
std::fs::write(source.path().join("math_basic.lua"), "return {}").unwrap();
std::fs::write(source.path().join("safety.lua"), "return {}").unwrap();
std::fs::write(source.path().join("README.md"), "# docs").unwrap();
let result = install_scenarios_from_dir(source.path(), dest.path()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let installed = parsed["installed"].as_array().unwrap();
assert_eq!(installed.len(), 2);
assert!(dest.path().join("math_basic.lua").exists());
assert!(dest.path().join("safety.lua").exists());
assert!(!dest.path().join("README.md").exists());
assert_eq!(parsed["failures"].as_array().unwrap().len(), 0);
}
#[test]
fn install_scenarios_from_dir_skips_existing() {
let source = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
std::fs::write(source.path().join("existing.lua"), "return {new=true}").unwrap();
std::fs::write(dest.path().join("existing.lua"), "return {old=true}").unwrap();
let result = install_scenarios_from_dir(source.path(), dest.path()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["skipped"].as_array().unwrap().len(), 1);
assert_eq!(parsed["installed"].as_array().unwrap().len(), 0);
assert_eq!(parsed["failures"].as_array().unwrap().len(), 0);
let content = std::fs::read_to_string(dest.path().join("existing.lua")).unwrap();
assert!(content.contains("old=true"));
}
#[test]
fn install_scenarios_from_dir_empty_source_errors() {
let source = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
let result = install_scenarios_from_dir(source.path(), dest.path());
assert!(result.is_err());
assert!(result.unwrap_err().contains("No .lua"));
}
#[test]
fn install_scenarios_from_dir_collects_copy_failures() {
let source = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
let bad_dest = dest.path().join("nonexistent_subdir");
std::fs::write(source.path().join("ok.lua"), "return 1").unwrap();
let result = install_scenarios_from_dir(source.path(), &bad_dest);
let parsed: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
let failures = parsed["failures"].as_array().unwrap();
assert_eq!(failures.len(), 1, "expected 1 copy failure");
assert_eq!(parsed["installed"].as_array().unwrap().len(), 0);
}
#[test]
fn display_name_prefers_stem() {
let path = Path::new("/tmp/math_basic.lua");
assert_eq!(display_name(path, "math_basic.lua"), "math_basic");
}
#[test]
fn display_name_falls_back_to_file_name() {
let path = Path::new("");
assert_eq!(display_name(path, "fallback"), "fallback");
}
#[test]
fn resolve_scenario_source_prefers_subdir() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir(root.path().join("scenarios")).unwrap();
std::fs::write(root.path().join("scenarios").join("a.lua"), "").unwrap();
std::fs::write(root.path().join("root.lua"), "").unwrap();
let source = resolve_scenario_source(root.path());
assert_eq!(source, root.path().join("scenarios"));
}
#[test]
fn resolve_scenario_source_falls_back_to_root() {
let root = tempfile::tempdir().unwrap();
std::fs::write(root.path().join("a.lua"), "").unwrap();
let source = resolve_scenario_source(root.path());
assert_eq!(source, root.path());
}
#[test]
fn eval_auto_installs_evalframe_on_missing() {
let _home_lock = super::super::test_support::lock_home();
if is_package_installed("evalframe") {
return;
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let tmp = tempfile::tempdir().unwrap();
let fake_pkg_dir = tmp.path().join("empty_packages");
std::fs::create_dir_all(&fake_pkg_dir).unwrap();
let executor = Arc::new(rt.block_on(async {
algocline_engine::Executor::new(vec![fake_pkg_dir])
.await
.unwrap()
}));
let config = AppConfig {
log_dir: Some(tmp.path().join("logs")),
log_dir_source: LogDirSource::EnvVar,
log_enabled: false,
};
let svc = {
let _guard = rt.enter();
AppService::new(executor, config, vec![])
};
let scenario = r#"return { cases = {} }"#;
let result = rt.block_on(svc.eval(Some(scenario.into()), None, None, "cove", None, false));
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("bundled") || err.contains("evalframe"),
"unexpected error: {err}"
);
}
#[test]
fn extract_strategy_from_id_splits_correctly() {
assert_eq!(extract_strategy_from_id("cove_1710672000"), Some("cove"));
assert_eq!(
extract_strategy_from_id("my_strat_1710672000"),
Some("my_strat")
);
assert_eq!(extract_strategy_from_id("nostamp"), None);
}
#[test]
fn save_compare_result_persists_file() {
let tmp = tempfile::tempdir().unwrap();
let evals = tmp.path().join(".algocline").join("evals");
std::fs::create_dir_all(&evals).unwrap();
let filename = "compare_a_1_vs_b_2.json";
let path = ContainedPath::child(&evals, filename).unwrap();
let data = r#"{"test": true}"#;
std::fs::write(&*path, data).unwrap();
let read = std::fs::read_to_string(&*path).unwrap();
assert_eq!(read, data);
}
#[test]
fn build_meta_extracts_aggregated_fields() {
let result_json = serde_json::json!({
"result": {
"aggregated": {
"pass_rate": 0.75,
"scores": { "mean": 8.5 },
"total": 4,
"passed": 3
},
"summary": "3/4 passed"
},
"stats": {
"auto": {
"llm_calls": 12,
"elapsed_ms": 5000
}
}
});
let meta = build_meta("cot_1700000000", "cot", 1700000000, &result_json);
assert_eq!(meta["eval_id"], "cot_1700000000");
assert_eq!(meta["strategy"], "cot");
assert_eq!(meta["timestamp"], 1700000000_u64);
assert_eq!(meta["pass_rate"], 0.75);
assert_eq!(meta["mean_score"], 8.5);
assert_eq!(meta["total_cases"], 4);
assert_eq!(meta["passed"], 3);
assert_eq!(meta["llm_calls"], 12);
assert_eq!(meta["elapsed_ms"], 5000);
assert_eq!(meta["summary"], "3/4 passed");
}
#[test]
fn build_meta_handles_missing_fields() {
let result_json = serde_json::json!({});
let meta = build_meta("sc_1700000000", "sc", 1700000000, &result_json);
assert_eq!(meta["eval_id"], "sc_1700000000");
assert_eq!(meta["strategy"], "sc");
assert!(meta["pass_rate"].is_null());
assert!(meta["mean_score"].is_null());
assert!(meta["llm_calls"].is_null());
}
fn write_eval_files(dir: &Path, eval_id: &str, strategy: &str, timestamp: u64) {
let meta = serde_json::json!({
"eval_id": eval_id,
"strategy": strategy,
"timestamp": timestamp,
"pass_rate": 1.0,
});
let meta_path = dir.join(format!("{eval_id}.meta.json"));
std::fs::write(&meta_path, serde_json::to_string(&meta).unwrap()).unwrap();
let result_path = dir.join(format!("{eval_id}.json"));
std::fs::write(&result_path, r#"{"result":{}}"#).unwrap();
}
#[test]
fn eval_history_empty_dir() {
let tmp = tempfile::tempdir().unwrap();
let result = list_eval_history(tmp.path(), None, 10).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["evals"].as_array().unwrap().len(), 0);
}
#[test]
fn eval_history_nonexistent_dir() {
let tmp = tempfile::tempdir().unwrap();
let missing = tmp.path().join("nonexistent");
let result = list_eval_history(&missing, None, 10).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["evals"].as_array().unwrap().len(), 0);
}
#[test]
fn eval_history_sorts_newest_first() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
write_eval_files(dir, "cot_1000", "cot", 1000);
write_eval_files(dir, "cot_3000", "cot", 3000);
write_eval_files(dir, "cot_2000", "cot", 2000);
let result = list_eval_history(dir, None, 10).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let evals = parsed["evals"].as_array().unwrap();
assert_eq!(evals.len(), 3);
assert_eq!(evals[0]["timestamp"], 3000);
assert_eq!(evals[1]["timestamp"], 2000);
assert_eq!(evals[2]["timestamp"], 1000);
}
#[test]
fn eval_history_filters_by_strategy() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
write_eval_files(dir, "cot_1000", "cot", 1000);
write_eval_files(dir, "sc_2000", "sc", 2000);
write_eval_files(dir, "cot_3000", "cot", 3000);
let result = list_eval_history(dir, Some("cot"), 10).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let evals = parsed["evals"].as_array().unwrap();
assert_eq!(evals.len(), 2);
assert!(evals.iter().all(|e| e["strategy"] == "cot"));
}
#[test]
fn eval_history_respects_limit() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
write_eval_files(dir, "cot_1000", "cot", 1000);
write_eval_files(dir, "cot_2000", "cot", 2000);
write_eval_files(dir, "cot_3000", "cot", 3000);
let result = list_eval_history(dir, None, 2).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let evals = parsed["evals"].as_array().unwrap();
assert_eq!(evals.len(), 2);
assert_eq!(evals[0]["timestamp"], 3000);
assert_eq!(evals[1]["timestamp"], 2000);
}
#[test]
fn eval_history_skips_entries_without_meta() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
std::fs::write(dir.join("orphan_1000.json"), r#"{"result":{}}"#).unwrap();
write_eval_files(dir, "cot_2000", "cot", 2000);
let result = list_eval_history(dir, None, 10).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let evals = parsed["evals"].as_array().unwrap();
assert_eq!(evals.len(), 1);
assert_eq!(evals[0]["eval_id"], "cot_2000");
}