use super::*;
struct EmptyFinalSummary;
impl Respond for EmptyFinalSummary {
fn respond(&self, req: &Request) -> ResponseTemplate {
if body_json(req).get("tools").is_some() {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": {"content": "", "tool_calls": [{
"function": {"name": "definitely_not_a_real_tool", "arguments": {}}
}]}
}))
} else {
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"message": {"content": ""}}))
}
}
}
#[tokio::test]
async fn empty_final_summary_yields_cap_fallback() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(EmptyFinalSummary)
.mount(&server)
.await;
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let mut c = ctx(&uri, &messages, &caveats);
c.max_tool_rounds = 2;
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("chat_complete should succeed");
assert!(reply.contains("tool-round limit (2"), "got: {reply}");
assert!(
reply.contains("increase the tool-round limit"),
"gives caller-neutral recovery advice"
);
}
struct HallucinatingFinalSummary;
impl Respond for HallucinatingFinalSummary {
fn respond(&self, req: &Request) -> ResponseTemplate {
if body_json(req).get("tools").is_some() {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": {"content": "", "tool_calls": [{
"function": {"name": "definitely_not_a_real_tool", "arguments": {}}
}]}
}))
} else {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": {"content":
"The /end command is defined in newt-tui/src/commands.rs \
(lines 38-40) as enum variants."}
}))
}
}
}
#[tokio::test]
async fn cap_exit_hallucinated_path_gets_claim_check_refutation() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(HallucinatingFinalSummary)
.mount(&server)
.await;
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let mut c = ctx(&uri, &messages, &caveats);
c.max_tool_rounds = 2;
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("chat_complete should succeed");
assert!(
reply.contains("newt-tui/src/commands.rs (lines 38-40)"),
"the model's prose is preserved verbatim: {reply}"
);
assert!(reply.starts_with("## Observed"), "{reply}");
assert!(reply.contains("⚠ claim check (#867)"), "got: {reply}");
assert!(
reply.contains("`newt-tui/src/commands.rs`"),
"the fabricated path is named in the refutation: {reply}"
);
}
struct HallucinatingFirstAnswer;
impl Respond for HallucinatingFirstAnswer {
fn respond(&self, req: &Request) -> ResponseTemplate {
let content = "The /end command is defined in newt-tui/src/commands.rs \
(lines 38-40) as enum variants.";
if is_stream(req) {
ndjson(&[serde_json::json!({
"message": {"content": content}, "done": true,
"prompt_eval_count": 5, "eval_count": 2
})])
} else {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": {"content": content},
"prompt_eval_count": 5, "eval_count": 2,
}))
}
}
}
#[tokio::test]
async fn normal_finish_hallucinated_path_gets_claim_check_refutation() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(HallucinatingFirstAnswer)
.mount(&server)
.await;
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let c = ctx(&uri, &messages, &caveats);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("chat_complete should succeed");
assert!(
reply.contains("newt-tui/src/commands.rs (lines 38-40)"),
"the model's prose is preserved verbatim: {reply}"
);
assert!(
reply.contains("⚠ claim check (#867)"),
"a normal finish must get the same claim check as a cap-exit summary: {reply}"
);
assert!(
reply.contains("`newt-tui/src/commands.rs`"),
"the fabricated path is named in the refutation: {reply}"
);
}
struct CleanFirstAnswer;
impl Respond for CleanFirstAnswer {
fn respond(&self, req: &Request) -> ResponseTemplate {
let content = "The task is already complete; nothing further to do.";
if is_stream(req) {
ndjson(&[serde_json::json!({
"message": {"content": content}, "done": true,
"prompt_eval_count": 5, "eval_count": 2
})])
} else {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"message": {"content": content},
"prompt_eval_count": 5, "eval_count": 2,
}))
}
}
}
#[tokio::test]
async fn clean_normal_finish_gets_no_annotation() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/chat"))
.respond_with(CleanFirstAnswer)
.mount(&server)
.await;
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let c = ctx(&uri, &messages, &caveats);
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("chat_complete should succeed");
assert_eq!(
crate::agentic::model_explanation(&reply),
"The task is already complete; nothing further to do.",
"a clean normal finish must pass through byte-for-byte: {reply}"
);
assert!(!reply.contains("⚠ claim check"), "got: {reply}");
}
struct OpenAiErrOnFinal;
impl Respond for OpenAiErrOnFinal {
fn respond(&self, req: &Request) -> ResponseTemplate {
if body_json(req).get("tools").is_some() {
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"choices": [{"message": {
"content": null,
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {"name": "definitely_not_a_real_tool", "arguments": "{}"}
}]
}}]
}))
} else {
ResponseTemplate::new(400).set_body_string("bad request")
}
}
}
#[tokio::test]
async fn openai_cap_exit_fallback_when_final_summary_errors() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/chat/completions"))
.respond_with(OpenAiErrOnFinal)
.mount(&server)
.await;
let messages = msgs();
let caveats = Caveats::top();
let uri = server.uri();
let mut c = ctx(&uri, &messages, &caveats);
c.kind = BackendKind::Openai;
c.max_tool_rounds = 2;
let (reply, _, _, _) = chat_complete(c, &mut NoMcp)
.await
.expect("must succeed even when the summary errors");
assert!(reply.contains("tool-round limit (2"), "got: {reply}");
assert!(reply.contains("increase the tool-round limit"));
}