#![cfg(unix)]
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use embacle::config::RunnerConfig;
use embacle::types::{ChatMessage, ChatRequest, ErrorKind, LlmProvider};
use embacle::{ClaudeCodeRunner, MetricsProvider};
mod support;
use support::retry_text_file_busy;
fn fake_claude(dir: &Path, stdout: &str, exit_code: i32) -> PathBuf {
let path = dir.join("claude");
let script = format!("#!/bin/sh\ncat <<'CLAUDE_EOF'\n{stdout}\nCLAUDE_EOF\nexit {exit_code}\n");
fs::write(&path, script).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
path
}
fn ping() -> ChatRequest {
ChatRequest::new(vec![ChatMessage::user("ping")])
}
#[tokio::test]
async fn an_error_document_outranks_the_exit_code() {
let dir = tempfile::tempdir().unwrap();
let padding = "x".repeat(600);
let doc = format!(
r#"{{"duration_api_ms":6681,"session_id":"{padding}","usage":{{"input_tokens":8,"output_tokens":4}},"is_error":true,"subtype":"error_during_execution","api_error_status":429,"result":"Claude usage limit reached"}}"#
);
let runner = ClaudeCodeRunner::new(RunnerConfig::new(fake_claude(dir.path(), &doc, 1)));
let request = ping();
let err = retry_text_file_busy(|| runner.complete(&request))
.await
.unwrap_err();
assert_eq!(err.kind, ErrorKind::RateLimit, "{}", err.message);
assert!(
err.message.contains("usage limit reached"),
"{}",
err.message
);
assert!(
err.message.contains("api_error_status: 429"),
"{}",
err.message
);
assert!(!err.message.contains("duration_api_ms"), "{}", err.message);
}
#[tokio::test]
async fn a_non_document_on_a_failed_exit_keeps_the_exit_diagnostic() {
let dir = tempfile::tempdir().unwrap();
let binary = fake_claude(dir.path(), "Not logged in · Please run /login", 1);
let runner = ClaudeCodeRunner::new(RunnerConfig::new(binary));
let request = ping();
let err = retry_text_file_busy(|| runner.complete(&request))
.await
.unwrap_err();
assert_eq!(err.kind, ErrorKind::ExternalService);
assert!(
err.message.contains("exited with code 1"),
"{}",
err.message
);
assert!(err.message.contains("Not logged in"), "{}", err.message);
}
#[tokio::test]
async fn a_success_document_on_a_failed_exit_is_still_a_failure() {
let dir = tempfile::tempdir().unwrap();
let binary = fake_claude(dir.path(), r#"{"is_error":false,"result":"pong"}"#, 3);
let runner = ClaudeCodeRunner::new(RunnerConfig::new(binary));
let request = ping();
let err = retry_text_file_busy(|| runner.complete(&request))
.await
.unwrap_err();
assert!(
err.message.contains("exited with code 3"),
"{}",
err.message
);
}
const HAIKU_5_5_TURN: &str = r#"{"type":"result","subtype":"success","is_error":false,"result":"ok","usage":{"input_tokens":200000,"output_tokens":100000},"modelUsage":{"claude-haiku-5-5":{"inputTokens":200000,"outputTokens":100000,"costUSD":0.07}}}"#;
#[tokio::test]
async fn a_claude_code_turn_is_priced_on_the_model_that_served_it() {
let dir = tempfile::tempdir().unwrap();
let binary = fake_claude(dir.path(), HAIKU_5_5_TURN, 0);
let runner = ClaudeCodeRunner::new(RunnerConfig::new(binary).with_model("haiku"));
let metered = MetricsProvider::new(Box::new(runner));
let request = ping();
let response = retry_text_file_busy(|| metered.complete(&request))
.await
.unwrap();
let report = metered.report().unwrap();
assert_eq!(response.model, "claude-haiku-5-5");
assert!(
(report.total_cost - 0.07).abs() < 1e-9,
"a Claude Code turn must bill at Haiku 5.5's rate, not $0; got {}",
report.total_cost
);
}
#[tokio::test]
async fn without_model_usage_the_turn_names_the_model_sent() {
let dir = tempfile::tempdir().unwrap();
let binary = fake_claude(dir.path(), r#"{"is_error":false,"result":"pong"}"#, 0);
let runner = ClaudeCodeRunner::new(RunnerConfig::new(binary).with_model("claude-sonnet-5-5"));
let request = ping();
let response = retry_text_file_busy(|| runner.complete(&request))
.await
.unwrap();
assert_eq!(response.model, "claude-sonnet-5-5");
}