use normcore::AdmissibilityStatus;
use normcore::ConversationMessage;
use normcore::CreatorType;
use normcore::EvaluateInput;
use normcore::EvidenceType;
use normcore::Ground;
use normcore::JsonValue;
use normcore::LinkRole;
use normcore::ToolCall;
use normcore::evaluate;
fn assistant_text(content: &str) -> ConversationMessage {
ConversationMessage {
role: "assistant".to_string(),
content: Some(JsonValue::String(content.to_string())),
tool_call_id: None,
tool_calls: Vec::new(),
function_name: None,
}
}
#[test]
fn scenario_conversation_with_tool_citation_produces_grounded_acceptable_status() {
let conversation = vec![
ConversationMessage {
role: "assistant".to_string(),
content: Some(JsonValue::String(String::new())),
tool_call_id: None,
tool_calls: vec![ToolCall {
id: "callWeatherNYC".to_string(),
kind: "function".to_string(),
function_name: Some("get_weather".to_string()),
function_arguments: Some(JsonValue::String("{\"city\":\"New York\"}".to_string())),
custom_name: None,
custom_input: None,
}],
function_name: None,
},
ConversationMessage {
role: "tool".to_string(),
content: Some(JsonValue::String(
"{\"weather_id\":\"nyc_2026-02-07\"}".to_string(),
)),
tool_call_id: Some("callWeatherNYC".to_string()),
tool_calls: Vec::new(),
function_name: None,
},
assistant_text("You should carry an umbrella [@callWeatherNYC]."),
];
let judgment = evaluate(EvaluateInput {
agent_output: None,
conversation: Some(conversation),
grounds: None,
})
.expect("evaluation should succeed");
assert_eq!(judgment.status, AdmissibilityStatus::Acceptable);
assert!(judgment.licensed);
assert!(!judgment.can_retry);
assert_eq!(judgment.grounds_accepted, 1);
assert_eq!(judgment.grounds_cited, 1);
assert_eq!(judgment.num_statements, 1);
assert_eq!(judgment.statement_evaluations.len(), 1);
assert!(!judgment.statement_evaluations[0].grounding_trace.is_empty());
}
#[test]
fn scenario_external_ground_without_tool_history_keeps_assertive_claim_acceptable() {
let grounds = vec![Ground {
citation_key: "file_weather_2025".to_string(),
ground_id: "file_weather_2025".to_string(),
role: LinkRole::Supports,
creator: CreatorType::UpstreamPipeline,
evidence_type: EvidenceType::Observation,
evidence_content: Some("openai_citation".to_string()),
signature: None,
}];
let judgment = evaluate(EvaluateInput {
agent_output: Some("You should compare with archive [@file_weather_2025].".to_string()),
conversation: None,
grounds: Some(grounds),
})
.expect("evaluation should succeed");
assert_eq!(judgment.status, AdmissibilityStatus::Acceptable);
assert_eq!(judgment.grounds_accepted, 1);
assert_eq!(judgment.grounds_cited, 1);
assert_eq!(judgment.violated_axioms, Vec::<String>::new());
}
#[test]
fn scenario_tool_plus_external_ground_includes_both_in_grounding_trace() {
let grounds = vec![Ground {
citation_key: "file_weather_archive_2025".to_string(),
ground_id: "file_weather_archive_2025".to_string(),
role: LinkRole::Supports,
creator: CreatorType::UpstreamPipeline,
evidence_type: EvidenceType::Observation,
evidence_content: Some("openai_citation".to_string()),
signature: None,
}];
let conversation = vec![
ConversationMessage {
role: "system".to_string(),
content: Some(JsonValue::String(
"RAG_CONTEXT file_weather_archive_2025: On 2025-02-07 in New York, condition=snow, temperature_c=-1.".to_string(),
)),
tool_call_id: None,
tool_calls: Vec::new(),
function_name: None,
},
ConversationMessage {
role: "user".to_string(),
content: Some(JsonValue::String(
"What is the weather in New York today, and how does it compare to last year?"
.to_string(),
)),
tool_call_id: None,
tool_calls: Vec::new(),
function_name: None,
},
ConversationMessage {
role: "assistant".to_string(),
content: Some(JsonValue::String(String::new())),
tool_call_id: None,
tool_calls: vec![ToolCall {
id: "callWeatherNYC".to_string(),
kind: "function".to_string(),
function_name: Some("get_weather".to_string()),
function_arguments: Some(JsonValue::String(
"{\"city\":\"New York\",\"units\":\"metric\"}".to_string(),
)),
custom_name: None,
custom_input: None,
}],
function_name: None,
},
ConversationMessage {
role: "tool".to_string(),
content: Some(JsonValue::String(
"{\"weather_id\":\"nyc_2026-02-07\",\"city\":\"New York\",\"condition\":\"rain\",\"temperature_c\":4}".to_string(),
)),
tool_call_id: Some("callWeatherNYC".to_string()),
tool_calls: Vec::new(),
function_name: None,
},
assistant_text(
"It is warmer and rainy in New York today compared to one year ago, so you should carry an umbrella [@callWeatherNYC] [@file_weather_archive_2025].",
),
];
let judgment = evaluate(EvaluateInput {
agent_output: None,
conversation: Some(conversation),
grounds: Some(grounds),
})
.expect("evaluation should succeed");
assert_eq!(judgment.status, AdmissibilityStatus::Acceptable);
assert_eq!(judgment.statement_evaluations.len(), 1);
let trace = &judgment.statement_evaluations[0].grounding_trace;
assert!(trace.len() >= 2);
assert!(
trace
.iter()
.any(|entry| entry.semantic_id.as_deref() == Some("file_weather_archive_2025"))
);
}
#[test]
fn scenario_assertive_without_grounding_returns_norm_violation() {
let judgment = evaluate(EvaluateInput {
agent_output: Some("We should deploy now.".to_string()),
conversation: None,
grounds: None,
})
.expect("evaluation should succeed");
assert_eq!(judgment.status, AdmissibilityStatus::ViolatesNorm);
assert!(!judgment.licensed);
assert!(judgment.can_retry);
assert_eq!(judgment.violated_axioms, vec!["A5".to_string()]);
assert_eq!(judgment.grounds_accepted, 0);
assert_eq!(judgment.grounds_cited, 0);
}