use super::*;
struct RuntimePathsEnvGuard {
state: Option<String>,
run: Option<String>,
worktree: Option<String>,
}
impl RuntimePathsEnvGuard {
fn capture() -> Self {
Self {
state: std::env::var(crate::runtime_paths::HARN_STATE_DIR_ENV).ok(),
run: std::env::var(crate::runtime_paths::HARN_RUN_DIR_ENV).ok(),
worktree: std::env::var(crate::runtime_paths::HARN_WORKTREE_DIR_ENV).ok(),
}
}
}
impl Drop for RuntimePathsEnvGuard {
fn drop(&mut self) {
match self.state.as_deref() {
Some(value) => std::env::set_var(crate::runtime_paths::HARN_STATE_DIR_ENV, value),
None => std::env::remove_var(crate::runtime_paths::HARN_STATE_DIR_ENV),
}
match self.run.as_deref() {
Some(value) => std::env::set_var(crate::runtime_paths::HARN_RUN_DIR_ENV, value),
None => std::env::remove_var(crate::runtime_paths::HARN_RUN_DIR_ENV),
}
match self.worktree.as_deref() {
Some(value) => {
std::env::set_var(crate::runtime_paths::HARN_WORKTREE_DIR_ENV, value);
}
None => std::env::remove_var(crate::runtime_paths::HARN_WORKTREE_DIR_ENV),
}
}
}
#[test]
fn lexically_collapse_resolves_sibling_walk() {
let path = PathBuf::from("/tmp/project/tests/../fixtures/x.json");
let collapsed = lexically_collapse(&path).expect("sibling walk");
assert_eq!(collapsed, PathBuf::from("/tmp/project/fixtures/x.json"));
}
#[test]
fn lexically_collapse_blocks_escape_past_root() {
let path = PathBuf::from("/app/../../etc/passwd");
assert!(lexically_collapse(&path).is_none());
}
#[test]
fn lexically_collapse_strips_curdir() {
let path = PathBuf::from("/app/./logs/today.txt");
let collapsed = lexically_collapse(&path).expect("curdir is benign");
assert_eq!(collapsed, PathBuf::from("/app/logs/today.txt"));
}
#[test]
fn resolve_source_relative_path_blocks_obvious_escape() {
let dir = std::env::temp_dir().join(format!("harn-process-escape-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&dir).unwrap();
set_thread_source_dir(&dir);
set_thread_execution_context(Some(crate::orchestration::RunExecutionRecord {
cwd: Some(dir.to_string_lossy().into_owned()),
project_root: None,
source_dir: Some(dir.to_string_lossy().into_owned()),
env: BTreeMap::new(),
adapter: None,
repo_path: None,
worktree_path: None,
branch: None,
base_ref: None,
cleanup: None,
grants: Vec::new(),
}));
let resolved = resolve_source_relative_path("../../../../../../../../etc/passwd");
assert!(
resolved
.to_string_lossy()
.contains("__harn_rejected_parent_dir_traversal__"),
"expected rejection sentinel, got {resolved:?}"
);
reset_process_state();
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn resolve_source_relative_path_ignores_thread_source_dir_without_execution_context() {
let dir = std::env::temp_dir().join(format!("harn-process-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&dir).unwrap();
let current_dir = std::env::current_dir().unwrap();
set_thread_source_dir(&dir);
let resolved = resolve_source_relative_path("templates/prompt.txt");
assert_eq!(resolved, current_dir.join("templates/prompt.txt"));
reset_process_state();
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn resolve_source_relative_path_prefers_execution_cwd_over_source_dir() {
let cwd = std::env::temp_dir().join(format!("harn-process-cwd-{}", uuid::Uuid::now_v7()));
let source_dir =
std::env::temp_dir().join(format!("harn-process-source-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&cwd).unwrap();
std::fs::create_dir_all(&source_dir).unwrap();
set_thread_source_dir(&source_dir);
set_thread_execution_context(Some(crate::orchestration::RunExecutionRecord {
cwd: Some(cwd.to_string_lossy().into_owned()),
project_root: None,
source_dir: Some(source_dir.to_string_lossy().into_owned()),
env: BTreeMap::new(),
adapter: None,
repo_path: None,
worktree_path: None,
branch: None,
base_ref: None,
cleanup: None,
grants: Vec::new(),
}));
let resolved = resolve_source_relative_path("templates/prompt.txt");
assert_eq!(resolved, cwd.join("templates/prompt.txt"));
reset_process_state();
let _ = std::fs::remove_dir_all(&cwd);
let _ = std::fs::remove_dir_all(&source_dir);
}
#[test]
fn resolve_source_asset_path_prefers_execution_source_dir_over_cwd() {
let cwd = std::env::temp_dir().join(format!("harn-asset-cwd-{}", uuid::Uuid::now_v7()));
let source_dir =
std::env::temp_dir().join(format!("harn-asset-source-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&cwd).unwrap();
std::fs::create_dir_all(&source_dir).unwrap();
set_thread_source_dir(&source_dir);
set_thread_execution_context(Some(crate::orchestration::RunExecutionRecord {
cwd: Some(cwd.to_string_lossy().into_owned()),
project_root: None,
source_dir: Some(source_dir.to_string_lossy().into_owned()),
env: BTreeMap::new(),
adapter: None,
repo_path: None,
worktree_path: None,
branch: None,
base_ref: None,
cleanup: None,
grants: Vec::new(),
}));
let resolved = resolve_source_asset_path("templates/prompt.txt");
assert_eq!(resolved, source_dir.join("templates/prompt.txt"));
reset_process_state();
let _ = std::fs::remove_dir_all(&cwd);
let _ = std::fs::remove_dir_all(&source_dir);
}
#[test]
fn set_thread_source_dir_absolutizes_relative_paths() {
reset_process_state();
let current_dir = std::env::current_dir().unwrap();
set_thread_source_dir(std::path::Path::new("scripts"));
assert_eq!(source_root_path(), current_dir.join("scripts"));
reset_process_state();
}
#[test]
fn project_root_builtin_prefers_explicit_execution_project_root() {
let cwd = std::env::temp_dir().join(format!("harn-process-cwd-{}", uuid::Uuid::now_v7()));
let project_root =
std::env::temp_dir().join(format!("harn-process-root-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&cwd).unwrap();
std::fs::create_dir_all(&project_root).unwrap();
set_thread_execution_context(Some(RunExecutionRecord {
cwd: Some(cwd.to_string_lossy().into_owned()),
project_root: Some(project_root.to_string_lossy().into_owned()),
..Default::default()
}));
let mut out = String::new();
let value = project_root_impl(&[], &mut out).unwrap();
assert_eq!(value.display(), project_root.display().to_string());
reset_process_state();
let _ = std::fs::remove_dir_all(&cwd);
let _ = std::fs::remove_dir_all(&project_root);
}
#[test]
fn runtime_paths_uses_configurable_state_roots() {
let _runtime_paths_env_lock = crate::runtime_paths::test_env_lock()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let _env_guard = RuntimePathsEnvGuard::capture();
let base = std::env::temp_dir().join(format!("harn-process-runtime-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&base).unwrap();
std::env::set_var(crate::runtime_paths::HARN_STATE_DIR_ENV, ".custom-harn");
std::env::set_var(crate::runtime_paths::HARN_RUN_DIR_ENV, ".custom-runs");
std::env::set_var(
crate::runtime_paths::HARN_WORKTREE_DIR_ENV,
".custom-worktrees",
);
set_thread_execution_context(Some(RunExecutionRecord {
cwd: Some(base.to_string_lossy().into_owned()),
..Default::default()
}));
let mut vm = crate::vm::Vm::new();
register_process_builtins(&mut vm);
let mut out = String::new();
let builtin = vm
.builtins
.get("runtime_paths")
.expect("runtime_paths builtin");
let paths = match builtin(&[], &mut out).unwrap() {
VmValue::Dict(map) => map,
other => panic!("expected dict, got {other:?}"),
};
assert_eq!(
paths.get("state_root").unwrap().display(),
base.join(".custom-harn").display().to_string()
);
assert_eq!(
paths.get("run_root").unwrap().display(),
base.join(".custom-runs").display().to_string()
);
assert_eq!(
paths.get("worktree_root").unwrap().display(),
base.join(".custom-worktrees").display().to_string()
);
reset_process_state();
let _ = std::fs::remove_dir_all(&base);
}