use super::*;
use metrale_grammar::{CompiledGrammar, GrammarMatcher};
fn read_file_tool() -> Vec<ToolDefinition> {
vec![ToolDefinition {
tool_type: "function".to_string(),
function: crate::tool_parser::FunctionDefinition {
name: "read_file".to_string(),
description: Some("Read a file from disk".to_string()),
parameters: Some(serde_json::json!({
"type": "object",
"properties": { "path": {"type": "string"} },
"required": ["path"]
})),
},
}]
}
fn grammar_accepts(compiled: &CompiledGrammar, input: &str) -> bool {
let mut matcher =
GrammarMatcher::new(compiled, None, true, -1).expect("GrammarMatcher::new failed");
if !matcher.accept_string(input, false) {
return false;
}
matcher.is_terminated()
}
const CANONICAL: &str = "<|tool_call>call:read_file{\"path\": \"./Cargo.toml\"}<tool_call|>";
#[test]
fn gemma4_required_accepts_canonical_call() {
let vocab = test_vocab();
let stop_ids = vec![130i32];
let mut engine = GrammarEngine::new(&vocab, &stop_ids).unwrap();
let compiled = engine
.compile_gemma4_tool_grammar(&read_file_tool(), false)
.expect("required-mode gemma4 grammar must compile");
assert!(
grammar_accepts(&compiled, CANONICAL),
"required-mode grammar must accept the canonical call; input: {CANONICAL:?}"
);
}
#[test]
fn gemma4_auto_accepts_canonical_call() {
let vocab = test_vocab();
let stop_ids = vec![130i32];
let mut engine = GrammarEngine::new(&vocab, &stop_ids).unwrap();
let compiled = engine
.compile_gemma4_tool_grammar(&read_file_tool(), true)
.expect("auto-mode gemma4 grammar must compile");
assert!(
grammar_accepts(&compiled, CANONICAL),
"auto-mode grammar must accept the canonical call; input: {CANONICAL:?}"
);
}
#[test]
fn gemma4_required_rejects_leading_prose() {
let vocab = test_vocab();
let stop_ids = vec![130i32];
let mut engine = GrammarEngine::new(&vocab, &stop_ids).unwrap();
let compiled = engine
.compile_gemma4_tool_grammar(&read_file_tool(), false)
.expect("required-mode gemma4 grammar must compile");
assert!(
!grammar_accepts(&compiled, "Sure, let me read that file for you."),
"required mode must REJECT leading prose (constrained from token 1)"
);
}
#[test]
fn gemma4_auto_allows_free_prose() {
let vocab = test_vocab();
let stop_ids = vec![130i32];
let mut engine = GrammarEngine::new(&vocab, &stop_ids).unwrap();
let compiled = engine
.compile_gemma4_tool_grammar(&read_file_tool(), true)
.expect("auto-mode gemma4 grammar must compile");
assert!(
grammar_accepts(&compiled, "Just a plain text answer, no tool needed."),
"auto mode must ALLOW free prose (no forced tool call)"
);
}