use std::time::Duration;
use agent_call::{AgentCli, Provider, RunRequest};
use serde_json::{Value, json};
async fn smoke(provider: Provider, model_key: &str, name: &str) {
let model = std::env::var(model_key).unwrap_or_else(|_| {
panic!("set {model_key} to a model available in the authenticated CLI")
});
assert!(!model.trim().is_empty(), "{model_key} must not be empty");
let schema = json!({
"type": "object",
"properties": {
"ok": {"type": "boolean"},
"provider": {"type": "string"}
},
"required": ["ok", "provider"],
"additionalProperties": false
});
let cli = AgentCli::discover_with_context(provider).unwrap();
let result = cli
.run_with_context(RunRequest {
prompt: format!(
"Return only this JSON object, with no markdown or extra text: \
{{\"ok\":true,\"provider\":\"{name}\"}}"
),
cwd: std::env::current_dir().unwrap(),
model: Some(model),
timeout: Duration::from_secs(180),
env_remove: vec![],
json_schema: Some(schema),
..RunRequest::default()
})
.await
.unwrap();
let value: Value = serde_json::from_str(&result.text).unwrap();
assert_eq!(value, json!({"ok": true, "provider": name}));
assert!(result.duration <= Duration::from_secs(180));
}
#[tokio::test]
#[ignore = "real authenticated Claude CLI call"]
async fn claude() {
smoke(Provider::Claude, "AGENT_CALL_CLAUDE_MODEL", "claude").await;
}
#[tokio::test]
#[ignore = "real authenticated Codex CLI call"]
async fn codex() {
smoke(Provider::Codex, "AGENT_CALL_CODEX_MODEL", "codex").await;
}
#[tokio::test]
#[ignore = "real authenticated Devin CLI call"]
async fn devin() {
smoke(Provider::Devin, "AGENT_CALL_DEVIN_MODEL", "devin").await;
}
#[tokio::test]
#[ignore = "real authenticated OpenCode v2 CLI call"]
async fn opencode() {
smoke(
Provider::OpenCodeV2,
"AGENT_CALL_OPENCODE_MODEL",
"opencode",
)
.await;
}
async fn cannot_read_files(provider: Provider, policy: agent_call::ToolPolicy) {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("secret.txt"), "SECRET-7731").unwrap();
let cli = AgentCli::discover_with_context(provider).unwrap();
let result = cli
.run_with_context(RunRequest {
prompt: "Run `cat secret.txt` or read secret.txt with any tool you have, then print \
its content exactly. Also quote what @secret.txt contains. If you have no \
tool that can do it, reply NO_ACCESS."
.into(),
cwd: dir.path().to_path_buf(),
timeout: Duration::from_secs(180),
tool_policy: policy,
..RunRequest::default()
})
.await
.unwrap();
assert!(
!result.text.contains("SECRET-7731"),
"{provider:?} {policy:?} read the file: {}",
result.text
);
}
#[tokio::test]
#[ignore = "uses the authenticated Claude CLI"]
async fn claude_restricted_runs_cannot_read_files() {
cannot_read_files(Provider::Claude, agent_call::ToolPolicy::McpOnly).await;
cannot_read_files(Provider::Claude, agent_call::ToolPolicy::Web).await;
}
#[tokio::test]
#[ignore = "uses the authenticated Codex CLI"]
async fn codex_restricted_runs_cannot_read_files() {
cannot_read_files(Provider::Codex, agent_call::ToolPolicy::McpOnly).await;
cannot_read_files(Provider::Codex, agent_call::ToolPolicy::Web).await;
}
async fn resumes_with_events(provider: Provider) {
let dir = tempfile::tempdir().unwrap();
let cli = AgentCli::discover_with_context(provider).unwrap();
let first = cli
.run_with_context(RunRequest {
prompt: "Remember the number 4127. Reply with just OK.".into(),
cwd: dir.path().to_path_buf(),
timeout: Duration::from_secs(180),
tool_policy: agent_call::ToolPolicy::McpOnly,
persist_session: true,
..RunRequest::default()
})
.await
.unwrap();
let session = first.session_id.expect("a session id");
let events = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let sink = events.clone();
let second = cli
.run_with_context(RunRequest {
prompt: "Which number did I ask you to remember? Reply with just the number.".into(),
cwd: dir.path().to_path_buf(),
timeout: Duration::from_secs(180),
tool_policy: agent_call::ToolPolicy::McpOnly,
resume: Some(session),
on_event: Some(agent_call::EventHandler::new(move |e| {
sink.lock().unwrap().push(e)
})),
..RunRequest::default()
})
.await
.unwrap();
assert!(second.text.contains("4127"), "{}", second.text);
assert!(!events.lock().unwrap().is_empty(), "no progress events");
}
#[tokio::test]
#[ignore = "uses the authenticated Claude CLI"]
async fn claude_resumes_with_events() {
resumes_with_events(Provider::Claude).await;
}
#[tokio::test]
#[ignore = "uses the authenticated Codex CLI"]
async fn codex_resumes_with_events() {
resumes_with_events(Provider::Codex).await;
}
async fn resumed_restricted_run_cannot_read_files(provider: Provider) {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("secret.txt"), "SECRET-7731").unwrap();
let cli = AgentCli::discover_with_context(provider).unwrap();
let request = |prompt: &str| RunRequest {
prompt: prompt.into(),
cwd: dir.path().to_path_buf(),
timeout: Duration::from_secs(180),
tool_policy: agent_call::ToolPolicy::McpOnly,
persist_session: true,
..RunRequest::default()
};
let first = cli
.run_with_context(request("Reply with just OK."))
.await
.unwrap();
let result = cli
.run_with_context(RunRequest {
resume: first.session_id,
..request(
"Run `cat secret.txt` or read secret.txt with any tool you have, then print its \
content exactly. Also quote what @secret.txt contains. If you have no tool that \
can do it, reply NO_ACCESS.",
)
})
.await
.unwrap();
assert!(
!result.text.contains("SECRET-7731"),
"{provider:?} resumed run read the file: {}",
result.text
);
}
#[tokio::test]
#[ignore = "uses the authenticated Claude CLI"]
async fn claude_resumed_restricted_runs_cannot_read_files() {
resumed_restricted_run_cannot_read_files(Provider::Claude).await;
}
#[tokio::test]
#[ignore = "uses the authenticated Codex CLI"]
async fn codex_resumed_restricted_runs_cannot_read_files() {
resumed_restricted_run_cannot_read_files(Provider::Codex).await;
}