use std::path::{Path, PathBuf};
use crate::config::StateConfig;
pub const STATE_DIR_NAME: &str = "state";
pub const STATE_DIR_ENV: &str = "LLMENV_STATE_DIR";
pub const RESERVED_STATE_ENV_VARS: &[&str] = &[STATE_DIR_ENV, "CLAUDE_CONFIG_DIR"];
#[must_use]
pub fn state_dir(adapter_root: &Path) -> PathBuf {
adapter_root.join(STATE_DIR_NAME)
}
#[must_use]
pub fn state_env_vars(cfg: &StateConfig, state_dir: &Path) -> Vec<(String, String)> {
let mut vars = Vec::with_capacity(cfg.tools.len() + 1);
vars.push((STATE_DIR_ENV.to_string(), state_dir.display().to_string()));
for tool in &cfg.tools {
let path = state_dir.join(&tool.subdir);
vars.push((tool.env.clone(), path.display().to_string()));
}
vars
}
pub fn ensure_state_dirs(cfg: &StateConfig, state_dir: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(state_dir)?;
for tool in &cfg.tools {
std::fs::create_dir_all(state_dir.join(&tool.subdir))?;
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::config::StateTool;
fn cfg(tools: &[(&str, &str)]) -> StateConfig {
StateConfig {
tools: tools
.iter()
.map(|(env, subdir)| StateTool {
env: (*env).into(),
subdir: (*subdir).into(),
})
.collect(),
}
}
#[test]
fn state_dir_is_unhashed_sibling() {
let root = Path::new("/cache/llmenv/claude-code");
assert_eq!(
state_dir(root),
Path::new("/cache/llmenv/claude-code/state")
);
}
#[test]
fn always_emits_llmenv_state_dir() {
let dir = Path::new("/cache/llmenv/claude-code/state");
let vars = state_env_vars(&StateConfig::default(), dir);
assert_eq!(
vars,
vec![(
"LLMENV_STATE_DIR".to_string(),
"/cache/llmenv/claude-code/state".to_string()
)]
);
}
#[test]
fn emits_per_tool_var_pointed_at_subdir() {
let dir = Path::new("/cache/llmenv/claude-code/state");
let vars = state_env_vars(&cfg(&[("CONTEXT_MODE_DATA_DIR", "context-mode")]), dir);
assert!(vars.contains(&(
"CONTEXT_MODE_DATA_DIR".to_string(),
"/cache/llmenv/claude-code/state/context-mode".to_string()
)));
assert!(vars.iter().any(|(k, _)| k == "LLMENV_STATE_DIR"));
}
#[test]
fn ensure_creates_base_and_subdirs() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("state");
ensure_state_dirs(&cfg(&[("A_DIR", "a"), ("B_DIR", "b")]), &dir).unwrap();
assert!(dir.is_dir());
assert!(dir.join("a").is_dir());
assert!(dir.join("b").is_dir());
}
#[test]
fn ensure_is_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("state");
let c = cfg(&[("A_DIR", "a")]);
ensure_state_dirs(&c, &dir).unwrap();
ensure_state_dirs(&c, &dir).unwrap();
assert!(dir.join("a").is_dir());
}
}