use super::{check_source, check_source_with_source};
use crate::diagnostic_codes::{Code, RepairSafety};
fn first_with_code(source: &str, code: Code) -> crate::TypeDiagnostic {
let diags = check_source(source);
diags
.into_iter()
.find(|d| d.code == code)
.unwrap_or_else(|| panic!("no diagnostic with code {code} emitted by source:\n{source}"))
}
fn first_with_code_with_source(source: &str, code: Code) -> crate::TypeDiagnostic {
let diags = check_source_with_source(source);
diags
.into_iter()
.find(|d| d.code == code)
.unwrap_or_else(|| panic!("no diagnostic with code {code} emitted by source:\n{source}"))
}
#[test]
fn type_mismatch_attaches_scope_local_repair() {
let diag = first_with_code(
r#"
pipeline main() {
let x: int = "not an int"
}
"#,
Code::VariableTypeMismatch,
);
let repair = diag
.repair
.expect("VariableTypeMismatch should carry a repair");
assert_eq!(repair.id.as_str(), "casts/insert-explicit-conversion");
assert_eq!(repair.safety, RepairSafety::ScopeLocal);
}
#[test]
fn non_exhaustive_match_attaches_scope_local_repair() {
let diag = first_with_code(
r#"
type Color = "red" | "green" | "blue"
pipeline main() {
let c: Color = "red"
match c {
"red" -> { 1 }
"green" -> { 2 }
}
}
"#,
Code::NonExhaustiveMatch,
);
let repair = diag
.repair
.expect("NonExhaustiveMatch should carry a repair");
assert_eq!(repair.id.as_str(), "match/add-missing-arms");
assert_eq!(repair.safety, RepairSafety::ScopeLocal);
}
#[test]
fn immutable_assignment_attaches_scope_local_repair() {
let diag = first_with_code(
r#"
pipeline main() {
let x = 1
x = 2
}
"#,
Code::ImmutableAssignment,
);
let repair = diag
.repair
.expect("ImmutableAssignment should carry a repair");
assert_eq!(repair.id.as_str(), "bindings/make-mutable");
assert_eq!(repair.safety, RepairSafety::ScopeLocal);
}
#[test]
fn string_interpolation_rewrite_attaches_behavior_preserving_repair() {
let diag = first_with_code_with_source(
"pipeline main() { let count = 1; let greeting = \"hello \" + count; greeting }",
Code::StringInterpolationRewrite,
);
let repair = diag
.repair
.expect("StringInterpolationRewrite should carry a repair");
assert_eq!(repair.id.as_str(), "style/string-interpolation");
assert_eq!(repair.safety, RepairSafety::BehaviorPreserving);
}
#[test]
fn try_outside_function_attaches_surface_changing_repair() {
let diag = first_with_code(
r#"
try* maybe_fail()
"#,
Code::TryOutsideFunction,
);
let repair = diag
.repair
.expect("TryOutsideFunction should carry a repair");
assert_eq!(repair.id.as_str(), "errors/wrap-in-fn");
assert_eq!(repair.safety, RepairSafety::SurfaceChanging);
}
#[test]
fn diagnostics_without_registered_template_have_no_repair() {
let diags = check_source(
r#"
pipeline main() {
let r = llm_call("you", "instructions", {
"schema": 42,
})
r
}
"#,
);
if let Some(diag) = diags.iter().find(|d| d.code == Code::LlmSchemaInvalid) {
assert!(
diag.repair.is_none(),
"LlmSchemaInvalid should not carry a repair until one is registered, got {:?}",
diag.repair
);
}
}
#[test]
fn every_emitted_repair_matches_registry_safety() {
let sources = [
"pipeline main() { let x: int = \"nope\" }",
"pipeline main() { let x = 1; x = 2 }",
"type Color = \"red\" | \"green\"\npipeline main() { let c: Color = \"red\"; match c { \"red\" -> { 1 } } }",
];
for source in sources {
for diag in check_source(source) {
let Some(repair) = diag.repair.as_ref() else {
continue;
};
let template = diag
.code
.repair_template()
.unwrap_or_else(|| panic!("{} carried repair but registry has none", diag.code));
assert_eq!(
repair.id.as_str(),
template.id,
"{} repair id drift",
diag.code
);
assert_eq!(repair.safety, template.safety, "{} safety drift", diag.code);
}
}
}