fix(ai): discover Grok CLI reasoning effort dynamically via ACP

This commit is contained in:
Guoyu Su
2026-08-10 23:02:42 +08:00
committed by GitHub
parent a5ce8510e7
commit cd78c0ae15
+470 -70
View File
@@ -7,18 +7,20 @@ use crate::ai_cli_agent::{
build_cli_agent_prompt, cli_command, dbx_mcp_enabled_tools, dbx_mcp_scope_env, run_cli_jsonl_agent, toml_string,
toml_string_array, CliAgentCommandSpec, CliAgentJsonlDialect, CliAgentProcessSpec, CliAgentRunOptions,
};
use serde_json::{json, Value};
use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::Notify;
const DEFAULT_GROK_MODELS: &[&str] = &["default", "grok-4.5"];
/// Matches Grok CLI model metadata (`supports_reasoning_effort` + `reasoning_efforts`).
const DEFAULT_GROK_EFFORTS: &[&str] = &["low", "medium", "high"];
const DEFAULT_GROK_EFFORT: &str = "high";
const GROK_MODEL_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(10);
const GROK_PROCESS_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2);
const GROK_ACP_REQUEST_ID: &str = "dbx_model_discovery";
const GROK_ISOLATION_ENV_KEYS: &[&str] = &["HOME", "USERPROFILE", "GROK_HOME", "GROK_AUTH_PATH"];
const DISALLOWED_BUILTIN_TOOLS: &str = "run_terminal_command,read_file,search_replace,list_dir,grep,kill_command_or_subagent,todo_write,get_command_or_subagent_output,spawn_subagent,scheduler_create,scheduler_delete,scheduler_list,monitor,search_tool,use_tool,workflow,enter_plan_mode,exit_plan_mode,ask_user_question,image_gen,image_edit,image_to_video,reference_to_video,write,bash,shell,edit,Glob,Grep";
pub type GrokRunOptions = CliAgentRunOptions;
@@ -26,29 +28,23 @@ pub type GrokCommandSpec = CliAgentCommandSpec;
struct GrokIsolatedHome {
path: PathBuf,
grok_home: PathBuf,
auth_path: Option<PathBuf>,
}
impl GrokIsolatedHome {
fn create(options: &GrokRunOptions) -> Result<Self, String> {
fn create(config: &AiConfig, options: Option<&GrokRunOptions>) -> Result<Self, String> {
let path = env::temp_dir().join(format!("dbx-grok-cli-{}", uuid::Uuid::new_v4()));
let grok_dir = path.join(".grok");
std::fs::create_dir_all(&grok_dir)
.map_err(|error| format!("[grokCliRunFailed] Failed to create isolated Grok home: {error}"))?;
if let Some(auth_source) = real_grok_auth_path() {
let auth_dest = grok_dir.join("auth.json");
std::fs::copy(&auth_source, &auth_dest).map_err(|error| {
format!(
"[grokCliNotAuthenticated] Failed to copy Grok auth credentials into the isolated home: {error}"
)
})?;
}
let config_path = grok_dir.join("config.toml");
std::fs::write(&config_path, grok_mcp_config_toml(options))
let config_contents = options.map(grok_mcp_config_toml).unwrap_or_else(grok_discovery_config_toml);
std::fs::write(&config_path, config_contents)
.map_err(|error| format!("[grokCliRunFailed] Failed to write isolated Grok MCP configuration: {error}"))?;
Ok(Self { path })
Ok(Self { path, grok_home: grok_dir, auth_path: resolve_grok_auth_path(config) })
}
fn write_prompt(&self, prompt: &str) -> Result<PathBuf, String> {
@@ -65,12 +61,37 @@ impl Drop for GrokIsolatedHome {
}
}
fn real_grok_home() -> Option<PathBuf> {
env::var_os("HOME").map(PathBuf::from).map(|home| home.join(".grok")).filter(|path| path.is_dir())
fn grok_discovery_config_toml() -> String {
"[cli]\nauto_update = false\nuse_leader = false\n".to_string()
}
fn real_grok_auth_path() -> Option<PathBuf> {
real_grok_home().map(|home| home.join("auth.json")).filter(|path| path.is_file())
fn configured_grok_env_value(config: &AiConfig, name: &str) -> Option<PathBuf> {
config
.grok_cli_env
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.trim())
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
fn resolve_grok_auth_path(config: &AiConfig) -> Option<PathBuf> {
configured_grok_env_value(config, "GROK_AUTH_PATH")
.or_else(|| configured_grok_env_value(config, "GROK_HOME").map(|home| home.join("auth.json")))
.or_else(|| env::var_os("GROK_AUTH_PATH").filter(|value| !value.is_empty()).map(PathBuf::from))
.or_else(|| {
env::var_os("GROK_HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.map(|home| home.join("auth.json"))
})
.or_else(|| {
env::var_os("HOME")
.or_else(|| env::var_os("USERPROFILE"))
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.map(|home| home.join(".grok").join("auth.json"))
})
}
fn grok_program(config: &AiConfig) -> String {
@@ -80,11 +101,18 @@ fn grok_program(config: &AiConfig) -> String {
fn grok_process_env(
config: &AiConfig,
command: &GrokCommandSpec,
home: Option<&Path>,
isolated_home: Option<&GrokIsolatedHome>,
) -> Result<Vec<(String, String)>, String> {
let mut env = BTreeMap::from_iter(grok_cli_env(config)?);
if let Some(home) = home {
env.insert("HOME".to_string(), home.to_string_lossy().to_string());
if let Some(isolated_home) = isolated_home {
env.retain(|key, _| !GROK_ISOLATION_ENV_KEYS.iter().any(|managed| key.eq_ignore_ascii_case(managed)));
let home = isolated_home.path.to_string_lossy().to_string();
env.insert("HOME".to_string(), home.clone());
env.insert("USERPROFILE".to_string(), home);
env.insert("GROK_HOME".to_string(), isolated_home.grok_home.to_string_lossy().to_string());
if let Some(auth_path) = &isolated_home.auth_path {
env.insert("GROK_AUTH_PATH".to_string(), auth_path.to_string_lossy().to_string());
}
}
if let Some(dir) = command_parent_dir(command) {
let user_path = env.get("PATH").map(String::as_str);
@@ -258,6 +286,7 @@ fn grok_mcp_config_toml(options: &GrokRunOptions) -> String {
let mut lines = vec![
"[cli]".to_string(),
"auto_update = false".to_string(),
"use_leader = false".to_string(),
String::new(),
// Tool auto-approval is driven by the `--always-approve` CLI flag, not a
// config key: grok's permission_mode enum has no "always-approve" value
@@ -289,6 +318,7 @@ pub fn build_grok_command(config: &AiConfig, prompt_file: &Path, options: &GrokR
// `--always-approve`, `--effort`). Prompt is written to a temp file so long
// DBX system+conversation prompts stay under OS argv limits.
let mut args = vec![
"--no-leader".to_string(),
"--prompt-file".to_string(),
prompt_file.to_string_lossy().to_string(),
"--output-format".to_string(),
@@ -335,41 +365,12 @@ pub fn build_grok_prompt(system_prompt: &str, messages: &[crate::ai::AiMessage],
build_cli_agent_prompt("Grok", system_prompt, messages, allow_write_sql)
}
fn grok_effort_option(id: &str, label: &str, description: &str) -> AiEffortOption {
AiEffortOption {
id: id.to_string(),
label: label.to_string(),
description: Some(description.to_string()),
selection: AiEffortSelection::Enum(id.to_string()),
}
}
/// Grok CLI reasoning effort levels (same surface as Codex model effort in the assistant).
pub fn grok_effort_capability() -> AiEffortCapability {
AiEffortCapability::Enum {
options: vec![
grok_effort_option("low", "Low", "Quick, fast implementations"),
grok_effort_option("medium", "Medium", "Balanced effort with standard implementation and testing"),
grok_effort_option("high", "High", "Highest implementation quality with extensive reasoning"),
],
default: AiEffortSelection::Enum(DEFAULT_GROK_EFFORT.to_string()),
source: AiCapabilitySource::LocalCli,
}
}
fn with_grok_effort(mut model: AiModelInfo) -> AiModelInfo {
model.effort_capability = Some(grok_effort_capability());
model.supported_effort_levels =
DEFAULT_GROK_EFFORTS.iter().filter_map(|level| level.parse::<AiEffortLevel>().ok()).collect();
model
}
fn default_grok_models() -> Vec<AiModelInfo> {
DEFAULT_GROK_MODELS
.iter()
.map(|id| {
let display = if *id == "default" { Some("Default".to_string()) } else { None };
with_grok_effort(AiModelInfo::new(*id, display))
AiModelInfo::new(*id, display)
})
.collect()
}
@@ -380,6 +381,71 @@ pub async fn list_grok_models(config: &AiConfig) -> Result<Vec<AiModelInfo>, Str
}
async fn discover_grok_models(config: &AiConfig, program: String) -> Option<Vec<AiModelInfo>> {
if let Some(models) = discover_grok_models_acp(config, program.clone()).await {
return Some(models);
}
discover_grok_models_listing(config, program).await
}
async fn discover_grok_models_acp(config: &AiConfig, program: String) -> Option<Vec<AiModelInfo>> {
let isolated_home = GrokIsolatedHome::create(config, None).ok()?;
let command =
GrokCommandSpec { program, args: vec!["agent".to_string(), "--no-leader".to_string(), "stdio".to_string()] };
let env = grok_process_env(config, &command, Some(&isolated_home)).ok()?;
let mut process = cli_command(&command.program);
process
.args(command.args.iter().map(String::as_str))
.current_dir(&isolated_home.path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true);
for key in GROK_ISOLATION_ENV_KEYS {
process.env_remove(key);
}
process.envs(env.iter().map(|(key, value)| (key.as_str(), value.as_str())));
let mut child = process.spawn().ok()?;
let discovery = async {
let mut stdin = child.stdin.take()?;
let stdout = child.stdout.take()?;
let mut request = serde_json::to_vec(&json!({
"jsonrpc": "2.0",
"id": GROK_ACP_REQUEST_ID,
"method": "initialize",
"params": {
"protocolVersion": 1,
"clientCapabilities": {
"fs": { "readTextFile": false, "writeTextFile": false },
"terminal": false,
},
"clientInfo": { "name": "DBX", "version": env!("CARGO_PKG_VERSION") },
},
}))
.ok()?;
request.push(b'\n');
stdin.write_all(&request).await.ok()?;
stdin.flush().await.ok()?;
let mut lines = BufReader::new(stdout).lines();
while let Ok(Some(line)) = lines.next_line().await {
if let Some(models) = parse_grok_acp_initialize(&line) {
return Some(models);
}
}
None
};
let result = tokio::time::timeout(GROK_MODEL_DISCOVERY_TIMEOUT, discovery).await.ok().flatten();
stop_grok_process(&mut child).await;
result
}
async fn stop_grok_process(child: &mut tokio::process::Child) {
let _ = child.start_kill();
let _ = tokio::time::timeout(GROK_PROCESS_SHUTDOWN_TIMEOUT, child.wait()).await;
}
async fn discover_grok_models_listing(config: &AiConfig, program: String) -> Option<Vec<AiModelInfo>> {
let command = GrokCommandSpec { program, args: vec!["models".to_string()] };
let env = grok_process_env(config, &command, None).ok()?;
let mut process = cli_command(&command.program);
@@ -398,6 +464,159 @@ async fn discover_grok_models(config: &AiConfig, program: String) -> Option<Vec<
parse_grok_models(&String::from_utf8_lossy(&output.stdout))
}
fn parse_grok_acp_initialize(line: &str) -> Option<Vec<AiModelInfo>> {
let response = serde_json::from_str::<Value>(line.trim()).ok()?;
if response.get("id").and_then(Value::as_str) != Some(GROK_ACP_REQUEST_ID) || response.get("error").is_some() {
return None;
}
let result = response.get("result")?;
let model_state = result
.get("_meta")
.and_then(|metadata| metadata.get("modelState").or_else(|| metadata.get("model_state")))
.or_else(|| {
result.get("meta").and_then(|metadata| metadata.get("modelState").or_else(|| metadata.get("model_state")))
})
.or_else(|| result.get("modelState"))
.or_else(|| result.get("model_state"))?;
parse_grok_model_state(model_state)
}
fn parse_grok_model_state(model_state: &Value) -> Option<Vec<AiModelInfo>> {
let current_model_id = model_state
.get("currentModelId")
.or_else(|| model_state.get("current_model_id"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|id| !id.is_empty())
.map(ToString::to_string);
let available_models =
model_state.get("availableModels").or_else(|| model_state.get("available_models")).and_then(Value::as_array)?;
let mut seen = BTreeSet::new();
let mut models = Vec::new();
for model in available_models {
let Some(id) = model
.get("modelId")
.or_else(|| model.get("model_id"))
.or_else(|| model.get("value"))
.or_else(|| model.get("id"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|id| !id.is_empty())
else {
continue;
};
if !seen.insert(id.to_string()) {
continue;
}
let display_name = model
.get("name")
.or_else(|| model.get("displayName"))
.or_else(|| model.get("display_name"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|name| !name.is_empty())
.map(ToString::to_string);
let mut info = AiModelInfo::new(id, display_name);
info.effort_capability = parse_grok_effort_capability(model);
info.supported_effort_levels = grok_legacy_effort_levels(info.effort_capability.as_ref());
models.push(info);
}
if models.is_empty() {
return None;
}
let inherited = current_model_id
.as_deref()
.and_then(|current| models.iter().find(|model| model.id == current))
.or_else(|| models.iter().find(|model| model.id.eq_ignore_ascii_case("default")))
.cloned();
models.retain(|model| !model.id.eq_ignore_ascii_case("default"));
let mut default_model = AiModelInfo::new("default", Some("Default".to_string()));
if let Some(inherited) = inherited {
default_model.effort_capability = inherited.effort_capability;
default_model.supported_effort_levels = inherited.supported_effort_levels;
}
models.insert(0, default_model);
Some(models)
}
fn grok_model_metadata_field<'a>(model: &'a Value, camel_case: &str, snake_case: &str) -> Option<&'a Value> {
model
.get("_meta")
.and_then(|metadata| metadata.get(camel_case).or_else(|| metadata.get(snake_case)))
.or_else(|| {
model.get("meta").and_then(|metadata| metadata.get(camel_case).or_else(|| metadata.get(snake_case)))
})
.or_else(|| model.get(camel_case))
.or_else(|| model.get(snake_case))
}
fn parse_grok_effort_capability(model: &Value) -> Option<AiEffortCapability> {
if grok_model_metadata_field(model, "supportsReasoningEffort", "supports_reasoning_effort").and_then(Value::as_bool)
== Some(false)
{
return Some(AiEffortCapability::Unsupported);
}
let efforts = grok_model_metadata_field(model, "reasoningEfforts", "reasoning_efforts")?.as_array()?;
let mut seen = BTreeSet::new();
let mut provider_default = None;
let options = efforts
.iter()
.filter_map(|effort| {
let id = match effort {
Value::String(value) => value.as_str(),
Value::Object(_) => effort.get("value").or_else(|| effort.get("id")).and_then(Value::as_str)?,
_ => return None,
}
.trim();
if id.is_empty() || !seen.insert(id.to_string()) {
return None;
}
if provider_default.is_none()
&& effort
.get("default")
.or_else(|| effort.get("isDefault"))
.or_else(|| effort.get("is_default"))
.and_then(Value::as_bool)
== Some(true)
{
provider_default = Some(AiEffortSelection::Enum(id.to_string()));
}
let label = effort
.get("label")
.or_else(|| effort.get("name"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|label| !label.is_empty())
.unwrap_or(id)
.to_string();
let description = effort
.get("description")
.and_then(Value::as_str)
.map(str::trim)
.filter(|description| !description.is_empty())
.map(ToString::to_string);
Some(AiEffortOption {
id: id.to_string(),
label,
description,
selection: AiEffortSelection::Enum(id.to_string()),
})
})
.collect::<Vec<_>>();
let default = provider_default.or_else(|| options.first().map(|option| option.selection.clone()))?;
Some(AiEffortCapability::Enum { options, default, source: AiCapabilitySource::LocalCli })
}
fn grok_legacy_effort_levels(capability: Option<&AiEffortCapability>) -> Vec<AiEffortLevel> {
let Some(AiEffortCapability::Enum { options, .. }) = capability else {
return Vec::new();
};
options.iter().filter_map(|option| option.id.parse::<AiEffortLevel>().ok()).collect()
}
fn parse_grok_models(stdout: &str) -> Option<Vec<AiModelInfo>> {
let mut seen = BTreeSet::new();
let mut models = Vec::new();
@@ -410,26 +629,37 @@ fn parse_grok_models(stdout: &str) -> Option<Vec<AiModelInfo>> {
if id.is_empty() || !seen.insert(id.to_string()) {
continue;
}
models.push(with_grok_effort(AiModelInfo::new(id, None)));
models.push(AiModelInfo::new(id, None));
}
if models.is_empty() {
return None;
}
if seen.insert("default".to_string()) {
models.insert(0, with_grok_effort(AiModelInfo::new("default", Some("Default".to_string()))));
models.insert(0, AiModelInfo::new("default", Some("Default".to_string())));
}
Some(models)
}
pub async fn test_grok_connection(config: &AiConfig) -> Result<AiTestConnectionResult, String> {
test_grok_connection_with_timeout(config, GROK_MODEL_DISCOVERY_TIMEOUT).await
}
async fn test_grok_connection_with_timeout(
config: &AiConfig,
timeout: Duration,
) -> Result<AiTestConnectionResult, String> {
let start = Instant::now();
let program = validate_grok_program(config)?;
let command = GrokCommandSpec { program, args: vec!["models".to_string()] };
let mut process = cli_command(&command.program);
process.args(command.args.iter().map(String::as_str));
process.envs(grok_process_env(config, &command, None)?.iter().map(|(key, value)| (key.as_str(), value.as_str())));
process.kill_on_drop(true);
let output = process.output().await.map_err(|e| classify_grok_spawn_error(&e.to_string()))?;
let output = tokio::time::timeout(timeout, process.output())
.await
.map_err(|_| "[grokCliRunFailed] Grok CLI connection test timed out.".to_string())?
.map_err(|e| classify_grok_spawn_error(&e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined =
@@ -507,16 +737,16 @@ pub async fn run_grok_agent(
on_event: impl Fn(AgentEvent) + Send + Sync + 'static,
) -> Result<String, String> {
let program = validate_grok_program(config)?;
let isolated_home = GrokIsolatedHome::create(&options)?;
let isolated_home = GrokIsolatedHome::create(config, Some(&options))?;
let prompt_path = isolated_home.write_prompt(prompt)?;
let mut command = build_grok_command(config, &prompt_path, &options);
command.program = program;
let env = grok_process_env(config, &command, Some(&isolated_home.path))?;
let env = grok_process_env(config, &command, Some(&isolated_home))?;
run_cli_jsonl_agent(
CliAgentProcessSpec {
command,
env,
env_remove: Vec::new(),
env_remove: GROK_ISOLATION_ENV_KEYS.iter().map(|key| (*key).to_string()).collect(),
current_dir: Some(isolated_home.path.clone()),
stdin: None,
// Grok headless `streaming-json`: text/thought/tool_call/tool_call_update/end/error.
@@ -533,14 +763,17 @@ pub async fn run_grok_agent(
#[cfg(test)]
mod tests {
use super::{
build_grok_command, classify_grok_run_error, default_grok_models, grok_cli_env, grok_effort_capability,
grok_mcp_config_toml, parse_grok_models, validate_grok_program, GrokRunOptions,
build_grok_command, classify_grok_run_error, default_grok_models, grok_cli_env, grok_mcp_config_toml,
grok_process_env, parse_grok_acp_initialize, parse_grok_models, validate_grok_program, GrokIsolatedHome,
GrokRunOptions,
};
use crate::ai::{
AiApiStyle, AiAuthMethod, AiCapabilitySource, AiConfig, AiEffortCapability, AiEffortLevel, AiEffortSelection,
AiProvider, AiReasoningLevel,
};
use crate::ai_cli_agent::CliAgentCommandSpec;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
fn base_config() -> AiConfig {
@@ -616,6 +849,7 @@ mod tests {
let prompt = PathBuf::from("/tmp/dbx-prompt.txt");
let command = build_grok_command(&config, &prompt, &run_options());
assert_eq!(command.program, "grok");
assert!(command.args.iter().any(|arg| arg == "--no-leader"));
assert!(command.args.windows(2).any(|pair| pair == ["--prompt-file", "/tmp/dbx-prompt.txt"]));
assert!(command.args.windows(2).any(|pair| pair == ["--output-format", "streaming-json"]));
assert!(command.args.iter().any(|arg| arg == "--always-approve"));
@@ -638,20 +872,185 @@ mod tests {
}
#[test]
fn models_expose_low_medium_high_effort_like_codex() {
let capability = grok_effort_capability();
let AiEffortCapability::Enum { options, default, source } = capability else {
panic!("expected enum effort capability");
fn parses_model_specific_effort_from_acp_initialize() {
let models = parse_grok_acp_initialize(
r#"{"jsonrpc":"2.0","id":"dbx_model_discovery","result":{"_meta":{"modelState":{"currentModelId":"grok-4.5","availableModels":[{"modelId":"grok-4.5","name":"Grok 4.5","_meta":{"supportsReasoningEffort":true,"reasoningEfforts":[{"value":"medium","label":"Medium Effort","default":false},{"id":"deep","value":"high","label":"High Effort","description":"Deep reasoning","default":true},{"value":"future","label":"Future Effort"},{"value":"high","label":"Duplicate","default":true}]}},{"modelId":"grok-fast","name":"Grok Fast","meta":{"supportsReasoningEffort":false}}]}}}}"#,
)
.unwrap();
assert_eq!(
models.iter().map(|model| model.id.as_str()).collect::<Vec<_>>(),
vec!["default", "grok-4.5", "grok-fast"]
);
for id in ["default", "grok-4.5"] {
let model = models.iter().find(|model| model.id == id).unwrap();
let Some(AiEffortCapability::Enum { options, default, source }) = &model.effort_capability else {
panic!("expected dynamic Grok effort capability");
};
assert_eq!(*source, AiCapabilitySource::LocalCli);
assert_eq!(*default, AiEffortSelection::Enum("high".to_string()));
assert_eq!(
options.iter().map(|option| option.id.as_str()).collect::<Vec<_>>(),
vec!["medium", "high", "future"]
);
assert_eq!(options[1].label, "High Effort");
assert_eq!(options[1].description.as_deref(), Some("Deep reasoning"));
assert_eq!(model.supported_effort_levels, vec![AiEffortLevel::Medium, AiEffortLevel::High]);
}
let fast = models.iter().find(|model| model.id == "grok-fast").unwrap();
assert_eq!(fast.effort_capability, Some(AiEffortCapability::Unsupported));
assert!(fast.supported_effort_levels.is_empty());
}
#[test]
fn accepts_compatibility_shapes_and_bare_effort_values() {
let models = parse_grok_acp_initialize(
r#"{"jsonrpc":"2.0","id":"dbx_model_discovery","result":{"meta":{"model_state":{"current_model_id":"grok-next","available_models":[{"model_id":"grok-next","display_name":"Grok Next","supports_reasoning_effort":true,"reasoning_efforts":["low","xhigh","quantum","low"]}]}}}}"#,
)
.unwrap();
let model = models.iter().find(|model| model.id == "grok-next").unwrap();
let Some(AiEffortCapability::Enum { options, .. }) = &model.effort_capability else {
panic!("expected dynamic Grok effort capability");
};
assert_eq!(source, AiCapabilitySource::LocalCli);
assert_eq!(default, AiEffortSelection::Enum("high".to_string()));
assert_eq!(options.iter().map(|option| option.id.as_str()).collect::<Vec<_>>(), vec!["low", "medium", "high"]);
assert_eq!(
options.iter().map(|option| option.id.as_str()).collect::<Vec<_>>(),
vec!["low", "xhigh", "quantum"]
);
assert_eq!(model.supported_effort_levels, vec![AiEffortLevel::Low, AiEffortLevel::Xhigh]);
}
#[test]
fn missing_effort_metadata_and_listing_fallback_do_not_invent_levels() {
let models = parse_grok_acp_initialize(
r#"{"jsonrpc":"2.0","id":"dbx_model_discovery","result":{"_meta":{"modelState":{"currentModelId":"grok-plain","availableModels":[{"modelId":"grok-plain"},{"modelId":"grok-empty","_meta":{"supportsReasoningEffort":true}}]}}}}"#,
)
.unwrap();
assert!(models.iter().all(|model| model.effort_capability.is_none()));
assert!(models.iter().all(|model| model.supported_effort_levels.is_empty()));
let stdout = "You are logged in with grok.com.\n\nDefault model: grok-4.5\n\nAvailable models:\n * grok-4.5 (default)\n";
let models = parse_grok_models(stdout).unwrap();
let grok = models.iter().find(|model| model.id == "grok-4.5").unwrap();
assert!(matches!(grok.effort_capability, Some(AiEffortCapability::Enum { .. })));
assert_eq!(grok.supported_effort_levels, vec![AiEffortLevel::Low, AiEffortLevel::Medium, AiEffortLevel::High]);
assert!(models.iter().all(|model| model.effort_capability.is_none()));
assert!(default_grok_models().iter().all(|model| model.effort_capability.is_none()));
}
#[test]
fn isolated_environment_overrides_config_home_but_reuses_selected_auth() {
let mut config = base_config();
config.grok_cli_env.insert("GROK_HOME".to_string(), "/configured/grok-home".to_string());
config.grok_cli_env.insert("GROK_AUTH_PATH".to_string(), "/selected/auth.json".to_string());
let isolated = GrokIsolatedHome::create(&config, None).unwrap();
let command = super::GrokCommandSpec {
program: "grok".to_string(),
args: vec!["agent".to_string(), "stdio".to_string()],
};
let env = grok_process_env(&config, &command, Some(&isolated))
.unwrap()
.into_iter()
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(env.get("HOME"), Some(&isolated.path.to_string_lossy().to_string()));
assert_eq!(env.get("USERPROFILE"), Some(&isolated.path.to_string_lossy().to_string()));
assert_eq!(env.get("GROK_HOME"), Some(&isolated.grok_home.to_string_lossy().to_string()));
assert_eq!(env.get("GROK_AUTH_PATH").map(String::as_str), Some("/selected/auth.json"));
assert_ne!(env.get("GROK_HOME").map(String::as_str), Some("/configured/grok-home"));
}
#[cfg(unix)]
fn fake_grok_config() -> (AiConfig, PathBuf, PathBuf, PathBuf) {
let project_dir = std::env::temp_dir().join(format!("dbx-grok-project-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(project_dir.join(".grok")).unwrap();
let fallback_marker = project_dir.join("models-command-used");
let inference_marker = project_dir.join("inference-command-used");
let isolation_marker = project_dir.join("isolation-failed");
let executable = project_dir.join("grok");
std::fs::write(
&executable,
r#"#!/bin/sh
case " $* " in
*" --prompt-file "*)
printf used > "$DBX_TEST_INFERENCE_MARKER"
exit 12
;;
*" agent --no-leader stdio "*)
if [ "$GROK_HOME" = "$DBX_TEST_PROJECT_DIR/.grok" ] || [ "$PWD" = "$DBX_TEST_PROJECT_DIR" ] || [ "$GROK_AUTH_PATH" != "/selected/auth.json" ]; then
printf leaked > "$DBX_TEST_ISOLATION_MARKER"
exit 13
fi
IFS= read -r request
case "$request" in
*'"method":"initialize"'*)
printf '%s\n' '{"jsonrpc":"2.0","id":"dbx_model_discovery","result":{"_meta":{"modelState":{"currentModelId":"grok-dynamic","availableModels":[{"modelId":"grok-dynamic","name":"Grok Dynamic","_meta":{"supportsReasoningEffort":true,"reasoningEfforts":[{"value":"xhigh","label":"Extra High"},{"value":"future","label":"Future"}]}}]}}}}'
;;
*)
exit 14
;;
esac
;;
*" models "*)
if [ "$DBX_TEST_MODELS_HANG" = "1" ]; then
while :; do :; done
fi
printf used > "$DBX_TEST_MODELS_MARKER"
printf '%s\n' 'You are logged in with grok.com.' 'Available models:' ' * grok-fallback (default)'
;;
*)
exit 15
;;
esac
"#,
)
.unwrap();
let mut permissions = std::fs::metadata(&executable).unwrap().permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&executable, permissions).unwrap();
let mut config = base_config();
config.grok_cli_path = Some(executable.to_string_lossy().to_string());
config.grok_cli_env.insert("GROK_HOME".to_string(), project_dir.join(".grok").to_string_lossy().to_string());
config.grok_cli_env.insert("GROK_AUTH_PATH".to_string(), "/selected/auth.json".to_string());
config.grok_cli_env.insert("DBX_TEST_PROJECT_DIR".to_string(), project_dir.to_string_lossy().to_string());
config.grok_cli_env.insert("DBX_TEST_MODELS_MARKER".to_string(), fallback_marker.to_string_lossy().to_string());
config
.grok_cli_env
.insert("DBX_TEST_INFERENCE_MARKER".to_string(), inference_marker.to_string_lossy().to_string());
config
.grok_cli_env
.insert("DBX_TEST_ISOLATION_MARKER".to_string(), isolation_marker.to_string_lossy().to_string());
(config, project_dir, fallback_marker, inference_marker)
}
#[cfg(unix)]
#[tokio::test]
async fn discovery_uses_isolated_acp_initialize_without_inference() {
let (config, project_dir, models_marker, inference_marker) = fake_grok_config();
let models = super::list_grok_models(&config).await.unwrap();
assert_eq!(models.iter().map(|model| model.id.as_str()).collect::<Vec<_>>(), ["default", "grok-dynamic"]);
let Some(AiEffortCapability::Enum { options, .. }) = &models[1].effort_capability else {
panic!("expected dynamic Grok effort capability");
};
assert_eq!(options.iter().map(|option| option.id.as_str()).collect::<Vec<_>>(), ["xhigh", "future"]);
assert!(!models_marker.exists(), "ACP success must not fall back to the human-readable model listing");
assert!(!inference_marker.exists(), "model discovery must not issue an inference request");
assert!(!project_dir.join("isolation-failed").exists());
let result = super::test_grok_connection(&config).await.unwrap();
assert!(result.success);
assert!(models_marker.exists(), "connection testing should use the non-inference models command");
assert!(!inference_marker.exists());
let _ = std::fs::remove_dir_all(project_dir);
}
#[cfg(unix)]
#[tokio::test]
async fn connection_test_is_timeout_bounded() {
let (mut config, project_dir, _, _) = fake_grok_config();
config.grok_cli_env.insert("DBX_TEST_MODELS_HANG".to_string(), "1".to_string());
let error =
super::test_grok_connection_with_timeout(&config, std::time::Duration::from_millis(50)).await.unwrap_err();
assert!(error.starts_with("[grokCliRunFailed]"));
assert!(error.contains("timed out"));
let _ = std::fs::remove_dir_all(project_dir);
}
#[test]
@@ -663,6 +1062,7 @@ mod tests {
assert!(toml.contains("DBX_MCP_SCOPE_CONNECTION_ID = \"conn-1\""));
assert!(toml.contains("DBX_MCP_ALLOW_WRITES = \"0\""));
assert!(toml.contains("[permission]"));
assert!(toml.contains("use_leader = false"));
assert!(toml.contains("MCPTool(dbx__*)"));
// Invalid fields removed: grok has no "always-approve" permission_mode,
// and startup_timeout_sec/tool_timeout_sec/enabled_tools are not mcp_servers keys.