Enforce machine-readable output across CLI surfaces

This commit is contained in:
Yeachan-Heo
2026-04-05 17:35:36 +00:00
parent 31163be347
commit 00b557c8b7
2 changed files with 803 additions and 100 deletions

View File

@@ -0,0 +1,449 @@
use std::fs;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};
use mock_anthropic_service::{MockAnthropicService, SCENARIO_PREFIX};
use serde_json::{json, Value};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn help_emits_json_when_requested() {
let root = unique_temp_dir("help-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "help"], &envs);
assert_eq!(parsed["kind"], "help");
assert!(parsed["message"]
.as_str()
.expect("help message")
.contains("Usage:"));
}
#[test]
fn version_emits_json_when_requested() {
let root = unique_temp_dir("version-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "version"], &envs);
assert_eq!(parsed["kind"], "version");
assert_eq!(parsed["version"], env!("CARGO_PKG_VERSION"));
}
#[test]
fn status_emits_json_when_requested() {
let root = unique_temp_dir("status-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "status"], &envs);
assert_eq!(parsed["kind"], "status");
assert!(parsed["workspace"]["cwd"].as_str().is_some());
}
#[test]
fn sandbox_emits_json_when_requested() {
let root = unique_temp_dir("sandbox-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "sandbox"], &envs);
assert_eq!(parsed["kind"], "sandbox");
assert!(parsed["sandbox"].is_object());
}
#[test]
fn dump_manifests_emits_json_when_requested() {
let root = unique_temp_dir("dump-manifests-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let upstream = write_upstream_fixture(&root);
let mut envs = isolated_env(&root);
envs.push((
"CLAUDE_CODE_UPSTREAM".to_string(),
upstream.display().to_string(),
));
let parsed = assert_json_command(&root, &["--output-format", "json", "dump-manifests"], &envs);
assert_eq!(parsed["kind"], "dump-manifests");
assert_eq!(parsed["commands"], 1);
assert_eq!(parsed["tools"], 1);
}
#[test]
fn bootstrap_plan_emits_json_when_requested() {
let root = unique_temp_dir("bootstrap-plan-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "bootstrap-plan"], &envs);
assert_eq!(parsed["kind"], "bootstrap-plan");
assert!(parsed["phases"].as_array().expect("phases array").len() > 1);
}
#[test]
fn agents_emits_json_when_requested() {
let root = unique_temp_dir("agents-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "agents"], &envs);
assert_eq!(parsed["kind"], "agents");
assert!(!parsed["message"].as_str().expect("agents text").is_empty());
}
#[test]
fn mcp_emits_json_when_requested() {
let root = unique_temp_dir("mcp-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "mcp"], &envs);
assert_eq!(parsed["kind"], "mcp");
assert!(parsed["message"]
.as_str()
.expect("mcp text")
.contains("MCP"));
}
#[test]
fn skills_emits_json_when_requested() {
let root = unique_temp_dir("skills-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "skills"], &envs);
assert_eq!(parsed["kind"], "skills");
assert!(!parsed["message"].as_str().expect("skills text").is_empty());
}
#[test]
fn system_prompt_emits_json_when_requested() {
let root = unique_temp_dir("system-prompt-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "system-prompt"], &envs);
assert_eq!(parsed["kind"], "system-prompt");
assert!(parsed["message"]
.as_str()
.expect("system prompt text")
.contains("You are an interactive agent"));
}
#[test]
fn login_emits_json_when_requested() {
let root = unique_temp_dir("login-json");
let workspace = root.join("workspace");
fs::create_dir_all(&workspace).expect("workspace should exist");
let mut envs = isolated_env(&root);
let callback_port = reserve_port();
let token_port = reserve_port();
fs::create_dir_all(workspace.join(".claw")).expect("config dir should exist");
fs::write(
workspace.join(".claw").join("settings.json"),
json!({
"oauth": {
"clientId": "test-client",
"authorizeUrl": format!("http://127.0.0.1:{token_port}/authorize"),
"tokenUrl": format!("http://127.0.0.1:{token_port}/token"),
"callbackPort": callback_port,
"scopes": ["user:test"]
}
})
.to_string(),
)
.expect("oauth config should write");
let token_server = thread::spawn(move || {
let listener = TcpListener::bind(("127.0.0.1", token_port)).expect("token server bind");
let (mut stream, _) = listener.accept().expect("token request");
let mut request = [0_u8; 4096];
let _ = stream
.read(&mut request)
.expect("token request should read");
let body = json!({
"access_token": "test-access-token",
"refresh_token": "test-refresh-token",
"expires_at": 9_999_999_999_u64,
"scopes": ["user:test"]
})
.to_string();
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
body.len(),
body
);
stream
.write_all(response.as_bytes())
.expect("token response should write");
});
let bin_dir = root.join("bin");
fs::create_dir_all(&bin_dir).expect("bin dir should exist");
let opener_path = bin_dir.join("xdg-open");
fs::write(
&opener_path,
format!(
"#!/usr/bin/env python3\nimport http.client\nimport sys\nimport urllib.parse\nurl = sys.argv[1]\nquery = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)\nstate = query['state'][0]\nconn = http.client.HTTPConnection('127.0.0.1', {callback_port}, timeout=5)\nconn.request('GET', f\"/callback?code=test-code&state={{urllib.parse.quote(state)}}\")\nresp = conn.getresponse()\nresp.read()\nconn.close()\n"
),
)
.expect("xdg-open wrapper should write");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(&opener_path)
.expect("wrapper metadata")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(&opener_path, permissions).expect("wrapper permissions");
}
let original_path = envs
.iter()
.find(|(key, _)| key == "PATH")
.map(|(_, value)| value.clone())
.unwrap_or_default();
for (key, value) in &mut envs {
if key == "PATH" {
*value = format!("{}:{original_path}", bin_dir.display());
}
}
let parsed = assert_json_command(&workspace, &["--output-format", "json", "login"], &envs);
token_server.join().expect("token server should finish");
assert_eq!(parsed["kind"], "login");
assert_eq!(parsed["callback_port"], callback_port);
}
#[test]
fn logout_emits_json_when_requested() {
let root = unique_temp_dir("logout-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&root, &["--output-format", "json", "logout"], &envs);
assert_eq!(parsed["kind"], "logout");
assert!(parsed["message"]
.as_str()
.expect("logout text")
.contains("cleared"));
}
#[test]
fn init_emits_json_when_requested() {
let root = unique_temp_dir("init-json");
let workspace = root.join("workspace");
fs::create_dir_all(&workspace).expect("workspace should exist");
let envs = isolated_env(&root);
let parsed = assert_json_command(&workspace, &["--output-format", "json", "init"], &envs);
assert_eq!(parsed["kind"], "init");
assert!(workspace.join("CLAUDE.md").exists());
}
#[test]
fn prompt_subcommand_emits_json_when_requested() {
let root = unique_temp_dir("prompt-subcommand-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let mut envs = isolated_env(&root);
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
let server = runtime
.block_on(MockAnthropicService::spawn())
.expect("mock service should start");
envs.push(("ANTHROPIC_API_KEY".to_string(), "test-key".to_string()));
envs.push(("ANTHROPIC_BASE_URL".to_string(), server.base_url()));
let prompt = format!("{SCENARIO_PREFIX}streaming_text");
let args = vec![
"--model".to_string(),
"sonnet".to_string(),
"--permission-mode".to_string(),
"read-only".to_string(),
"--output-format".to_string(),
"json".to_string(),
"prompt".to_string(),
prompt,
];
let output = run_claw_with_env_owned(&root, &args, &envs);
let parsed = parse_json_stdout(&output);
assert_eq!(parsed["model"], "claude-sonnet-4-6");
assert!(parsed["message"]
.as_str()
.expect("assistant text")
.contains("streaming"));
}
#[test]
fn bare_prompt_mode_emits_json_when_requested() {
let root = unique_temp_dir("bare-prompt-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let mut envs = isolated_env(&root);
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
let server = runtime
.block_on(MockAnthropicService::spawn())
.expect("mock service should start");
envs.push(("ANTHROPIC_API_KEY".to_string(), "test-key".to_string()));
envs.push(("ANTHROPIC_BASE_URL".to_string(), server.base_url()));
let prompt = format!("{SCENARIO_PREFIX}streaming_text");
let args = vec![
"--model".to_string(),
"sonnet".to_string(),
"--permission-mode".to_string(),
"read-only".to_string(),
"--output-format".to_string(),
"json".to_string(),
prompt,
];
let output = run_claw_with_env_owned(&root, &args, &envs);
let parsed = parse_json_stdout(&output);
assert_eq!(parsed["model"], "claude-sonnet-4-6");
assert!(parsed["message"]
.as_str()
.expect("assistant text")
.contains("streaming"));
}
#[test]
fn resume_restore_emits_json_when_requested() {
let root = unique_temp_dir("resume-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let envs = isolated_env(&root);
let session_path = root.join("session.jsonl");
fs::write(
&session_path,
"{\"type\":\"session_meta\",\"version\":3,\"session_id\":\"resume-json\",\"created_at_ms\":0,\"updated_at_ms\":0}\n{\"type\":\"message\",\"message\":{\"role\":\"user\",\"blocks\":[{\"type\":\"text\",\"text\":\"hello\"}]}}\n",
)
.expect("session should write");
let args = vec![
"--output-format".to_string(),
"json".to_string(),
"--resume".to_string(),
session_path.display().to_string(),
];
let output = run_claw_with_env_owned(&root, &args, &envs);
let parsed = parse_json_stdout(&output);
assert_eq!(parsed["kind"], "resume");
assert_eq!(parsed["messages"], 1);
}
fn assert_json_command(current_dir: &Path, args: &[&str], envs: &[(String, String)]) -> Value {
let output = run_claw_with_env(current_dir, args, envs);
parse_json_stdout(&output)
}
fn parse_json_stdout(output: &Output) -> Value {
assert!(
output.status.success(),
"stdout:\n{}\n\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).expect("stdout should be json")
}
fn run_claw_with_env(current_dir: &Path, args: &[&str], envs: &[(String, String)]) -> Output {
let owned_args = args
.iter()
.map(|value| (*value).to_string())
.collect::<Vec<_>>();
run_claw_with_env_owned(current_dir, &owned_args, envs)
}
fn run_claw_with_env_owned(
current_dir: &Path,
args: &[String],
envs: &[(String, String)],
) -> Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
command.current_dir(current_dir).args(args).env_clear();
for (key, value) in envs {
command.env(key, value);
}
command.output().expect("claw should launch")
}
fn isolated_env(root: &Path) -> Vec<(String, String)> {
let config_home = root.join("config-home");
let home = root.join("home");
fs::create_dir_all(&config_home).expect("config home should exist");
fs::create_dir_all(&home).expect("home should exist");
vec![
(
"CLAW_CONFIG_HOME".to_string(),
config_home.display().to_string(),
),
("HOME".to_string(), home.display().to_string()),
(
"PATH".to_string(),
std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin".to_string()),
),
("NO_COLOR".to_string(), "1".to_string()),
]
}
fn write_upstream_fixture(root: &Path) -> PathBuf {
let upstream = root.join("claw-code");
let src = upstream.join("src");
let entrypoints = src.join("entrypoints");
fs::create_dir_all(&entrypoints).expect("upstream entrypoints dir should exist");
fs::write(
src.join("commands.ts"),
"import FooCommand from './commands/foo'\n",
)
.expect("commands fixture should write");
fs::write(
src.join("tools.ts"),
"import ReadTool from './tools/read'\n",
)
.expect("tools fixture should write");
fs::write(
entrypoints.join("cli.tsx"),
"if (args[0] === '--version') {}\nstartupProfiler()\n",
)
.expect("cli fixture should write");
upstream
}
fn reserve_port() -> u16 {
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("ephemeral port should bind");
let port = listener.local_addr().expect("local addr").port();
drop(listener);
port
}
fn unique_temp_dir(label: &str) -> PathBuf {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after epoch")
.as_millis();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"claw-output-format-{label}-{}-{millis}-{counter}",
std::process::id()
))
}