use pounce_studio_core::analysis::{
compare_runs, convergence_trace, diagnose, find_stalls, get_iterate, restoration_windows,
summarize, Severity,
};
use pounce_studio_core::iter_dump::IterDumpTrace;
use pounce_studio_core::markdown::render_inspect;
use pounce_studio_core::report::{Error, InputDescriptor, SolveReport, SOLVE_REPORT_SCHEMA};
const ROSENBROCK: &str = include_str!("../../../studio/mcp/fixtures/rosenbrock.json");
const STALLED: &str = include_str!("../../../studio/mcp/fixtures/rosenbrock-stalled.json");
const QUADRATIC: &str = include_str!("../../../studio/mcp/fixtures/quadratic.json");
const CIRCLE: &str = include_str!("../../../studio/mcp/fixtures/circle.json");
const EQ_TRACE: &[u8] = include_bytes!("../../../studio/mcp/fixtures/eq-quadratic.iterdump");
#[test]
fn fixtures_round_trip_schema() {
for (name, src) in [
("rosenbrock", ROSENBROCK),
("rosenbrock-stalled", STALLED),
("quadratic", QUADRATIC),
("circle", CIRCLE),
] {
let r = SolveReport::from_json_str(src).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(r.schema, SOLVE_REPORT_SCHEMA, "{name}");
}
}
#[test]
fn loads_cbf_file_input_descriptor() {
let mut v: serde_json::Value = serde_json::from_str(ROSENBROCK).unwrap();
v["fair_metadata"]["input"] = serde_json::json!({
"kind": "cbf-file",
"path": "/tmp/cblib/instance.cbf",
"size_bytes": 4096,
});
let src = serde_json::to_string(&v).unwrap();
let r = SolveReport::from_json_str(&src).expect("cbf-file report must load");
match r.fair_metadata.input {
InputDescriptor::CbfFile { path, size_bytes } => {
assert_eq!(path, "/tmp/cblib/instance.cbf");
assert_eq!(size_bytes, Some(4096));
}
other => panic!("expected CbfFile, got {other:?}"),
}
}
#[test]
fn rejects_wrong_schema() {
let bogus = r#"{"schema":"other/v1"}"#;
let err = SolveReport::from_json_str(bogus).expect_err("should fail");
assert!(matches!(err, Error::SchemaMismatch { .. }));
}
#[test]
fn rosenbrock_summary_matches_known_outcome() {
let r = SolveReport::from_json_str(ROSENBROCK).unwrap();
let s = summarize(&r);
assert_eq!(s.status, "SolveSucceeded");
assert!(s.iteration_count >= 1);
assert!(s.iterations_captured >= 1);
assert_eq!(s.restoration_calls, 0);
assert_eq!(s.n_variables, 2);
}
#[test]
fn convergence_trace_columns_aligned() {
let r = SolveReport::from_json_str(ROSENBROCK).unwrap();
let t = convergence_trace(&r);
let n = t.iter.len();
assert_eq!(t.objective.len(), n);
assert_eq!(t.inf_pr.len(), n);
assert_eq!(t.alpha_primal_char.len(), n);
}
#[test]
fn diagnose_success_includes_converged() {
let r = SolveReport::from_json_str(ROSENBROCK).unwrap();
let findings = diagnose(&r);
assert!(
findings.iter().any(|f| f.code == "converged"),
"missing 'converged' in {findings:?}",
);
assert!(
!findings.iter().any(|f| f.code == "convergence_stall"),
"stall should be suppressed on clean convergence: {findings:?}",
);
}
#[test]
fn diagnose_stalled_includes_max_iter_error() {
let r = SolveReport::from_json_str(STALLED).unwrap();
let findings = diagnose(&r);
assert!(findings
.iter()
.any(|f| { f.code == "max_iter_exceeded" && matches!(f.severity, Severity::Error) }));
}
#[test]
fn get_iterate_returns_first_row_with_derived_log10() {
let r = SolveReport::from_json_str(ROSENBROCK).unwrap();
let iter0 = get_iterate(&r, 0).unwrap();
assert_eq!(iter0.raw.iter, 0);
assert!(iter0.log10_mu.is_some());
}
#[test]
fn restoration_windows_empty_on_clean_run() {
let r = SolveReport::from_json_str(ROSENBROCK).unwrap();
assert!(restoration_windows(&r).is_empty());
}
#[test]
fn find_stalls_runs_without_panic() {
let r = SolveReport::from_json_str(ROSENBROCK).unwrap();
let _ = find_stalls(&r); }
#[test]
fn compare_runs_aligns_two_reports() {
let a = SolveReport::from_json_str(ROSENBROCK).unwrap();
let b = SolveReport::from_json_str(STALLED).unwrap();
let rows = compare_runs([("ok", &a), ("stalled", &b)]);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].label, "ok");
assert_eq!(rows[1].label, "stalled");
assert_eq!(rows[0].status, "SolveSucceeded");
assert_eq!(rows[1].status, "MaximumIterationsExceeded");
}
#[test]
fn iter_dump_parses_real_solver_trace() {
let trace = IterDumpTrace::from_bytes(EQ_TRACE).expect("parse");
assert_eq!(trace.header.format_version, 1);
assert_eq!(trace.header.name, "eq-quadratic");
assert!(!trace.records.is_empty());
assert_eq!(trace.records[0].x.len() as u32, trace.header.n);
}
#[test]
fn markdown_renders_full_report() {
let r = SolveReport::from_json_str(ROSENBROCK).unwrap();
let md = render_inspect(&r);
assert!(md.contains("# Pounce solve report"));
assert!(md.contains("## Findings"));
assert!(md.contains("## Convergence trajectory"));
assert!(md.contains("converged"));
}