use super::diagnostic_log::{DiagnosticLog, unreported};
use super::validate::{Severity, ValidationError};
use crate::core::config::e2e::{ArgMapping, CallConfig, E2eConfig};
use crate::core::ir::{FunctionDef, ParamDef, TypeDef};
use crate::e2e::codegen::call_ir::{CallIr, binding_excluded_for_language, resolves_only_via_adapter_handled_method};
use crate::e2e::fixture::Fixture;
pub fn enforce_call_arg_signatures(
fixtures: &[Fixture],
e2e_config: &E2eConfig,
functions: &[FunctionDef],
type_defs: &[TypeDef],
languages: &[String],
log: &DiagnosticLog,
) -> anyhow::Result<()> {
let diagnostics = validate_call_arg_signatures(fixtures, e2e_config, functions, type_defs, languages);
for diag in unreported(&diagnostics, log) {
tracing::warn!("{}: {}", diag.file, diag.message);
}
let errors: Vec<_> = diagnostics
.iter()
.filter(|diag| diag.severity == Severity::Error)
.collect();
if errors.is_empty() {
return Ok(());
}
anyhow::bail!(
"e2e call argument signature validation failed: {}",
errors
.iter()
.map(|diag| format!("{}: {}", diag.file, diag.message))
.collect::<Vec<_>>()
.join("; ")
);
}
pub fn validate_call_arg_signatures(
fixtures: &[Fixture],
e2e_config: &E2eConfig,
functions: &[FunctionDef],
type_defs: &[TypeDef],
languages: &[String],
) -> Vec<ValidationError> {
if functions.is_empty() && type_defs.is_empty() {
return Vec::new();
}
let ir = CallIr { functions, type_defs };
let mut errors = Vec::new();
for fixture in fixtures {
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let Some(lookup_name) = canonical_lookup_name(call_config, languages) else {
continue;
};
let Some(signature) = ir.signature(&lookup_name) else {
continue;
};
if resolves_only_via_adapter_handled_method(&lookup_name, ir) {
continue;
}
if languages
.iter()
.all(|language| binding_excluded_for_language(&lookup_name, language, ir))
{
continue;
}
let args = fixture.resolved_args(call_config);
check_unknown_args(fixture, &lookup_name, args, signature.params, &mut errors);
check_missing_required_params(fixture, &lookup_name, args, signature.params, &mut errors);
}
errors
}
fn canonical_lookup_name<'a>(call: &'a CallConfig, languages: &[String]) -> Option<std::borrow::Cow<'a, str>> {
languages.iter().find_map(|language| call.core_lookup_name(language))
}
fn check_unknown_args(
fixture: &Fixture,
lookup_name: &str,
args: &[ArgMapping],
params: &[ParamDef],
errors: &mut Vec<ValidationError>,
) {
for arg in args {
if params.iter().any(|param| param.name == arg.name) {
continue;
}
errors.push(ValidationError {
file: fixture.source.clone(),
message: format!(
"fixture '{}' arg '{}' names a parameter '{}' does not declare (call resolves to Rust \
function/method '{lookup_name}')",
fixture.id, arg.name, lookup_name
),
severity: Severity::Error,
});
}
}
fn check_missing_required_params(
fixture: &Fixture,
lookup_name: &str,
args: &[ArgMapping],
params: &[ParamDef],
errors: &mut Vec<ValidationError>,
) {
for param in params {
if param.optional || param.default.is_some() {
continue;
}
if args.iter().any(|arg| arg.name == param.name) {
continue;
}
errors.push(ValidationError {
file: fixture.source.clone(),
message: format!(
"fixture '{}' does not supply required parameter '{}' of '{lookup_name}' (no arg, and the \
parameter has no default)",
fixture.id, param.name
),
severity: Severity::Error,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{MethodDef, TypeRef};
fn param(name: &str) -> ParamDef {
ParamDef {
name: name.to_string(),
ty: TypeRef::String,
..ParamDef::default()
}
}
fn optional_param(name: &str) -> ParamDef {
ParamDef {
optional: true,
..param(name)
}
}
fn function(name: &str, params: Vec<ParamDef>) -> FunctionDef {
FunctionDef {
name: name.to_string(),
params,
return_type: TypeRef::String,
..FunctionDef::default()
}
}
fn arg(name: &str, optional: bool) -> ArgMapping {
ArgMapping {
name: name.to_string(),
field: format!("input.{name}"),
arg_type: "string".to_string(),
optional,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
fn call_named(function: &str, args: Vec<ArgMapping>) -> CallConfig {
CallConfig {
function: function.to_string(),
args,
..CallConfig::default()
}
}
fn fixture_with_call(id: &str, call: Option<&str>) -> Fixture {
Fixture {
id: id.to_string(),
call: call.map(str::to_string),
source: format!("{id}.json"),
..Fixture::default()
}
}
#[test]
fn matching_args_pass() {
let functions = vec![function("complete", vec![param("prompt"), optional_param("model")])];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false), arg("model", true)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn an_arg_naming_a_removed_parameter_is_flagged() {
let functions = vec![function("complete", vec![param("prompt")])];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false), arg("concurrency", true)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert_eq!(errors[0].severity, Severity::Error);
assert!(
errors[0].message.contains("fixture 'basic' arg 'concurrency'"),
"got: {}",
errors[0].message
);
assert!(errors[0].message.contains("'complete'"), "got: {}", errors[0].message);
}
#[test]
fn a_missing_required_parameter_is_flagged() {
let functions = vec![function("complete", vec![param("prompt"), param("config")])];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(
errors[0]
.message
.contains("fixture 'basic' does not supply required parameter 'config'"),
"got: {}",
errors[0].message
);
}
#[test]
fn a_missing_optional_parameter_is_not_flagged() {
let functions = vec![function("complete", vec![param("prompt"), optional_param("model")])];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn a_missing_parameter_with_a_declared_default_is_not_flagged() {
let functions = vec![function(
"complete",
vec![
param("prompt"),
ParamDef {
default: Some("Config::default()".to_string()),
..param("config")
},
],
)];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn an_unresolvable_call_licenses_no_claim() {
let functions = vec![function("complete", vec![param("prompt")])];
let e2e_config = E2eConfig {
call: call_named("mystery", vec![arg("wrong_name", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(
errors.len(),
0,
"an unresolved call must not be claimed wrong: {errors:?}"
);
}
#[test]
fn an_empty_ir_skips_validation_entirely() {
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("wrong_name", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &[], &[], &["rust".to_string()]);
assert_eq!(errors.len(), 0, "an absent IR must license no claim: {errors:?}");
}
#[test]
fn a_binding_excluded_function_is_still_validated_when_rust_is_resolved() {
let functions = vec![FunctionDef {
binding_excluded: true,
..function("complete", vec![param("prompt")])
}];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false), arg("wrong_name", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(
errors[0].message.contains("fixture 'basic' arg 'wrong_name'"),
"got: {}",
errors[0].message
);
}
#[test]
fn a_binding_excluded_function_licenses_no_claim_when_no_resolved_language_renders_it() {
let functions = vec![FunctionDef {
binding_excluded: true,
..function("complete", vec![param("prompt")])
}];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("wrong_name", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["python".to_string()]);
assert_eq!(
errors.len(),
0,
"no resolved language renders an excluded call, so it must not be claimed wrong: {errors:?}"
);
}
#[test]
fn a_fixture_level_args_override_replaces_the_call_args_for_validation() {
let functions = vec![function("complete", vec![param("prompt"), param("visitor")])];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false)]),
..E2eConfig::default()
};
let mut fixture = fixture_with_call("with_visitor", None);
fixture.args = vec![arg("prompt", false), arg("visitor", false)];
let fixtures = vec![fixture];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(errors.len(), 0, "fixture-level args must replace call args: {errors:?}");
}
#[test]
fn a_method_declared_on_an_ir_type_resolves_too() {
let type_defs = vec![TypeDef {
name: "Client".to_string(),
methods: vec![MethodDef {
name: "chat".to_string(),
params: vec![param("request")],
return_type: TypeRef::String,
..MethodDef::default()
}],
..TypeDef::default()
}];
let e2e_config = E2eConfig {
call: call_named("chat", vec![arg("wrong_name", false), arg("request", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &[], &type_defs, &["rust".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(errors[0].message.contains("'chat'"), "got: {}", errors[0].message);
}
fn adapter_handled_method(name: &str, params: Vec<ParamDef>) -> MethodDef {
MethodDef {
name: name.to_string(),
params,
return_type: TypeRef::String,
binding_excluded: true,
binding_exclusion_reason: Some(format!(
"{} entry `{name}`",
crate::core::ir::ADAPTER_HANDLED_REASON_PREFIX
)),
..MethodDef::default()
}
}
#[test]
fn an_adapter_handled_method_with_no_visible_free_function_licenses_no_claim() {
let type_defs = vec![TypeDef {
name: "Handle".to_string(),
methods: vec![adapter_handled_method("stream", vec![param("req")])],
..TypeDef::default()
}];
let e2e_config = E2eConfig {
call: call_named("stream", vec![arg("handle", false), arg("url", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(
&fixtures,
&e2e_config,
&[],
&type_defs,
&["python".to_string(), "rust".to_string()],
);
assert_eq!(
errors.len(),
0,
"an adapter-handled method's own signature must not be asserted against a call whose \
free-function sibling is invisible here: {errors:?}"
);
}
#[test]
fn a_visible_free_function_still_validates_even_with_an_adapter_handled_namesake_method() {
let functions = vec![function("stream", vec![param("handle"), param("url")])];
let type_defs = vec![TypeDef {
name: "Handle".to_string(),
methods: vec![adapter_handled_method("stream", vec![param("req")])],
..TypeDef::default()
}];
let e2e_config = E2eConfig {
call: call_named(
"stream",
vec![arg("handle", false), arg("url", false), arg("wrong_name", false)],
),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors =
validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &type_defs, &["python".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(
errors[0].message.contains("arg 'wrong_name'"),
"got: {}",
errors[0].message
);
}
#[test]
fn a_plain_binding_excluded_method_without_an_adapter_reason_still_validates_for_rust() {
let type_defs = vec![TypeDef {
name: "Handle".to_string(),
methods: vec![MethodDef {
name: "stream".to_string(),
params: vec![param("req")],
return_type: TypeRef::String,
binding_excluded: true,
binding_exclusion_reason: Some("source binding exclusion".to_string()),
..MethodDef::default()
}],
..TypeDef::default()
}];
let e2e_config = E2eConfig {
call: call_named("stream", vec![arg("req", false), arg("wrong_name", false)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &[], &type_defs, &["rust".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(
errors[0].message.contains("arg 'wrong_name'"),
"got: {}",
errors[0].message
);
}
#[test]
fn a_named_call_resolves_through_the_fixtures_call_field() {
let functions = vec![function("embed", vec![param("text")])];
let mut e2e_config = E2eConfig::default();
e2e_config.calls.insert(
"embed".to_string(),
call_named("embed", vec![arg("wrong_name", false), arg("text", false)]),
);
let fixtures = vec![fixture_with_call("embed_basic", Some("embed"))];
let errors = validate_call_arg_signatures(&fixtures, &e2e_config, &functions, &[], &["rust".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(
errors[0].message.contains("fixture 'embed_basic'"),
"got: {}",
errors[0].message
);
}
#[test]
fn enforce_bails_when_an_arg_names_a_removed_parameter() {
let functions = vec![function("complete", vec![param("prompt")])];
let e2e_config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false), arg("concurrency", true)]),
..E2eConfig::default()
};
let fixtures = vec![fixture_with_call("basic", None)];
let result = enforce_call_arg_signatures(
&fixtures,
&e2e_config,
&functions,
&[],
&["rust".to_string()],
&DiagnosticLog::new(),
);
let err = result.expect_err("a removed-parameter arg must abort generation");
assert!(
err.to_string().contains("fixture 'basic' arg 'concurrency'"),
"got: {err}"
);
}
#[test]
fn enforce_does_not_bail_on_the_legitimate_patterns_the_fleet_survey_found() {
let functions = vec![
function("complete", vec![param("prompt"), optional_param("model")]),
FunctionDef {
binding_excluded: true,
..function("chat", vec![param("request")])
},
];
let e2e_config = {
let mut config = E2eConfig {
call: call_named("complete", vec![arg("prompt", false)]),
..E2eConfig::default()
};
config
.calls
.insert("chat".to_string(), call_named("chat", vec![arg("request", false)]));
config
};
let fixtures = vec![
fixture_with_call("basic", None),
fixture_with_call("chat_basic", Some("chat")),
];
let result = enforce_call_arg_signatures(
&fixtures,
&e2e_config,
&functions,
&[],
&["python".to_string()],
&DiagnosticLog::new(),
);
assert!(result.is_ok(), "expected Ok(()), got: {result:?}");
}
}