use std::path::Path;
use std::process::Stdio;
use std::time::Duration;
use kodact_test_host::{kodact_bin, MockGemini};
use serde_json::{json, Value};
async fn kodact(cwd: &Path, home: &Path, api: &MockGemini, args: &[&str]) -> (i32, String, String) {
let mut c = tokio::process::Command::new(kodact_bin());
c.args(args)
.current_dir(cwd)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", home)
.env("KODACT_HOME", home.join(".kodact"))
.env("KODACT_GEMINI_BASE_URL", &api.url)
.env("KODACT_GEMINI_API_KEY", "test-key")
.env("KODACT_MAX_RETRIES", "0")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let out = tokio::time::timeout(Duration::from_secs(60), c.output()).await.unwrap().unwrap();
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into(),
String::from_utf8_lossy(&out.stderr).into(),
)
}
fn read(path: &Path) -> Value {
json!({"name": "Read", "args": {"file_path": path.to_string_lossy()}})
}
#[tokio::test]
async fn a_gemini_3_tool_loop_keeps_its_signatures_through_steps_and_sessions() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
let (cwd, home) = (root.join("project"), root.join("home"));
std::fs::create_dir_all(&cwd).unwrap();
std::fs::create_dir_all(&home).unwrap();
for (f, text) in [("a.txt", "alpha"), ("b.txt", "beta"), ("c.txt", "gamma")] {
std::fs::write(cwd.join(f), text).unwrap();
}
let api = MockGemini::start(vec![
vec![
json!({"text": "Reading both.", "thought": true, "thoughtSignature": "THINK-1"}),
json!({"functionCall": read(&cwd.join("a.txt")), "thoughtSignature": "SIG-A"}),
json!({"functionCall": read(&cwd.join("b.txt"))}),
],
vec![json!({"functionCall": read(&cwd.join("c.txt")), "thoughtSignature": "SIG-C"})],
vec![json!({"text": "They say alpha, beta and gamma."})],
vec![json!({"text": "Still alpha, beta and gamma."})],
])
.await;
let args = [
"-p",
"What do a.txt, b.txt and c.txt say?",
"--model",
"gemini-3-flash-preview",
"--dangerously-skip-permissions",
];
let (code, out, err) = kodact(&cwd, &home, &api, &args).await;
assert_eq!(code, 0, "stdout: {out}\nstderr: {err}\nviolations: {:?}", api.violations());
assert!(out.contains("alpha, beta and gamma"), "{out}");
let again = ["-p", "--continue", "And now?", "--model", "gemini-3-flash-preview"];
let (code, out, err) = kodact(&cwd, &home, &api, &again).await;
assert_eq!(code, 0, "stdout: {out}\nstderr: {err}\nviolations: {:?}", api.violations());
assert!(out.contains("Still alpha"), "{out}");
assert!(api.violations().is_empty(), "{:?}", api.violations());
let last = api.requests().last().unwrap().to_string();
for sig in ["THINK-1", "SIG-A", "SIG-C"] {
assert!(last.contains(sig), "{sig} was lost on resume");
}
}
#[tokio::test]
async fn a_lost_signature_is_what_the_strict_endpoint_refuses() {
let issued = vec![("Read{\"file_path\":\"a\"}".to_string(), "SIG".to_string())];
let body = json!({"contents": [
{"role": "user", "parts": [{"text": "go"}]},
{"role": "model", "parts": [{"functionCall": {"name": "Read", "args": {"file_path": "a"}}}]},
{"role": "user", "parts": [{"functionResponse": {"name": "Read", "response": {"output": "x"}}}]}
]});
let why = kodact_test_host::gemini_violation("/v1beta/models/gemini-3-flash:streamGenerateContent", &body, &issued);
assert!(why.is_some_and(|w| w.contains("thought_signature")));
}