#![cfg(unix)]
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::fmt::Write as _;
use std::fs;
use std::mem;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use embacle::config::RunnerConfig;
use embacle::types::{ChatMessage, ChatRequest, LlmProvider};
use embacle::{
ClaudeCodeRunner, ClineCliRunner, ContinueCliRunner, CursorAgentRunner, GeminiCliRunner,
GooseCliRunner, KiloCliRunner, KiroCliRunner, OpenCodeRunner, WarpCliRunner,
};
use tokio_stream::StreamExt;
mod support;
use support::retry_text_file_busy;
const SESSION_ID: &str = "sess-alice-7f3a";
const CALL_END: &str = "--END-OF-CALL--";
const RESUME_FLAGS: &[&str] = &[
"--resume",
"-r",
"--continue",
"-c",
"--session",
"--session-id",
"--taskId",
"--conversation",
"--fork-session",
];
const RECORDED_ENV: &[&str] = &[
"CLAUDE_CODE_DISABLE_AUTO_MEMORY",
"CLAUDE_CODE_DISABLE_CLAUDE_MDS",
];
fn fake_cli(dir: &Path, stdout: &str) -> PathBuf {
let path = dir.join("fake-cli");
let log = dir.join("argv.log");
let mut env_lines = String::new();
for name in RECORDED_ENV {
write!(env_lines, " printf 'env:{name}=%s\\n' \"${name}\";").unwrap();
}
let script = format!(
"#!/bin/sh\n{{ for a in \"$@\"; do printf '%s\\n' \"$a\"; done;{env_lines} printf '%s\\n' '{CALL_END}'; }} >> '{}'\ncat <<'FAKE_EOF'\n{stdout}\nFAKE_EOF\n",
log.display()
);
fs::write(&path, script).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
path
}
fn recorded_calls(dir: &Path) -> Vec<Vec<String>> {
let log = fs::read_to_string(dir.join("argv.log")).unwrap();
let mut calls = Vec::new();
let mut current = Vec::new();
for line in log.lines() {
if line == CALL_END {
calls.push(mem::take(&mut current));
} else {
current.push(line.to_owned());
}
}
calls
}
fn requests() -> [ChatRequest; 2] {
[
ChatRequest::new(vec![ChatMessage::user("I am Alice, resting HR 47")]).with_model("m"),
ChatRequest::new(vec![ChatMessage::user("I am Bob, what did Alice say?")]).with_model("m"),
]
}
fn assert_never_resumed(dir: &Path) -> Vec<Vec<String>> {
let calls = assert_no_resume_args(dir);
assert!(
calls[1].iter().any(|a| a.contains("Bob")),
"the second call carries its own prompt: {:?}",
calls[1]
);
calls
}
fn assert_no_resume_args(dir: &Path) -> Vec<Vec<String>> {
let calls = recorded_calls(dir);
assert_eq!(calls.len(), 2, "expected two invocations, got {calls:?}");
for (i, argv) in calls.iter().enumerate() {
for flag in RESUME_FLAGS {
assert!(
!argv.iter().any(|a| a == flag),
"call {i} passed {flag}: {argv:?}"
);
}
assert!(
!argv.iter().any(|a| a.contains(SESSION_ID)),
"call {i} named the session a previous call opened: {argv:?}"
);
}
calls
}
fn assert_claude_isolated(argv: &[String]) {
assert!(
argv.iter().any(|a| a == "--no-session-persistence"),
"every call is an unsaved session: {argv:?}"
);
let sources = argv
.iter()
.position(|a| a == "--setting-sources")
.and_then(|i| argv.get(i + 1));
assert_eq!(
sources.map(String::as_str),
Some("user"),
"project and local settings from the shared cwd are never loaded: {argv:?}"
);
for line in [
"env:CLAUDE_CODE_DISABLE_AUTO_MEMORY=1",
"env:CLAUDE_CODE_DISABLE_CLAUDE_MDS=1",
] {
assert!(argv.iter().any(|a| a == line), "missing {line}: {argv:?}");
}
}
async fn complete_twice(runner: &dyn LlmProvider) -> Vec<String> {
let mut answers = Vec::new();
for request in &requests() {
let response = retry_text_file_busy(|| runner.complete(request)).await;
answers.push(response.unwrap().content);
}
answers
}
async fn stream_twice(runner: &dyn LlmProvider) -> Vec<String> {
let mut answers = Vec::new();
for request in &requests() {
let mut stream = retry_text_file_busy(|| runner.complete_stream(request))
.await
.unwrap();
let mut text = String::new();
while let Some(chunk) = stream.next().await {
if let Ok(chunk) = chunk {
text.push_str(&chunk.delta);
}
}
answers.push(text);
}
answers
}
#[tokio::test]
async fn claude_code_complete_never_resumes_and_shares_no_cwd_state() {
let dir = tempfile::tempdir().unwrap();
let doc = format!(r#"{{"result":"pong","is_error":false,"session_id":"{SESSION_ID}"}}"#);
let runner = ClaudeCodeRunner::new(RunnerConfig::new(fake_cli(dir.path(), &doc)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
for argv in assert_never_resumed(dir.path()) {
assert_claude_isolated(&argv);
}
}
#[tokio::test]
async fn claude_code_stream_never_resumes_and_shares_no_cwd_state() {
let dir = tempfile::tempdir().unwrap();
let lines = format!(
"{{\"type\":\"system\",\"subtype\":\"init\",\"session_id\":\"{SESSION_ID}\"}}\n\
{{\"type\":\"assistant\",\"message\":{{\"content\":[{{\"type\":\"text\",\"text\":\"pong\"}}]}},\"session_id\":\"{SESSION_ID}\"}}\n\
{{\"type\":\"result\",\"is_error\":false,\"result\":\"pong\",\"session_id\":\"{SESSION_ID}\"}}"
);
let runner = ClaudeCodeRunner::new(RunnerConfig::new(fake_cli(dir.path(), &lines)));
assert_eq!(stream_twice(&runner).await, ["pong", "pong"]);
for argv in assert_never_resumed(dir.path()) {
assert_claude_isolated(&argv);
}
}
#[tokio::test]
async fn claude_code_complete_then_stream_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let doc = format!(
r#"{{"type":"result","result":"pong","is_error":false,"session_id":"{SESSION_ID}"}}"#
);
let runner = ClaudeCodeRunner::new(RunnerConfig::new(fake_cli(dir.path(), &doc)));
let [alice, bob] = requests();
let answer = retry_text_file_busy(|| runner.complete(&alice)).await;
assert_eq!(answer.unwrap().content, "pong");
let mut stream = retry_text_file_busy(|| runner.complete_stream(&bob))
.await
.unwrap();
while stream.next().await.is_some() {}
for argv in assert_never_resumed(dir.path()) {
assert_claude_isolated(&argv);
}
}
#[tokio::test]
async fn cursor_agent_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let doc = format!(r#"{{"result":"pong","is_error":false,"session_id":"{SESSION_ID}"}}"#);
let runner = CursorAgentRunner::new(RunnerConfig::new(fake_cli(dir.path(), &doc)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn cursor_agent_stream_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let doc = format!(
r#"{{"type":"result","result":"pong","is_error":false,"session_id":"{SESSION_ID}"}}"#
);
let runner = CursorAgentRunner::new(RunnerConfig::new(fake_cli(dir.path(), &doc)));
stream_twice(&runner).await;
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn gemini_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let doc = format!(r#"{{"session_id":"{SESSION_ID}","response":"pong"}}"#);
let runner = GeminiCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), &doc)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn gemini_stream_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let lines = format!(
"{{\"type\":\"init\",\"session_id\":\"{SESSION_ID}\"}}\n\
{{\"type\":\"message\",\"role\":\"assistant\",\"content\":\"pong\",\"delta\":true}}\n\
{{\"type\":\"result\",\"status\":\"success\"}}"
);
let runner = GeminiCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), &lines)));
stream_twice(&runner).await;
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn goose_never_resumes_and_runs_without_a_session() {
let dir = tempfile::tempdir().unwrap();
let doc = r#"{"messages":[{"role":"assistant","content":[{"type":"text","text":"pong"}]}]}"#;
let runner = GooseCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), doc)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
for argv in assert_no_resume_args(dir.path()) {
assert!(argv.iter().any(|a| a == "--no-session"), "{argv:?}");
}
}
#[tokio::test]
async fn goose_stream_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let doc = r#"{"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"pong"}]}}"#;
let runner = GooseCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), doc)));
stream_twice(&runner).await;
for argv in assert_no_resume_args(dir.path()) {
assert!(argv.iter().any(|a| a == "--no-session"), "{argv:?}");
}
}
#[tokio::test]
async fn cline_never_resumes_a_task() {
let dir = tempfile::tempdir().unwrap();
let lines = format!(
"{{\"type\":\"task_started\",\"taskId\":\"{SESSION_ID}\"}}\n\
{{\"type\":\"say\",\"say\":\"completion_result\",\"text\":\"pong\"}}"
);
let runner = ClineCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), &lines)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn cline_stream_never_resumes_a_task() {
let dir = tempfile::tempdir().unwrap();
let lines = format!(
"{{\"type\":\"task_started\",\"taskId\":\"{SESSION_ID}\"}}\n\
{{\"type\":\"say\",\"say\":\"completion_result\",\"text\":\"pong\"}}"
);
let runner = ClineCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), &lines)));
stream_twice(&runner).await;
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn continue_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let runner = ContinueCliRunner::new(RunnerConfig::new(fake_cli(
dir.path(),
r#"{"response":"pong"}"#,
)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn continue_stream_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let runner = ContinueCliRunner::new(RunnerConfig::new(fake_cli(
dir.path(),
r#"{"response":"pong"}"#,
)));
assert_eq!(stream_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn kiro_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let runner = KiroCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), "pong")));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn kiro_stream_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let runner = KiroCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), "pong")));
assert_eq!(stream_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn kilo_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let lines = format!(
"{{\"type\":\"text\",\"sessionID\":\"{SESSION_ID}\",\"part\":{{\"type\":\"text\",\"text\":\"pong\"}}}}\n\
{{\"type\":\"step_finish\",\"sessionID\":\"{SESSION_ID}\",\"part\":{{\"type\":\"step-finish\",\"reason\":\"stop\"}}}}"
);
let runner = KiloCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), &lines)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn kilo_stream_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let lines = format!(
"{{\"type\":\"text\",\"sessionID\":\"{SESSION_ID}\",\"part\":{{\"type\":\"text\",\"text\":\"pong\"}}}}\n\
{{\"type\":\"step_finish\",\"sessionID\":\"{SESSION_ID}\",\"part\":{{\"type\":\"step-finish\",\"reason\":\"stop\"}}}}"
);
let runner = KiloCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), &lines)));
stream_twice(&runner).await;
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn opencode_never_resumes() {
let dir = tempfile::tempdir().unwrap();
let lines = format!(
"{{\"type\":\"text\",\"sessionID\":\"{SESSION_ID}\",\"part\":{{\"type\":\"text\",\"text\":\"pong\"}}}}\n\
{{\"type\":\"step_finish\",\"sessionID\":\"{SESSION_ID}\",\"part\":{{\"type\":\"step-finish\",\"reason\":\"stop\"}}}}"
);
let runner = OpenCodeRunner::new(RunnerConfig::new(fake_cli(dir.path(), &lines)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}
#[tokio::test]
async fn warp_never_resumes_a_conversation() {
let dir = tempfile::tempdir().unwrap();
let lines = format!(
"{{\"type\":\"system\",\"event_type\":\"conversation_started\",\"conversation_id\":\"{SESSION_ID}\"}}\n\
{{\"type\":\"agent\",\"text\":\"pong\"}}"
);
let runner = WarpCliRunner::new(RunnerConfig::new(fake_cli(dir.path(), &lines)));
assert_eq!(complete_twice(&runner).await, ["pong", "pong"]);
assert_never_resumed(dir.path());
}