use fs2::FileExt;
use std::io::Read;
use std::process::Stdio;
use std::sync::{Arc, Mutex, atomic::AtomicBool, mpsc};
use std::time::{Duration, Instant};
const READER_RESULT_TIMEOUT: Duration = Duration::from_secs(2);
const TIMEOUT_MARKER: &str = "ERROR: command timed out after ";
fn is_build_or_test_command(command: &str) -> bool {
let tokens: Vec<_> = command
.split(|ch: char| ch.is_ascii_whitespace() || matches!(ch, ';' | '&' | '|'))
.filter(|token| !token.is_empty())
.collect();
tokens.windows(2).any(|pair| {
let tool = std::path::Path::new(pair[0])
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(pair[0]);
(tool == "cargo"
&& matches!(
pair[1],
"build" | "check" | "clippy" | "test" | "run" | "bench" | "install" | "fast"
))
|| (matches!(tool, "npm" | "pnpm" | "yarn" | "bun")
&& matches!(pair[1], "test" | "build" | "check"))
|| (tool == "lean-ctx" && pair[1] == "dev-install")
|| (matches!(tool, "make" | "gmake")
&& matches!(
pair[1],
"build"
| "check"
| "dev"
| "install"
| "install-release"
| "test"
| "preflight"
| "preflight-fast"
))
}) || tokens.windows(3).any(|triple| {
matches!(triple[0], "npm" | "pnpm" | "yarn" | "bun")
&& triple[1] == "run"
&& matches!(triple[2], "test" | "build" | "check" | "lint" | "typecheck")
}) || tokens.iter().any(|token| {
std::path::Path::new(token)
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| matches!(name, "rustc" | "dev-install.sh" | "install.sh"))
})
}
fn acquire_build_lease(
command: &str,
cancel: Option<&AtomicBool>,
) -> Result<Option<std::fs::File>, String> {
let config = lean_ctx::core::config::Config::load();
if !config.agents.serialize_build_commands || !is_build_or_test_command(command) {
return Ok(None);
}
let dir = lean_ctx::core::data_dir::lean_ctx_data_dir()?.join("resources");
std::fs::create_dir_all(&dir).map_err(|error| error.to_string())?;
let path = dir.join("build.lock");
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)
.map_err(|error| format!("open build lease {}: {error}", path.display()))?;
loop {
match file.try_lock_exclusive() {
Ok(()) => return Ok(Some(file)),
Err(error) if lock_is_contended(&error) => {
if cancel.is_some_and(|flag| flag.load(std::sync::atomic::Ordering::Acquire)) {
return Err(
"build cancelled while waiting for the machine-wide slot".to_string()
);
}
std::thread::sleep(Duration::from_millis(100));
}
Err(error) => {
return Err(format!("acquire build lease {}: {error}", path.display()));
}
}
}
}
fn lock_is_contended(error: &std::io::Error) -> bool {
error.kind() == std::io::ErrorKind::WouldBlock
|| cfg!(windows) && matches!(error.raw_os_error(), Some(32 | 33))
}
pub(crate) fn output_before_timeout_marker(output: &str) -> Option<&str> {
output.find(TIMEOUT_MARKER).map(|idx| output[..idx].trim())
}
#[cfg(test)]
pub(crate) fn execute_command_in(command: &str, cwd: &str) -> (String, i32) {
execute_command_with_env(command, cwd, &std::collections::HashMap::new(), None)
}
#[cfg(test)]
pub(crate) fn execute_command_with_env(
command: &str,
cwd: &str,
extra_env: &std::collections::HashMap<String, String>,
timeout_ms: Option<u64>,
) -> (String, i32) {
execute_command_with_env_cancellable(command, cwd, extra_env, timeout_ms, None, false, None)
}
pub(crate) struct ShellExecution {
pub output: String,
pub exit_code: i32,
pub completeness: lean_ctx::core::context_admission::OutputCompleteness,
pub notice: Option<&'static str>,
}
pub(crate) fn with_notice(notice: Option<&'static str>, text: String) -> String {
match notice {
Some(notice) => format!("{notice}\n{text}"),
None => text,
}
}
#[cfg(unix)]
fn split_notice(text: String) -> (Option<&'static str>, String) {
match leading_notice(&text) {
Some(notice) => {
let rest = text.get(notice.len() + 1..).unwrap_or_default().to_string();
(Some(notice), rest)
}
None => (None, text),
}
}
#[cfg(unix)]
pub(crate) fn leading_notice(text: &str) -> Option<&'static str> {
use lean_ctx::shell::containment::{END_NOTICE, HELPER_NOTICE, RELEASE_NOTICE, UNCLEAR_NOTICE};
[HELPER_NOTICE, END_NOTICE, UNCLEAR_NOTICE, RELEASE_NOTICE]
.into_iter()
.find(|notice| {
text.strip_prefix(notice)
.is_some_and(|rest| rest.is_empty() || rest.starts_with('\n'))
})
}
#[cfg(not(unix))]
pub(crate) fn leading_notice(_text: &str) -> Option<&'static str> {
None
}
pub(crate) fn keep_notice(notice: Option<&'static str>, text: String) -> String {
if leading_notice(&text).is_some() {
text
} else {
with_notice(notice, text)
}
}
impl ShellExecution {
fn complete(output: String, exit_code: i32) -> Self {
Self {
output,
exit_code,
completeness: lean_ctx::core::context_admission::OutputCompleteness::complete(),
notice: None,
}
}
#[cfg(unix)]
fn unknown_capture(output: String, exit_code: i32) -> Self {
Self {
output,
exit_code,
completeness: lean_ctx::core::context_admission::OutputCompleteness {
reader_incomplete: true,
..lean_ctx::core::context_admission::OutputCompleteness::complete()
},
notice: None,
}
}
}
#[cfg(unix)]
pub fn register_protected_shell_runner() {
lean_ctx::core::protected_execution::register_shell_runner(
execute_command_with_env_cancellable,
);
}
#[cfg(any(unix, test))]
pub(crate) fn execute_command_with_env_cancellable(
command: &str,
cwd: &str,
extra_env: &std::collections::HashMap<String, String>,
timeout_ms: Option<u64>,
cancel: Option<&AtomicBool>,
idle_keyed: bool,
live: Option<&std::sync::Mutex<String>>,
) -> (String, i32) {
let _ = live;
let execution = execute_command_with_env_cancellable_captured(
command, cwd, extra_env, timeout_ms, cancel, idle_keyed,
);
(
with_notice(execution.notice, execution.output),
execution.exit_code,
)
}
pub(crate) fn execute_command_with_env_captured(
command: &str,
cwd: &str,
extra_env: &std::collections::HashMap<String, String>,
timeout_ms: Option<u64>,
) -> ShellExecution {
execute_command_with_env_cancellable_captured(command, cwd, extra_env, timeout_ms, None, false)
}
pub(crate) fn execute_command_with_env_cancellable_captured(
command: &str,
cwd: &str,
extra_env: &std::collections::HashMap<String, String>,
timeout_ms: Option<u64>,
cancel: Option<&AtomicBool>,
idle_keyed: bool,
) -> ShellExecution {
#[cfg(unix)]
let protected_prefix: Option<Vec<String>> = {
use lean_ctx::core::protected_execution::{Mode, launch_mode, sandbox_prefix};
match launch_mode() {
Err(error) => return ShellExecution::complete(format!("ERROR: {error}"), 1),
Ok(Mode::Brokered(client)) => {
let (output, exit_code) = client.shell(
command, cwd, extra_env, timeout_ms, cancel, idle_keyed, None,
);
let (notice, output) = split_notice(output);
return ShellExecution {
notice,
..ShellExecution::unknown_capture(output, exit_code)
};
}
Ok(Mode::Sandboxed(profile)) => Some(sandbox_prefix(&profile)),
Ok(Mode::Direct) => None,
}
};
#[cfg(not(unix))]
let protected_prefix = match lean_ctx::core::protected_store::protected_child_prefix() {
Ok(prefix) => prefix,
Err(error) => return ShellExecution::complete(format!("ERROR: {error}"), 1),
};
let _build_lease = match acquire_build_lease(command, cancel) {
Ok(lease) => lease,
Err(error) => return ShellExecution::complete(format!("ERROR: {error}"), 130),
};
let resource_config = lean_ctx::core::config::Config::load().agents;
let (shell, flag) = lean_ctx::shell::shell_and_flag();
let normalized_cmd = crate::tools::ctx_shell::normalize_command_for_shell(command);
let normalized_cmd = lean_ctx::shell::platform::zsh_safe_command(&normalized_cmd, &shell);
let dir = std::path::Path::new(cwd);
let mut cmd = std::process::Command::new(&shell);
if let Some([program, arguments @ ..]) = protected_prefix.as_deref() {
cmd = std::process::Command::new(program);
cmd.args(arguments).arg(&shell);
}
if cfg!(windows) && lean_ctx::shell::platform::is_powershell(&shell) {
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass"]);
}
cmd.arg(&flag)
.arg(&normalized_cmd)
.env("GIT_TERMINAL_PROMPT", "0")
.stdin(Stdio::null());
lean_ctx::shell::reentry::mark_child(&mut cmd);
if !extra_env.contains_key("GIT_PAGER") {
cmd.env("GIT_PAGER", "cat");
}
if !extra_env.contains_key("PAGER") {
cmd.env("PAGER", "cat");
}
ensure_utf8_locale(&mut cmd, extra_env);
lean_ctx::shell::platform::apply_profile_free_env(&mut cmd);
let mut forwarded: std::collections::HashSet<String> = std::collections::HashSet::new();
for (key, val) in std::env::vars() {
if lean_ctx::core::agent_runtime_env::is_forwardable(&key) {
cmd.env(&key, &val);
forwarded.insert(key);
}
}
for (key, val) in lean_ctx::core::agent_runtime_env::load() {
if !forwarded.contains(&key) {
cmd.env(&key, &val);
}
}
for (key, val) in extra_env {
cmd.env(key, val);
}
if is_build_or_test_command(command) {
if resource_config.cargo_build_jobs > 0 && !extra_env.contains_key("CARGO_BUILD_JOBS") {
cmd.env(
"CARGO_BUILD_JOBS",
resource_config.cargo_build_jobs.to_string(),
);
}
if resource_config.shared_cargo_target
&& !extra_env.contains_key("CARGO_TARGET_DIR")
&& let Ok(data_dir) = lean_ctx::core::data_dir::lean_ctx_data_dir()
{
cmd.env(
"CARGO_TARGET_DIR",
data_dir.join("build-cache/cargo-target"),
);
}
}
if dir.is_dir() {
cmd.current_dir(dir);
} else {
return ShellExecution::complete(
format!("ERROR: working directory does not exist or is not a directory: {cwd}"),
1,
);
}
let cap = lean_ctx::core::limits::max_shell_bytes();
lean_ctx::shell::process_tree::ProcessTree::prepare(&mut cmd);
let started = match lean_ctx::shell::process_tree::ShellChild::start(&mut cmd) {
Ok(started) => started,
Err(e) => return ShellExecution::complete(format!("ERROR: {e}"), 1),
};
let mut child = started.child;
let (out_buf, out_done) = spawn_capture(started.stdout, cap);
let (err_buf, err_done) = spawn_capture(started.stderr, cap);
let timeout = command_timeout(command, timeout_ms);
let start = Instant::now();
let hard_deadline = idle_keyed.then(|| start + streaming_max_lifetime());
let mut last_len = 0usize;
let mut last_output_at = start;
let mut still_running: Vec<String> = Vec::new();
let (code, timed_out, cancelled) = loop {
match child.try_wait() {
Ok(Some(status)) => {
break (
lean_ctx::shell::exit_status::exit_code(status),
false,
false,
);
}
Ok(None) => {
if cancel.is_some_and(|signal| signal.load(std::sync::atomic::Ordering::Acquire)) {
child.kill();
child.wait();
break (130, false, true);
}
let idle_for = if idle_keyed {
let len = captured_len(&out_buf) + captured_len(&err_buf);
if len != last_len {
last_len = len;
last_output_at = Instant::now();
}
last_output_at.elapsed()
} else {
start.elapsed()
};
if idle_for >= timeout || hard_deadline.is_some_and(|d| Instant::now() >= d) {
still_running = running_segments(child.id(), &normalized_cmd);
child.kill();
child.wait();
break (124, true, false);
}
std::thread::sleep(Duration::from_millis(25));
}
Err(_) => {
child.kill();
break (1, false, false);
}
}
};
let lost_notice = child.lost_notice();
let release_notice = child.release();
let containment_notice = started.notice.or(lost_notice).or(release_notice);
let reader_deadline = Instant::now() + READER_RESULT_TIMEOUT;
let out_complete = wait_for_reader(&out_done, reader_deadline);
let err_complete = wait_for_reader(&err_done, reader_deadline);
let (out_bytes, out_trunc, out_read_failed) = snapshot(&out_buf);
let (err_bytes, err_trunc, err_read_failed) = snapshot(&err_buf);
let reader_incomplete = !out_complete || !err_complete || out_read_failed || err_read_failed;
let decode_lossy =
std::str::from_utf8(&out_bytes).is_err() || std::str::from_utf8(&err_bytes).is_err();
let stdout =
lean_ctx::shell::resolve_carriage_returns(&lean_ctx::shell::decode_output(&out_bytes));
let stderr =
lean_ctx::shell::resolve_carriage_returns(&lean_ctx::shell::decode_output(&err_bytes));
let mut text = lean_ctx::shell::combine_streams(&stdout, &stderr, code);
if out_trunc || err_trunc {
text.push_str(&format!(
"\n[truncated: cap={}B stdout={}B stderr={}B]",
cap,
out_bytes.len(),
err_bytes.len()
));
}
if reader_incomplete && !timed_out {
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
text.push_str(&format!(
"[lean-ctx: output reader still draining after {}s — a background process is likely \
holding the pipe open; output above may be partial]",
READER_RESULT_TIMEOUT.as_secs()
));
}
if timed_out {
if !text.ends_with('\n') && !text.is_empty() {
text.push('\n');
}
text.push_str(&format!(
"{TIMEOUT_MARKER}{}ms{}",
timeout.as_millis(),
if idle_keyed {
" without new output"
} else {
""
}
));
if !still_running.is_empty() {
text.push_str(&format!(
"\n[still running at timeout: {}]",
still_running.join("; ")
));
}
}
if let Some(hint) = super::walk_hint::walk_hint(command, cwd) {
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
text.push_str(&hint);
}
if cancelled {
if !text.ends_with('\n') && !text.is_empty() {
text.push('\n');
}
text.push_str("[cancelled: command stopped on request]");
}
ShellExecution {
output: text,
exit_code: code,
notice: containment_notice,
completeness: lean_ctx::core::context_admission::OutputCompleteness {
truncated: out_trunc || err_trunc,
timed_out,
reader_incomplete: reader_incomplete || lost_notice.is_some(),
cancelled,
decode_lossy,
},
}
}
#[derive(Default)]
struct CaptureBuf {
bytes: Vec<u8>,
truncated: bool,
read_failed: bool,
}
fn spawn_capture<R: Read + Send + 'static>(
pipe: Option<R>,
cap: usize,
) -> (Arc<Mutex<CaptureBuf>>, mpsc::Receiver<()>) {
let shared = Arc::new(Mutex::new(CaptureBuf::default()));
let (done_tx, done_rx) = mpsc::channel();
let writer = Arc::clone(&shared);
std::thread::spawn(move || {
if let Some(mut r) = pipe {
let mut buf = [0u8; 8192];
loop {
match r.read(&mut buf) {
Ok(0) => break,
Err(ref error) if error.kind() == std::io::ErrorKind::Interrupted => {}
Err(_) => {
writer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.read_failed = true;
break;
}
Ok(n) => {
let mut s = writer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if s.bytes.len() < cap {
let remaining = cap - s.bytes.len();
let take = remaining.min(n);
s.bytes.extend_from_slice(&buf[..take]);
if take < n {
s.truncated = true;
}
} else {
s.truncated = true;
}
}
}
}
}
let _ = done_tx.send(());
});
(shared, done_rx)
}
fn snapshot(buf: &Arc<Mutex<CaptureBuf>>) -> (Vec<u8>, bool, bool) {
let s = buf
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(s.bytes.clone(), s.truncated, s.read_failed)
}
fn captured_len(buf: &Arc<Mutex<CaptureBuf>>) -> usize {
buf.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.bytes
.len()
}
fn streaming_max_lifetime() -> Duration {
Duration::from_millis(
std::env::var("LEAN_CTX_SHELL_BG_MAX_MS")
.ok()
.and_then(|v| v.parse().ok())
.filter(|&ms: &u64| ms > 0)
.unwrap_or(3_600_000),
)
}
#[cfg(unix)]
struct LiveProc {
pid: u32,
ppid: u32,
args: String,
}
#[cfg(unix)]
fn running_segments(pgid: u32, command: &str) -> Vec<String> {
let Ok(out) = std::process::Command::new("ps")
.args(["-A", "-o", "pid=,ppid=,pgid=,args="])
.output()
else {
return Vec::new();
};
let needle = command.split_whitespace().collect::<Vec<_>>().join(" ");
let live: Vec<LiveProc> = String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| parse_ps_row(line, pgid, &needle))
.collect();
describe_live(&live)
}
#[cfg(unix)]
fn describe_live(live: &[LiveProc]) -> Vec<String> {
let pids: std::collections::HashSet<u32> = live.iter().map(|p| p.pid).collect();
live.iter()
.filter(|p| !pids.contains(&p.ppid))
.map(|parent| {
let kids: Vec<&str> = live
.iter()
.filter(|c| c.ppid == parent.pid)
.map(|c| c.args.as_str())
.collect();
if kids.is_empty() {
parent.args.clone()
} else {
format!("{} (spawned: {})", parent.args, kids.join(", "))
}
})
.collect()
}
#[cfg(unix)]
fn parse_ps_row(line: &str, pgid: u32, needle: &str) -> Option<LiveProc> {
let rest = line.trim_start();
let (pid, rest) = rest.split_once(char::is_whitespace)?;
let rest = rest.trim_start();
let (ppid, rest) = rest.split_once(char::is_whitespace)?;
let args = rest
.trim_start()
.split_once(char::is_whitespace)
.and_then(|(row_pgid, args)| {
(row_pgid.parse::<u32>().ok()? == pgid).then_some(args.trim())
})?;
if args.is_empty() || args.contains(needle) {
return None;
}
Some(LiveProc {
pid: pid.parse().ok()?,
ppid: ppid.parse().ok()?,
args: args.chars().take(200).collect(),
})
}
#[cfg(not(unix))]
fn running_segments(_pgid: u32, _command: &str) -> Vec<String> {
Vec::new()
}
fn wait_for_reader(done: &mpsc::Receiver<()>, deadline: Instant) -> bool {
let remaining = deadline.saturating_duration_since(Instant::now());
done.recv_timeout(remaining).is_ok()
}
fn ensure_utf8_locale(
cmd: &mut std::process::Command,
extra_env: &std::collections::HashMap<String, String>,
) {
if extra_env.contains_key("LC_ALL") || extra_env.contains_key("LC_CTYPE") {
return;
}
lean_ctx::shell::platform::apply_utf8_locale(cmd);
}
fn command_timeout(command: &str, timeout_ms: Option<u64>) -> Duration {
lean_ctx::shell::shell_timeout_with_override(command, timeout_ms)
}
#[cfg(all(test, target_os = "macos"))]
#[path = "execute_supervised_tests.rs"]
mod supervised_tests;
#[cfg(all(test, unix))]
#[path = "execute_containment_tests.rs"]
mod containment_tests;
#[cfg(test)]
mod tests {
use super::{
command_timeout, ensure_utf8_locale, execute_command_in, is_build_or_test_command,
};
#[test]
fn resource_broker_build_slot_classifier_is_precise() {
for command in [
"cargo test --lib",
"cargo fast",
"cd rust && cargo fast --features dev-tools",
"make dev",
"make build",
"make preflight-fast",
"make install-release",
"./install.sh --build-only",
"bash rust/dev-install.sh --fast",
"./rust/dev-install.sh --release",
"lean-ctx dev-install",
"/opt/bin/lean-ctx dev-install --fast",
"cd rust && cargo clippy --all-features",
"/opt/bin/cargo build --release",
"npm test",
"npm run typecheck",
"pnpm build",
"rustc main.rs",
] {
assert!(is_build_or_test_command(command), "{command}");
}
for command in [
"git status",
"cargo metadata",
"make help",
"lean-ctx --version",
"npm view lean-ctx",
"rg cargo",
] {
assert!(!is_build_or_test_command(command), "{command}");
}
}
#[test]
fn resource_broker_serializes_build_leases() {
let isolation = lean_ctx::core::data_dir::isolated_data_dir();
assert!(
super::acquire_build_lease("cargo test", None)
.expect("lease")
.is_none()
);
std::fs::write(
isolation.path().join("config.toml"),
"[agents]\nserialize_build_commands = true\n",
)
.expect("opt in");
let first = super::acquire_build_lease("cargo test first", None)
.expect("first lease")
.expect("build command gets lease");
let acquired = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let worker_acquired = std::sync::Arc::clone(&acquired);
let worker = std::thread::spawn(move || {
let _second = super::acquire_build_lease("cargo test second", None)
.expect("second lease")
.expect("build command gets lease");
worker_acquired.store(true, std::sync::atomic::Ordering::Release);
});
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(!acquired.load(std::sync::atomic::Ordering::Acquire));
drop(first);
worker.join().expect("lease waiter");
assert!(acquired.load(std::sync::atomic::Ordering::Acquire));
}
#[test]
fn command_timeout_delegates_to_shell_timeout() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let saved = std::env::var("LEAN_CTX_SHELL_TIMEOUT_MS").ok();
lean_ctx::test_env::remove_var("LEAN_CTX_SHELL_TIMEOUT_MS");
assert!(
command_timeout("cargo install --path .", None) > command_timeout("git status", None)
);
lean_ctx::test_env::set_var("LEAN_CTX_SHELL_TIMEOUT_MS", "5000");
assert_eq!(
command_timeout("cargo install --path .", None),
std::time::Duration::from_secs(5)
);
assert_eq!(
command_timeout("git status", None),
std::time::Duration::from_secs(5)
);
lean_ctx::test_env::remove_var("LEAN_CTX_SHELL_TIMEOUT_MS");
if let Some(v) = saved {
lean_ctx::test_env::set_var("LEAN_CTX_SHELL_TIMEOUT_MS", v);
}
}
#[cfg(target_os = "macos")]
#[test]
fn the_supervisor_runs_the_shell_under_the_pinned_child_rules() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let isolation = lean_ctx::core::data_dir::isolated_data_dir();
let fact = isolation.path().join("knowledge/knowledge.json");
std::fs::create_dir_all(fact.parent().unwrap()).unwrap();
std::fs::write(&fact, "synthetic-provider-fact").unwrap();
let work = tempfile::tempdir().unwrap();
let cwd = work.path().to_str().unwrap();
let read_fact = format!("cat {}", fact.display());
let write_fact = format!("printf downgraded > {}", fact.display());
lean_ctx::core::protected_store::unpin_synthetic_session();
let (text, code) = execute_command_in(&read_fact, cwd);
assert_eq!(code, 0, "{text}");
assert!(text.contains("synthetic-provider-fact"), "{text}");
let _session = lean_ctx::core::protected_store::pin_synthetic_session(isolation.path())
.expect("pinned rules");
lean_ctx::core::protected_execution::with_test_supervisor(|| {
let (text, code) = execute_command_in(&read_fact, cwd);
assert_ne!(code, 0, "{text}");
assert!(!text.contains("synthetic-provider-fact"), "{text}");
let (text, code) = execute_command_in(&write_fact, cwd);
assert_ne!(code, 0, "{text}");
let (text, code) =
execute_command_in("printf 'fn main() {}' > main.rs && cat main.rs", cwd);
assert_eq!(code, 0, "{text}");
assert!(text.contains("fn main()"), "{text}");
});
assert_eq!(
std::fs::read_to_string(&fact).unwrap(),
"synthetic-provider-fact"
);
std::fs::write(&fact, "engine-write").unwrap();
lean_ctx::test_env::remove_var(lean_ctx::core::protected_store::CHILD_PROFILE_ENV);
let (text, code) = execute_command_in("printf ran-unconfined", cwd);
assert_ne!(code, 0, "{text}");
assert!(text.starts_with("ERROR:"), "{text}");
assert!(!text.contains("ran-unconfined"), "{text}");
}
#[test]
#[cfg_attr(windows, ignore)]
fn idle_keyed_timeout_survives_a_trickling_loop() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
lean_ctx::test_env::remove_var("LEAN_CTX_SHELL_TIMEOUT_MS");
let env = std::collections::HashMap::new();
let cmd = "for i in 1 2 3 4 5 6 7 8 9; do printf T; sleep 0.1; done; printf DONE";
let (idle_out, idle_code) = super::execute_command_with_env_cancellable(
cmd,
".",
&env,
Some(400),
None,
true,
None,
);
assert_eq!(
idle_code, 0,
"output kept resetting the idle clock: {idle_out}"
);
assert!(idle_out.contains("DONE"));
let (wall_out, wall_code) = super::execute_command_with_env_cancellable(
cmd,
".",
&env,
Some(400),
None,
false,
None,
);
assert_eq!(wall_code, 124, "wall-clock mode must still enforce the cap");
assert!(wall_out.contains("timed out"));
}
#[test]
#[cfg_attr(windows, ignore)]
fn timeout_names_the_still_running_segment() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
lean_ctx::test_env::remove_var("LEAN_CTX_SHELL_TIMEOUT_MS");
let env = std::collections::HashMap::new();
let (out, code) = super::execute_command_with_env_cancellable(
"printf FIRST_OK; sleep 47",
".",
&env,
Some(600),
None,
false,
None,
);
assert_eq!(code, 124);
assert!(
out.contains("FIRST_OK"),
"partial output must survive: {out}"
);
assert!(
out.contains("still running at timeout") && out.contains("47"),
"the hung segment must be named: {out}"
);
}
#[test]
#[cfg(unix)]
fn ps_row_names_segments_and_drops_the_whole_command() {
let whole = "printf hi; grep -rn needle /repo";
assert_eq!(
super::parse_ps_row(" 4242 4200 4200 grep -rn needle /repo", 4200, whole)
.map(|p| p.args),
Some("grep -rn needle /repo".to_string()),
"a segment must be named"
);
assert!(
super::parse_ps_row(
"4200 1 4200 /bin/sh -c printf hi; grep -rn needle /repo",
4200,
whole
)
.is_none()
);
assert!(super::parse_ps_row("4242 4200 9999 sleep 5", 4200, whole).is_none());
assert!(super::parse_ps_row("garbage", 4200, whole).is_none());
}
#[test]
#[cfg(unix)]
fn a_spawned_child_is_labelled_not_presented_as_a_stage() {
let live = vec![
super::LiveProc {
pid: 100,
ppid: 1,
args: "go run ./ --template x".into(),
},
super::LiveProc {
pid: 101,
ppid: 100,
args: "/var/folders/T/go-build/b001/exe/evcc --template x".into(),
},
];
let out = super::describe_live(&live);
assert_eq!(out.len(), 1, "the child is not a peer entry: {out:?}");
assert!(out[0].starts_with("go run ./"), "{out:?}");
assert!(
out[0].contains("(spawned: /var/folders/T/go-build/b001/exe/evcc"),
"the descendant is named as one: {out:?}"
);
}
#[test]
#[cfg(unix)]
fn genuine_pipeline_siblings_both_stay_top_level() {
let live = vec![
super::LiveProc {
pid: 200,
ppid: 50,
args: "grep -rn needle /repo".into(),
},
super::LiveProc {
pid: 201,
ppid: 50,
args: "head -30".into(),
},
];
let mut out = super::describe_live(&live);
out.sort();
assert_eq!(out, vec!["grep -rn needle /repo", "head -30"]);
}
#[test]
#[cfg(unix)]
fn the_timeout_line_does_not_imply_a_pipeline() {
let joined = ["a".to_string(), "b".to_string()].join("; ");
assert!(!joined.contains('|'), "no pipe may be invented: {joined}");
}
#[test]
fn ensure_utf8_locale_sets_fallback_when_none_inherited() {
let _env_lock = lean_ctx::core::data_dir::test_env_lock();
let empty: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let mut cmd = std::process::Command::new("true");
let saved = (
std::env::var("LC_ALL").ok(),
std::env::var("LC_CTYPE").ok(),
std::env::var("LANG").ok(),
);
lean_ctx::test_env::remove_var("LC_ALL");
lean_ctx::test_env::remove_var("LC_CTYPE");
lean_ctx::test_env::remove_var("LANG");
ensure_utf8_locale(&mut cmd, &empty);
if let Some(v) = saved.0 {
lean_ctx::test_env::set_var("LC_ALL", v);
}
if let Some(v) = saved.1 {
lean_ctx::test_env::set_var("LC_CTYPE", v);
}
if let Some(v) = saved.2 {
lean_ctx::test_env::set_var("LANG", v);
}
}
#[test]
fn ensure_utf8_locale_skips_when_extra_env_has_lc_all() {
let mut extra = std::collections::HashMap::new();
extra.insert("LC_ALL".to_string(), "C".to_string());
let mut cmd = std::process::Command::new("true");
ensure_utf8_locale(&mut cmd, &extra);
}
#[test]
#[cfg_attr(windows, ignore)]
fn utf8_bytes_survive_shell_roundtrip() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let (output, code) = execute_command_in(
"printf '\\xD0\\x9F\\xD1\\x80\\xD0\\xB8\\xD0\\xB2\\xD0\\xB5\\xD1\\x82'",
".",
);
assert_eq!(code, 0, "printf failed: {output}");
assert_eq!(output, "Привет", "Cyrillic bytes must survive roundtrip");
}
#[test]
#[cfg_attr(windows, ignore)] fn execute_command_closes_stdin() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let command = "sh -c 'if read -t 1 line; then echo 67890; else echo 12345; fi'";
let (output, code) = execute_command_in(command, ".");
assert_eq!(code, 0, "command failed: {output}");
assert!(
output.contains("12345"),
"child process should receive EOF on stdin, got: {output}"
);
}
#[test]
#[cfg(unix)]
fn execute_command_reports_signal_termination_as_128_plus_signal() {
let (output, code) = execute_command_in("echo before-kill; kill -TERM $$", ".");
assert_eq!(code, 143, "SIGTERM must map to 143, got {code}: {output}");
assert!(output.contains("before-kill"), "output kept: {output}");
let (_, code) = execute_command_in("kill -KILL $$", ".");
assert_eq!(code, 137, "SIGKILL must map to 137");
}
#[test]
#[cfg_attr(windows, ignore)] fn background_pipe_holder_keeps_foreground_output() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let (output, code) = execute_command_in("echo REPRO_CANARY_945; sleep 4 &", ".");
assert_eq!(code, 0, "command should succeed: {output:?}");
assert!(
output.contains("REPRO_CANARY_945"),
"foreground stdout must survive a lingering background pipe holder, got: {output:?}"
);
assert!(
output.contains("output reader still draining"),
"an incomplete reader must be flagged, got: {output:?}"
);
}
#[test]
#[cfg(unix)]
fn command_has_no_controlling_terminal() {
let (output, code) = execute_command_in(
"if (exec 3</dev/tty) 2>/dev/null; then echo HAS_TTY; else echo NO_TTY; fi",
".",
);
assert_eq!(code, 0, "{output:?}");
assert!(output.contains("NO_TTY"), "got: {output:?}");
}
#[test]
#[cfg_attr(windows, ignore)]
fn forwards_captured_agent_runtime_env() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
lean_ctx::test_env::remove_var("CODEX_THREAD_ID");
lean_ctx::test_env::set_var("CODEX_THREAD_ID", "thread-from-hook");
lean_ctx::core::agent_runtime_env::capture();
lean_ctx::test_env::remove_var("CODEX_THREAD_ID");
let (output, code) = execute_command_in("printf 'TID=%s' \"$CODEX_THREAD_ID\"", ".");
assert_eq!(code, 0, "command failed: {output}");
assert!(
output.contains("TID=thread-from-hook"),
"captured agent runtime var must be forwarded, got: {output}"
);
}
#[test]
#[cfg_attr(windows, ignore)] fn per_call_timeout_kills_long_command() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let (output, code) = super::execute_command_with_env(
"sleep 3",
".",
&std::collections::HashMap::new(),
Some(200),
);
assert_eq!(code, 124, "timed-out command must exit 124: {output}");
assert!(
output.contains("timed out after 200ms"),
"timeout message must carry the per-call budget, got: {output}"
);
}
#[test]
#[cfg_attr(windows, ignore)] fn per_call_timeout_preserves_partial_output() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let (output, code) = super::execute_command_with_env(
"printf TIMEOUT_PARTIAL_995; sleep 3",
".",
&std::collections::HashMap::new(),
Some(200),
);
assert_eq!(code, 124, "timed-out command must exit 124: {output}");
assert!(
output.contains("TIMEOUT_PARTIAL_995"),
"stdout emitted before timeout must be preserved: {output:?}"
);
assert!(
output.contains("timed out after 200ms"),
"timeout notice must remain explicit: {output:?}"
);
}
#[test]
fn git_version_returns_when_git_is_available() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let git_available = std::process::Command::new("git")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok();
if !git_available {
return;
}
let (output, code) = execute_command_in("git --version", ".");
assert_eq!(code, 0, "git command failed: {output}");
assert!(
output.to_ascii_lowercase().contains("git version"),
"unexpected git output: {output}"
);
}
}