use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
use nmbrs_metrics::component::{Component, ComponentState, attach};
use nmbrs_metrics::labels::Labels;
use nmbrs_metrics::metrics_query::MetricsQuery;
#[derive(Clone)]
pub struct Execution {
pub exec_id: u64,
pub verb: &'static str,
pub started_at_nanos: i64,
pub workload: String,
pub scenario: String,
pub component: Arc<RwLock<Component>>,
}
impl Execution {
pub fn start(
session: &Session,
workload: &str,
scenario: &str,
verb: &'static str,
exec_id: u64,
) -> Self {
let workload_stem = Path::new(workload)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("workload");
let component = Arc::new(RwLock::new(Component::new(
Labels::of("exec_id", exec_id.to_string()).with("workload", workload_stem),
std::collections::HashMap::new(),
)));
attach(&session.component, &component);
component
.write()
.unwrap_or_else(|e| e.into_inner())
.set_state(ComponentState::Running);
Self {
exec_id,
verb,
started_at_nanos: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as i64)
.unwrap_or(0),
workload: workload_stem.to_string(),
scenario: scenario.to_string(),
component,
}
}
}
impl std::fmt::Debug for Execution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Execution")
.field("exec_id", &self.exec_id)
.field("verb", &self.verb)
.field("started_at_nanos", &self.started_at_nanos)
.field("workload", &self.workload)
.field("scenario", &self.scenario)
.finish_non_exhaustive()
}
}
pub struct Session {
pub id: String,
pub output_dir: PathBuf,
pub component: Arc<RwLock<Component>>,
pub metrics_query: Mutex<Option<Arc<MetricsQuery>>>,
}
pub fn flag_env_name(flag: &str) -> String {
let stem = flag.trim_start_matches("--");
format!("NMBRS_{}", stem.replace('-', "_").to_ascii_uppercase())
}
pub fn resolve_flag(args: &[String], flag: &str) -> Option<String> {
let cli = {
let eq_prefix = format!("{flag}=");
let mut iter = args.iter();
let mut found = None;
while let Some(arg) = iter.next() {
if let Some(rest) = arg.strip_prefix(&eq_prefix) {
found = Some(rest.to_string());
break;
}
if arg == flag {
found = iter.next().cloned();
break;
}
}
found
};
let env_name = flag_env_name(flag);
let env = std::env::var(&env_name)
.ok()
.filter(|v| !v.trim().is_empty());
match (cli, env) {
(Some(_), Some(_)) => {
eprintln!(
"error: configuration conflict — both `{flag}` (CLI) and \
`{env_name}` (env) are set. Pick one. Per SRD-04, every \
CLI flag has an env equivalent prefixed with `NMBRS_`; \
specifying both at once is a hard error so the operator \
sees their inputs are fighting."
);
std::process::exit(2);
}
(Some(v), None) | (None, Some(v)) => Some(v),
(None, None) => None,
}
}
pub fn check_session_path(p: &str, source: &str) -> Result<(), String> {
if let Some((head, _)) = p.split_once('=') {
let head_looks_like_param = !head.is_empty()
&& head
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
&& !head.contains('/');
if head_looks_like_param {
return Err(format!(
"session path from {source} is '{p}' — that looks \
like a `key=value` workload param, not a path. The umbrella \
`--session <kv>` parser splits only on `:`, so this would \
silently create a `<cwd>/{p}/…` directory tree. \
Did you mean: `--session-path <path>` (with `{p}` as a \
separate workload arg), or `--session path:<path>` (umbrella \
form, `:` as the kv separator)?"
));
}
}
Ok(())
}
pub(crate) fn validate_session_path_or_exit(p: &str, source: &str) {
if let Err(msg) = check_session_path(p, source) {
eprintln!("error: {msg}");
std::process::exit(2);
}
}
pub const SESSION_DIRECTORY_ENV: &str = "SESSION_DIRECTORY";
pub const SESSION_TOKEN: &str = "SESSION";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SessionReuse {
#[default]
Error,
Restart,
Resume,
}
impl SessionReuse {
pub fn parse(s: &str) -> Result<Self, String> {
match s.trim().to_ascii_lowercase().as_str() {
"error" | "fail" | "abort" => Ok(Self::Error),
"restart" | "wipe" | "clean" => Ok(Self::Restart),
"resume" | "continue" => Ok(Self::Resume),
other => Err(format!(
"session-reuse: expected one of 'error' | 'restart' | 'resume', got '{other}'",
)),
}
}
}
pub fn default_session_dir(id: &str) -> PathBuf {
default_sessions_root().join(id)
}
pub fn latest_session_dir() -> PathBuf {
default_sessions_root().join("latest")
}
pub fn latest_metrics_db() -> PathBuf {
latest_session_dir().join("metrics.db")
}
pub fn latest_session_log() -> PathBuf {
latest_session_dir().join("session.log")
}
pub fn latest_checkpoint_jsonl() -> PathBuf {
latest_session_dir().join("checkpoint.jsonl")
}
pub fn session_dir_named(name: &str) -> PathBuf {
default_sessions_root().join(name)
}
pub fn args_request_dryrun(args: &[String]) -> bool {
args.iter().any(|a| {
let a = a.strip_prefix("--").unwrap_or(a);
a.strip_prefix("dryrun=")
.map(|v| {
let v = v.trim();
!v.is_empty() && v != "false" && v != "0"
})
.unwrap_or(false)
})
}
pub(crate) fn symlink_any(target: &Path, link: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
{
if let Ok(md) = std::fs::symlink_metadata(link)
&& (md.file_type().is_symlink() || md.file_type().is_dir())
{
let _ = std::fs::remove_file(link);
let _ = std::fs::remove_dir(link);
}
let resolved = match link.parent() {
Some(p) => p.join(target),
None => target.to_path_buf(),
};
if resolved.is_dir() {
std::os::windows::fs::symlink_dir(target, link).or_else(|_| {
let abs = std::path::absolute(&resolved)?;
junction::create(&abs, link)
})
} else {
std::os::windows::fs::symlink_file(target, link)
.or_else(|_| std::fs::hard_link(&resolved, link))
}
}
}
pub fn point_latest_at(session_dir: &Path) {
let root = default_sessions_root();
if !target_is_under(&root, session_dir) {
return;
}
if std::fs::create_dir_all(&root).is_err() {
return;
}
let latest = root.join("latest");
let relative_target = relative_symlink_target(&latest, session_dir);
let _ = std::fs::remove_file(&latest);
let _ = symlink_any(&relative_target, &latest);
}
pub fn init_empty_session(
id: &str,
explicit_path: Option<&Path>,
reuse: SessionReuse,
) -> Result<PathBuf, String> {
let dir = match explicit_path {
Some(p) => p.to_path_buf(),
None => default_session_dir(id),
};
let metrics_db = dir.join("metrics.db");
if metrics_db.exists() {
match reuse {
SessionReuse::Error => {
return Err(format!(
"session '{id}' already exists at {} — pass session-reuse=restart to \
overwrite, session-reuse=resume to keep it, or choose another name",
dir.display(),
));
}
SessionReuse::Restart => {
let _ = std::fs::remove_file(&metrics_db);
}
SessionReuse::Resume => return Ok(dir),
}
}
std::fs::create_dir_all(&dir)
.map_err(|e| format!("create session dir {}: {e}", dir.display()))?;
{
let mut reporter = nmbrs_metrics::reporters::sqlite::SqliteReporter::new(&metrics_db)
.map_err(|e| format!("create metrics.db at {}: {e}", metrics_db.display()))?;
reporter.set_metadata("session", id);
}
point_latest_at(&dir);
Ok(dir)
}
pub fn default_sessions_root() -> PathBuf {
if cwd_is_workspace_dir() {
std::env::temp_dir().join("nmbrs-sessions")
} else {
PathBuf::from("sessions")
}
}
fn cwd_is_workspace_dir() -> bool {
if std::env::var_os("CARGO_MANIFEST_DIR").is_none() {
return false;
}
std::env::current_dir()
.map(|cwd| cwd.join("Cargo.toml").is_file())
.unwrap_or(false)
}
#[derive(Debug, Clone, Default)]
pub struct SessionDirSpec {
pub session_name: Option<String>,
pub session_path: Option<String>,
pub reuse: SessionReuse,
pub session_keep: usize,
pub session_shelflife: std::time::Duration,
pub force_new: bool,
}
impl SessionDirSpec {
pub fn is_empty(&self) -> bool {
self.session_name.is_none() && self.session_path.is_none()
}
pub fn resolve(&self, auto_id: &str) -> Option<(PathBuf, String)> {
if self.is_empty() {
return None;
}
let id = self
.session_name
.clone()
.unwrap_or_else(|| auto_id.to_string());
let path = match &self.session_path {
Some(p) => PathBuf::from(p.replace(SESSION_TOKEN, &id)),
None => default_session_dir(&id),
};
let id = path
.file_name()
.and_then(|s| s.to_str())
.map(String::from)
.unwrap_or(id);
Some((path, id))
}
pub fn needs_auto_id(&self) -> bool {
if self.session_name.is_some() {
return false;
}
match self.session_path.as_deref() {
Some(p) => p.contains(SESSION_TOKEN),
None => true,
}
}
}
pub fn parse_session_kv(s: &str) -> SessionDirSpec {
let mut spec = SessionDirSpec {
session_keep: DEFAULT_SESSIONS_MAX,
session_shelflife: DEFAULT_SESSIONS_SHELFLIFE,
..SessionDirSpec::default()
};
for raw_item in s.split(',') {
let item = raw_item.trim();
if item.is_empty() {
continue;
}
match item {
"restart" => {
spec.reuse = SessionReuse::Restart;
continue;
}
"resume" => {
spec.reuse = SessionReuse::Resume;
continue;
}
"error" => {
spec.reuse = SessionReuse::Error;
continue;
}
"new" => {
spec.force_new = true;
continue;
}
_ => {}
}
let drive_letter_path = item.len() >= 3
&& item.as_bytes()[0].is_ascii_alphabetic()
&& item.as_bytes()[1] == b':'
&& matches!(item.as_bytes()[2], b'\\' | b'/');
let kv = if drive_letter_path {
None
} else {
item.split_once(':')
};
let (key, value) = match kv {
Some((k, v)) => (k.trim(), v.trim()),
None => {
if drive_letter_path
|| item.contains('/')
|| (cfg!(windows) && item.contains('\\'))
|| std::path::Path::new(item).is_dir()
{
validate_session_path_or_exit(item, "umbrella `--session <bare-token>`");
spec.session_path = Some(item.to_string());
} else {
spec.session_name = Some(item.to_string());
}
continue;
}
};
match key {
"name" => spec.session_name = Some(value.to_string()),
"path" | "dir" => {
validate_session_path_or_exit(value, "umbrella `--session path:<v>`");
spec.session_path = Some(value.to_string());
}
"reuse" => match SessionReuse::parse(value) {
Ok(r) => spec.reuse = r,
Err(e) => crate::observer::log(
crate::observer::LogLevel::Warn,
&format!("--session: {e}"),
),
},
"keep" => match value.parse::<usize>() {
Ok(n) => spec.session_keep = n,
Err(_) => crate::observer::log(
crate::observer::LogLevel::Warn,
&format!("--session: keep:{value:?} is not a non-negative integer"),
),
},
"shelflife" => match parse_duration(value) {
Ok(d) => spec.session_shelflife = d,
Err(e) => crate::observer::log(
crate::observer::LogLevel::Warn,
&format!("--session: shelflife: {e}"),
),
},
other => crate::observer::log(
crate::observer::LogLevel::Warn,
&format!(
"--session: unknown key {other:?} (recognised: name, path, dir, reuse, keep, shelflife)"
),
),
}
}
spec
}
fn aliased_flag(args: &[String], canonical: &str, alias: &str) -> Option<String> {
let canon = resolve_flag(args, canonical);
let aliased = resolve_flag(args, alias);
match (&canon, &aliased) {
(Some(c), Some(a)) if c != a => crate::observer::log(
crate::observer::LogLevel::Warn,
&format!("{canonical}={c} and {alias}={a} disagree; using {canonical}={c}"),
),
_ => {}
}
canon.or(aliased)
}
pub fn resolve_session_dir(args: &[String]) -> SessionDirSpec {
let bare_session = args
.iter()
.find_map(|a| a.strip_prefix("session="))
.map(|v| v.to_string());
let mut spec = match resolve_flag(args, "--session").or(bare_session) {
Some(kv) => parse_session_kv(&kv),
None => SessionDirSpec {
session_keep: DEFAULT_SESSIONS_MAX,
session_shelflife: DEFAULT_SESSIONS_SHELFLIFE,
..SessionDirSpec::default()
},
};
if let Some(v) = resolve_flag(args, "--session-name") {
spec.session_name = Some(v);
}
if let Some(v) = aliased_flag(args, "--session-path", "--session-dir") {
validate_session_path_or_exit(&v, "`--session-path` flag (or NMBRS_SESSION_PATH env)");
spec.session_path = Some(v);
}
if let Some(v) = resolve_flag(args, "--session-reuse")
&& let Ok(r) = SessionReuse::parse(&v)
{
spec.reuse = r;
}
if let Some(v) = aliased_flag(args, "--session-keep", "--sessions-max")
&& let Ok(n) = v.trim().parse::<usize>()
{
spec.session_keep = n;
}
if let Some(v) = aliased_flag(args, "--session-shelflife", "--sessions-shelflife")
&& let Ok(d) = parse_duration(&v)
{
spec.session_shelflife = d;
}
if spec.session_path.is_none()
&& let Ok(v) = std::env::var(SESSION_DIRECTORY_ENV)
&& !v.trim().is_empty()
{
crate::observer::log(
crate::observer::LogLevel::Warn,
"SESSION_DIRECTORY is deprecated; use NMBRS_SESSION_PATH (SRD-04 NMBRS_-prefix convention).",
);
validate_session_path_or_exit(&v, "legacy `SESSION_DIRECTORY` env");
spec.session_path = Some(v);
}
spec
}
pub fn read_session_dir(args: &[String]) -> Option<PathBuf> {
let spec = resolve_session_dir(args);
if spec.is_empty() || spec.needs_auto_id() {
return None;
}
spec.resolve("").map(|(p, _)| p)
}
pub fn resolve_active(args: &[String]) -> Result<PathBuf, String> {
if let Some(p) = read_session_dir(args) {
if !p.exists() {
return Err(format!(
"session directory '{}' does not exist",
p.display(),
));
}
return Ok(p);
}
let latest = latest_session_dir();
if latest.exists() {
let resolved = std::fs::canonicalize(&latest).unwrap_or(latest.clone());
return Ok(resolved);
}
Err("no active session — run a workload first, or pass \
`--session <name>` / `--session-path <dir>` to point \
at an existing one"
.to_string())
}
pub const DEFAULT_SESSIONS_MAX: usize = 10;
pub const DEFAULT_SESSIONS_SHELFLIFE: std::time::Duration =
std::time::Duration::from_secs(60 * 60 * 24 * 7 * 4);
pub fn parse_duration(s: &str) -> Result<std::time::Duration, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty duration".into());
}
let (num_part, unit_seconds) = if let Some(n) = s.strip_suffix('w') {
(n, 60 * 60 * 24 * 7)
} else if let Some(n) = s.strip_suffix('d') {
(n, 60 * 60 * 24)
} else if let Some(n) = s.strip_suffix('h') {
(n, 60 * 60)
} else if let Some(n) = s.strip_suffix('m') {
(n, 60)
} else if let Some(n) = s.strip_suffix('s') {
(n, 1)
} else {
(s, 1) };
let n: u64 = num_part.trim().parse().map_err(|_| {
format!("duration: '{s}' is not a valid number with optional s/m/h/d/w suffix",)
})?;
Ok(std::time::Duration::from_secs(n * unit_seconds))
}
pub fn session_dir_has_prior_artifacts(dir: &Path) -> bool {
if !dir.exists() {
return false;
}
for marker in ["metrics.db", "session.log", "checkpoint.jsonl"] {
if dir.join(marker).exists() {
return true;
}
}
false
}
pub fn count_session_dirs(parent: &Path) -> usize {
let Ok(rd) = std::fs::read_dir(parent) else {
return 0;
};
rd.filter_map(|e| e.ok())
.filter(|e| {
std::fs::symlink_metadata(e.path())
.map(|m| !m.file_type().is_symlink() && m.file_type().is_dir())
.unwrap_or(false)
})
.count()
}
pub fn forecast_keep_purge(parent: &Path, keep_cap: usize) -> usize {
if keep_cap == 0 {
return 0;
}
let current = count_session_dirs(parent);
(current + 1).saturating_sub(keep_cap)
}
fn looks_like_session_dir(path: &Path) -> bool {
const SIGNATURES: &[&str] = &["metrics.db", "session.log", "checkpoint.jsonl"];
SIGNATURES.iter().any(|s| path.join(s).exists())
}
pub fn purge_stale_sessions(parent: &Path, max_sessions: usize, shelflife: std::time::Duration) {
if !parent.exists() {
return;
}
let latest_target: Option<PathBuf> = std::fs::read_link(parent.join("latest"))
.ok()
.map(|t| if t.is_absolute() { t } else { parent.join(t) });
let mut entries: Vec<(PathBuf, std::time::SystemTime)> = match std::fs::read_dir(parent) {
Ok(rd) => rd,
Err(e) => {
crate::observer::log(
crate::observer::LogLevel::Warn,
&format!(
"warning: session cleanup: failed to read {}: {e}",
parent.display(),
),
);
return;
}
}
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let path = entry.path();
let md = std::fs::symlink_metadata(&path).ok()?;
if md.file_type().is_symlink() || !md.file_type().is_dir() {
return None;
}
if let Some(target) = latest_target.as_ref()
&& path == *target
{
return None;
}
if !looks_like_session_dir(&path) {
return None;
}
let mtime = md.modified().ok()?;
Some((path, mtime))
})
.collect();
if entries.is_empty() {
return;
}
entries.sort_by(|a, b| b.1.cmp(&a.1));
let now = std::time::SystemTime::now();
let mut to_purge: Vec<&PathBuf> = Vec::new();
if max_sessions > 0 && entries.len() > max_sessions {
for (p, _) in &entries[max_sessions..] {
to_purge.push(p);
}
}
if !shelflife.is_zero() {
for (p, mtime) in &entries[..entries.len().min(if max_sessions == 0 {
usize::MAX
} else {
max_sessions
})] {
if let Ok(age) = now.duration_since(*mtime)
&& age > shelflife
&& !to_purge.contains(&p)
{
to_purge.push(p);
}
}
}
for path in to_purge {
if let Err(e) = std::fs::remove_dir_all(path) {
crate::observer::log(
crate::observer::LogLevel::Warn,
&format!(
"warning: session cleanup: failed to remove {}: {e}",
path.display(),
),
);
}
}
}
pub fn purge_stale_sessions_at_startup(args: &[String], creating_session: bool) {
if !creating_session {
return;
}
let spec = resolve_session_dir(args);
let cleanup_parent = if let Some(sd) = spec.session_path.as_ref() {
let resolved = sd.replace(SESSION_TOKEN, "");
PathBuf::from(resolved)
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(default_sessions_root)
} else {
default_sessions_root()
};
purge_stale_sessions(&cleanup_parent, spec.session_keep, spec.session_shelflife);
}
pub(crate) fn target_is_under(logs_dir: &Path, target: &Path) -> bool {
let cwd = std::env::current_dir().ok();
let abs = |p: &Path| -> Option<PathBuf> {
if p.is_absolute() {
Some(p.to_path_buf())
} else {
cwd.as_ref().map(|c| c.join(p))
}
};
match (abs(logs_dir), abs(target)) {
(Some(l), Some(t)) => t.starts_with(&l),
_ => false,
}
}
pub(crate) fn relative_symlink_target(link_path: &Path, target: &Path) -> PathBuf {
let link_parent = link_path.parent().unwrap_or_else(|| Path::new("."));
let cwd = std::env::current_dir().ok();
let abs = |p: &Path| -> Option<PathBuf> {
if p.is_absolute() {
Some(p.to_path_buf())
} else {
cwd.as_ref().map(|c| c.join(p))
}
};
let (Some(link_abs), Some(tgt_abs)) = (abs(link_parent), abs(target)) else {
return target.to_path_buf();
};
let link_comps: Vec<_> = link_abs.components().collect();
let tgt_comps: Vec<_> = tgt_abs.components().collect();
let common = link_comps
.iter()
.zip(tgt_comps.iter())
.take_while(|(a, b)| a == b)
.count();
if common == 0 {
return tgt_abs;
}
let ups = link_comps.len() - common;
let mut rel = PathBuf::new();
for _ in 0..ups {
rel.push("..");
}
for c in &tgt_comps[common..] {
rel.push(c.as_os_str());
}
if rel.as_os_str().is_empty() {
rel.push(".");
}
rel
}
impl Session {
pub fn new_with_args(session_name: &str, args: &[String]) -> Self {
let timestamp = format_timestamp();
let auto_id = format!("{session_name}_{timestamp}");
let spec = resolve_session_dir(args);
let (output_dir, id) = spec
.resolve(&auto_id)
.unwrap_or_else(|| (default_session_dir(&auto_id), auto_id.clone()));
if session_dir_has_prior_artifacts(&output_dir) {
match spec.reuse {
SessionReuse::Error => {
eprintln!(
"error: session directory {} already contains artifacts \
(metrics.db / session.log / checkpoint.jsonl).\n \
Pick a reuse policy:\n \
--session-reuse=restart (wipe artifacts, fresh run)\n \
--session-reuse=resume (continue with the prior session — \
equivalent to --resume <id>)\n \
Or pick a different path via --session, --logs-dir, \
--session-dir, or SESSION_DIRECTORY env.",
output_dir.display(),
);
std::process::exit(2);
}
SessionReuse::Restart => {
for marker in [
"metrics.db",
"session.log",
"checkpoint.jsonl",
"checkpoint.lock",
"summary.md",
"summary.txt",
"summary.json",
"tui.dump",
"flamegraph.svg",
"flamegraph-perf.svg",
"flamegraph-perf.md",
] {
let _ = std::fs::remove_file(output_dir.join(marker));
}
crate::observer::log(
crate::observer::LogLevel::Warn,
&format!(
"session-reuse=restart: wiped prior artifacts in {}",
output_dir.display(),
),
);
}
SessionReuse::Resume => {
eprintln!(
"error: --session-reuse=resume requires --resume to actually \
continue the prior session. Add --resume (or --resume-latest) \
to your command line. Path: {}",
output_dir.display(),
);
std::process::exit(2);
}
}
}
if let Err(e) = std::fs::create_dir_all(&output_dir) {
crate::observer::log(
crate::observer::LogLevel::Warn,
&format!(
"warning: failed to create session output directory {}: {e}",
output_dir.display()
),
);
}
let logs = default_sessions_root();
if target_is_under(&logs, &output_dir) && !args_request_dryrun(args) {
let _ = std::fs::create_dir_all(&logs);
let latest = logs.join("latest");
let _ = std::fs::remove_file(&latest);
let _ = symlink_any(&latest_symlink_target(&output_dir, &logs, &id), &latest);
for stale in [
"summary.md",
"flamegraph.svg",
"flamegraph-perf.svg",
"flamegraph-perf.md",
"tui.dump",
] {
let _ = std::fs::remove_file(logs.join(stale));
}
for artifact in ["session.log", "metrics.db"] {
let link = logs.join(artifact);
let _ = std::fs::remove_file(&link);
let target = PathBuf::from("latest").join(artifact);
let _ = symlink_any(&target, &link);
}
}
let component =
Component::root(Labels::of("session", &id), std::collections::HashMap::new());
crate::polydat_nodes::runtime_context::set_session_root(component.clone());
Self {
id,
output_dir,
component,
metrics_query: Mutex::new(None),
}
}
pub fn new(session_name: &str) -> Self {
Self::new_with_args(session_name, &[])
}
pub fn reattach(prior_session_dir: PathBuf, session_name: &str) -> Self {
let id = prior_session_dir
.file_name()
.and_then(|s| s.to_str())
.map(String::from)
.unwrap_or_else(|| {
format!("{session_name}_{}", format_timestamp())
});
let logs = default_sessions_root();
if target_is_under(&logs, &prior_session_dir) {
let latest = logs.join("latest");
let _ = std::fs::remove_file(&latest);
let _ = symlink_any(Path::new(&id), &latest);
for artifact in ["session.log", "metrics.db"] {
let link = logs.join(artifact);
let _ = std::fs::remove_file(&link);
let target = PathBuf::from("latest").join(artifact);
let _ = symlink_any(&target, &link);
}
}
let component =
Component::root(Labels::of("session", &id), std::collections::HashMap::new());
crate::polydat_nodes::runtime_context::set_session_root(component.clone());
Self {
id,
output_dir: prior_session_dir,
component,
metrics_query: Mutex::new(None),
}
}
pub fn link_artifact(name: &str) {
let logs = default_sessions_root();
let link = logs.join(name);
let _ = std::fs::remove_file(&link);
let target = PathBuf::from("latest").join(name);
let _ = symlink_any(&target, &link);
}
pub fn set_metrics_query(&self, query: Arc<MetricsQuery>) {
let mut slot = self.metrics_query.lock().unwrap_or_else(|e| e.into_inner());
assert!(slot.is_none(), "session metrics_query already installed");
*slot = Some(query);
}
pub fn metrics_query(&self) -> Option<Arc<MetricsQuery>> {
self.metrics_query
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub fn metrics_path(&self) -> PathBuf {
self.output_dir.join("metrics.db")
}
pub fn profiler_path(&self, suffix: &str) -> PathBuf {
self.output_dir.join(format!("flamegraph{suffix}.svg"))
}
pub fn artifact_path(&self, filename: &str) -> PathBuf {
self.output_dir.join(filename)
}
}
fn latest_symlink_target(output_dir: &Path, logs: &Path, id: &str) -> PathBuf {
if output_dir.parent() == Some(logs) {
return PathBuf::from(id);
}
let latest = logs.join("latest");
relative_symlink_target(&latest, output_dir)
}
pub fn utc_datetime_fields() -> (u64, u64, u64, u64, u64, u64, u128, u64) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let millis = now.as_millis();
let day_count = secs / 86400;
let t = secs % 86400;
let (year, month, day) = days_to_ymd(day_count);
(
year,
month,
day,
t / 3600,
(t % 3600) / 60,
t % 60,
millis,
secs,
)
}
fn format_timestamp() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let days = secs / 86400;
let time = secs % 86400;
let hours = time / 3600;
let minutes = (time % 3600) / 60;
let seconds = time % 60;
let (year, month, day) = days_to_ymd(days);
format!("{year:04}{month:02}{day:02}_{hours:02}{minutes:02}{seconds:02}")
}
pub fn now_log_timestamp() -> String {
format_log_timestamp(std::time::SystemTime::now())
}
pub fn format_log_timestamp(t: std::time::SystemTime) -> String {
let dur = t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
let secs = dur.as_secs();
let millis = dur.subsec_millis();
let days = secs / 86400;
let time = secs % 86400;
let hours = time / 3600;
let minutes = (time % 3600) / 60;
let seconds = time % 60;
let (year, month, day) = days_to_ymd(days);
format!("{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{millis:03}")
}
pub fn format_utc_short(epoch_seconds: f64) -> String {
if !epoch_seconds.is_finite() || epoch_seconds <= 0.0 {
return "-".to_string();
}
let secs = epoch_seconds as u64;
let (_, month, day) = days_to_ymd(secs / 86400);
let t = secs % 86400;
format!(
"{month:02}-{day:02} {:02}:{:02}:{:02}",
t / 3600,
(t % 3600) / 60,
t % 60
)
}
fn days_to_ymd(days: u64) -> (u64, u64, u64) {
let z = days + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
pub fn confine_to_dir(base: &Path, rel: &str) -> Result<PathBuf, String> {
use std::path::Component;
let rel_path = Path::new(rel.trim());
if rel.trim().is_empty() {
return Err("empty path".to_string());
}
for comp in rel_path.components() {
match comp {
Component::Normal(_) | Component::CurDir => {}
Component::ParentDir => {
return Err(format!(
"'{rel}' escapes the session directory (`..` is not allowed)"
));
}
Component::RootDir | Component::Prefix(_) => {
return Err(format!(
"'{rel}' is absolute; only paths relative to the session directory are accepted here"
));
}
}
}
Ok(base.join(rel_path))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timestamp_format() {
let ts = format_timestamp();
assert_eq!(ts.len(), 15, "timestamp: {ts}");
assert!(
ts.contains('_'),
"timestamp should contain underscore: {ts}"
);
}
fn env_test_lock() -> std::sync::MutexGuard<'static, ()> {
use std::sync::Mutex;
static LOCK: Mutex<()> = Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn dryrun_detection_from_args() {
let yes =
|a: &[&str]| args_request_dryrun(&a.iter().map(|s| s.to_string()).collect::<Vec<_>>());
assert!(yes(&["run", "workload=w", "dryrun=op"]));
assert!(yes(&["run", "dryrun=phase,wiring"]));
assert!(yes(&["run", "--dryrun=structure"]));
assert!(!yes(&["run", "workload=w", "scenario=incremental"]));
assert!(!yes(&["run", "dryrun=false"]));
assert!(!yes(&["run", "dryrun=0"]));
assert!(!yes(&["run", "dryrun="]));
}
#[test]
fn session_id_format() {
let _root_guard = crate::polydat_nodes::runtime_context::session_root_test_guard();
let _g = env_test_lock();
unsafe {
std::env::remove_var(SESSION_DIRECTORY_ENV);
}
let session = Session::new("fknn_rampup");
assert!(session.id.starts_with("fknn_rampup_"), "id: {}", session.id);
let expected_root = default_sessions_root();
assert!(
session.output_dir.starts_with(&expected_root),
"output_dir {} should start with {}",
session.output_dir.display(),
expected_root.display()
);
}
#[test]
fn session_paths() {
let _root_guard = crate::polydat_nodes::runtime_context::session_root_test_guard();
let _g = env_test_lock();
unsafe {
std::env::remove_var(SESSION_DIRECTORY_ENV);
}
let session = Session::new("smoke");
assert!(session.metrics_path().ends_with("metrics.db"));
assert!(session.profiler_path("").ends_with("flamegraph.svg"));
assert!(
session
.profiler_path("-perf")
.ends_with("flamegraph-perf.svg")
);
}
fn clear_session_env() {
unsafe {
std::env::remove_var("NMBRS_SESSION");
std::env::remove_var("NMBRS_SESSION_NAME");
std::env::remove_var("NMBRS_SESSION_PATH");
std::env::remove_var("NMBRS_SESSION_REUSE");
std::env::remove_var("NMBRS_SESSION_KEEP");
std::env::remove_var("NMBRS_SESSION_SHELFLIFE");
std::env::remove_var(SESSION_DIRECTORY_ENV);
}
}
#[test]
fn spec_session_path_flag_yields_basename_id() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session-path=/tmp/explicit".into()];
let spec = resolve_session_dir(&args);
let (path, id) = spec.resolve("auto-id").unwrap();
assert_eq!(path.to_str(), Some("/tmp/explicit"));
assert_eq!(id, "explicit");
}
#[test]
fn documented_flag_aliases_are_honoured() {
let _g = env_test_lock();
clear_session_env();
let spec = resolve_session_dir(&["--sessions-max=5".to_string()]);
assert_eq!(
spec.session_keep, 5,
"`--sessions-max` must set the keep cap"
);
let spec = resolve_session_dir(&["--sessions-shelflife=2w".to_string()]);
assert_eq!(
spec.session_shelflife,
std::time::Duration::from_secs(14 * 24 * 3600),
"`--sessions-shelflife` must set the retention window"
);
let spec = resolve_session_dir(&["--session-dir=/tmp/explicit".to_string()]);
let (path, id) = spec.resolve("auto").unwrap();
assert_eq!(
path.to_str(),
Some("/tmp/explicit"),
"`--session-dir` must set the session path"
);
assert_eq!(id, "explicit");
let spec = resolve_session_dir(&["--session-keep=7".to_string()]);
assert_eq!(spec.session_keep, 7);
let spec = resolve_session_dir(&[
"--session-keep=7".to_string(),
"--sessions-max=99".to_string(),
]);
assert_eq!(
spec.session_keep, 7,
"the canonical spelling must win over the alias"
);
}
#[test]
fn bare_session_kv_resolves_like_the_dash_flag() {
let _g = env_test_lock();
clear_session_env();
let bare = resolve_session_dir(&["session=/tmp/explicit".to_string()])
.resolve("auto-id")
.unwrap();
let dashed = resolve_session_dir(&["--session=/tmp/explicit".to_string()])
.resolve("auto-id")
.unwrap();
assert_eq!(bare, dashed, "both spellings must name the same session");
assert_eq!(bare.0.to_str(), Some("/tmp/explicit"));
}
#[test]
fn dash_session_wins_over_bare_session() {
let _g = env_test_lock();
clear_session_env();
let args = vec![
"session=/tmp/from-param".to_string(),
"--session=/tmp/from-flag".to_string(),
];
let (path, _) = resolve_session_dir(&args).resolve("auto-id").unwrap();
assert_eq!(path.to_str(), Some("/tmp/from-flag"));
}
#[test]
fn spec_session_name_only_yields_default_logs_dir() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session-name=alpha".into()];
let (path, id) = resolve_session_dir(&args).resolve("autogen").unwrap();
assert_eq!(path, default_sessions_root().join("alpha"));
assert_eq!(id, "alpha");
}
#[test]
fn spec_session_path_token_replaced_with_name() {
let _g = env_test_lock();
clear_session_env();
let args = vec![
"--session-path=/data/SESSION_run".into(),
"--session-name=alpha".into(),
];
let (path, id) = resolve_session_dir(&args).resolve("autogen").unwrap();
assert_eq!(path.to_str(), Some("/data/alpha_run"));
assert_eq!(id, "alpha_run");
}
#[test]
fn spec_session_path_token_falls_back_to_auto_id_when_no_name() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session-path=/data/SESSION_run".into()];
let (path, id) = resolve_session_dir(&args).resolve("autogen").unwrap();
assert_eq!(path.to_str(), Some("/data/autogen_run"));
assert_eq!(id, "autogen_run");
}
#[test]
fn spec_space_form_session_path_flag() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session-path".into(), "/data/path".into()];
let (path, _) = resolve_session_dir(&args).resolve("auto").unwrap();
assert_eq!(path.to_str(), Some("/data/path"));
}
#[test]
fn spec_falls_back_to_env() {
let _g = env_test_lock();
let prior = std::env::var(SESSION_DIRECTORY_ENV).ok();
unsafe {
std::env::set_var(SESSION_DIRECTORY_ENV, "/from/env");
}
let spec = resolve_session_dir(&[]);
match prior {
Some(v) => unsafe {
std::env::set_var(SESSION_DIRECTORY_ENV, v);
},
None => unsafe {
std::env::remove_var(SESSION_DIRECTORY_ENV);
},
}
let (path, _) = spec.resolve("auto").unwrap();
assert_eq!(path.to_str(), Some("/from/env"));
}
#[test]
fn spec_cli_flag_overrides_env() {
let _g = env_test_lock();
let prior = std::env::var(SESSION_DIRECTORY_ENV).ok();
unsafe {
std::env::set_var(SESSION_DIRECTORY_ENV, "/from/env");
}
let args = vec!["--session-path=/from/cli".into()];
let spec = resolve_session_dir(&args);
match prior {
Some(v) => unsafe {
std::env::set_var(SESSION_DIRECTORY_ENV, v);
},
None => unsafe {
std::env::remove_var(SESSION_DIRECTORY_ENV);
},
}
let (path, _) = spec.resolve("auto").unwrap();
assert_eq!(
path.to_str(),
Some("/from/cli"),
"CLI --session-path must win over SESSION_DIRECTORY env"
);
}
#[test]
fn spec_empty_returns_no_resolution() {
let _g = env_test_lock();
let prior = std::env::var(SESSION_DIRECTORY_ENV).ok();
unsafe {
std::env::remove_var(SESSION_DIRECTORY_ENV);
}
let spec = resolve_session_dir(&[]);
if let Some(v) = prior {
unsafe {
std::env::set_var(SESSION_DIRECTORY_ENV, v);
}
}
assert!(spec.is_empty());
assert!(spec.resolve("auto").is_none());
}
#[test]
fn spec_needs_auto_id_when_token_present() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session-path=/data/SESSION_x".into()];
assert!(resolve_session_dir(&args).needs_auto_id());
}
#[test]
fn spec_does_not_need_auto_id_when_path_is_concrete() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session-path=/data/specific".into()];
assert!(!resolve_session_dir(&args).needs_auto_id());
}
#[test]
fn spec_does_not_need_auto_id_with_explicit_name() {
let _g = env_test_lock();
clear_session_env();
let args = vec![
"--session-path=/data/SESSION_x".into(),
"--session-name=alpha".into(),
];
assert!(!resolve_session_dir(&args).needs_auto_id());
}
#[test]
fn umbrella_dir_shortcut_sets_path() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session=dir:asldkfjsldfj".into()];
let (path, id) = resolve_session_dir(&args).resolve("auto").unwrap();
assert_eq!(path.to_str(), Some("asldkfjsldfj"));
assert_eq!(id, "asldkfjsldfj", "session id is the basename of the path");
}
#[test]
fn umbrella_dir_with_subpath_yields_basename_id() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session=dir:l2k3j4/drr".into()];
let (path, id) = resolve_session_dir(&args).resolve("auto").unwrap();
assert_eq!(path.to_str(), Some("l2k3j4/drr"));
assert_eq!(id, "drr");
}
#[test]
fn umbrella_full_kv_list() {
let _g = env_test_lock();
clear_session_env();
let args =
vec!["--session=keep:42,name:sessname42,path:sessions/dir/SESSION,reuse:resume".into()];
let spec = resolve_session_dir(&args);
assert_eq!(spec.session_name.as_deref(), Some("sessname42"));
assert_eq!(spec.session_path.as_deref(), Some("sessions/dir/SESSION"));
assert_eq!(spec.reuse, SessionReuse::Resume);
assert_eq!(spec.session_keep, 42);
let (path, id) = spec.resolve("autogen").unwrap();
assert_eq!(path.to_str(), Some("sessions/dir/sessname42"));
assert_eq!(id, "sessname42");
}
#[test]
fn umbrella_bare_restart_token_sets_reuse() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session=restart,dir:/tmp/x".into()];
let spec = resolve_session_dir(&args);
assert_eq!(spec.reuse, SessionReuse::Restart);
assert_eq!(spec.session_path.as_deref(), Some("/tmp/x"));
}
#[test]
fn umbrella_bare_resume_token_sets_reuse() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session=resume,name:foo".into()];
let spec = resolve_session_dir(&args);
assert_eq!(spec.reuse, SessionReuse::Resume);
assert_eq!(spec.session_name.as_deref(), Some("foo"));
}
#[test]
fn umbrella_long_form_overrides_umbrella() {
let _g = env_test_lock();
clear_session_env();
let args = vec![
"--session=name:from-umbrella".into(),
"--session-name=from-longform".into(),
];
let spec = resolve_session_dir(&args);
assert_eq!(spec.session_name.as_deref(), Some("from-longform"));
}
#[test]
fn umbrella_unknown_key_logs_warn_but_keeps_rest() {
let _g = env_test_lock();
clear_session_env();
let args = vec!["--session=name:foo,what:nope,reuse:restart".into()];
let spec = resolve_session_dir(&args);
assert_eq!(spec.session_name.as_deref(), Some("foo"));
assert_eq!(spec.reuse, SessionReuse::Restart);
}
#[test]
fn check_session_path_rejects_workload_param_shape() {
for bad in &[
"scenario=foo",
"scenario=/tmp/foo",
"scenario=target",
"scenario=target/test-tmp/x",
"k=v",
"key_with_underscore=value",
"kebab-case=value",
] {
assert!(
check_session_path(bad, "test").is_err(),
"should reject '{bad}'"
);
}
}
#[test]
fn latest_session_dir_is_under_default_sessions_root() {
let root = default_sessions_root();
let latest = latest_session_dir();
assert!(
latest.starts_with(&root),
"latest_session_dir MUST live under default_sessions_root \
so the env-aware redirect (cargo tmp, etc.) covers it; \
root={root:?}, latest={latest:?}"
);
assert_eq!(latest.file_name().and_then(|s| s.to_str()), Some("latest"));
}
#[test]
fn latest_metrics_db_is_under_latest_session_dir() {
let metrics = latest_metrics_db();
let latest = latest_session_dir();
assert!(
metrics.starts_with(&latest),
"latest_metrics_db MUST live under latest_session_dir; \
metrics={metrics:?}, latest={latest:?}"
);
assert_eq!(
metrics.file_name().and_then(|s| s.to_str()),
Some("metrics.db")
);
}
#[test]
fn latest_session_log_is_under_latest_session_dir() {
let log = latest_session_log();
let latest = latest_session_dir();
assert!(log.starts_with(&latest));
assert_eq!(
log.file_name().and_then(|s| s.to_str()),
Some("session.log")
);
}
#[test]
fn latest_checkpoint_jsonl_is_under_latest_session_dir() {
let ckpt = latest_checkpoint_jsonl();
let latest = latest_session_dir();
assert!(ckpt.starts_with(&latest));
assert_eq!(
ckpt.file_name().and_then(|s| s.to_str()),
Some("checkpoint.jsonl")
);
}
#[test]
fn session_dir_named_is_a_sibling_of_latest() {
let root = default_sessions_root();
let named = session_dir_named("default_20260601_120000");
assert!(named.starts_with(&root));
assert_eq!(
named.file_name().and_then(|s| s.to_str()),
Some("default_20260601_120000"),
);
}
#[test]
fn check_session_path_accepts_real_paths() {
for good in &[
"/tmp/foo",
"/tmp/foo/bar",
"logs/session_2026",
"./local/x",
"../sibling/y",
"relative/path",
"/var/run/x=y", "C:/Windows/maybe", "logs/SESSION/x",
] {
assert!(
check_session_path(good, "test").is_ok(),
"should accept '{good}'"
);
}
}
#[test]
fn check_session_path_message_names_remediation() {
let err = check_session_path("scenario=foo", "test").unwrap_err();
assert!(err.contains("--session-path"), "missing flag hint: {err}");
assert!(
err.contains(":") && err.contains("kv separator"),
"missing umbrella-form hint: {err}"
);
}
#[test]
fn check_session_path_empty_head_passes() {
assert!(check_session_path("=foo", "test").is_ok());
}
#[test]
fn flag_env_name_canonicalisation() {
assert_eq!(flag_env_name("--session"), "NMBRS_SESSION");
assert_eq!(flag_env_name("--session-name"), "NMBRS_SESSION_NAME");
assert_eq!(flag_env_name("--session-path"), "NMBRS_SESSION_PATH");
assert_eq!(flag_env_name("--multi-word-flag"), "NMBRS_MULTI_WORD_FLAG");
}
#[test]
fn resolve_flag_picks_cli_when_only_cli_set() {
let _g = env_test_lock();
unsafe {
std::env::remove_var("NMBRS_SESSION_NAME");
}
let args = vec!["--session-name=foo".into()];
assert_eq!(
resolve_flag(&args, "--session-name").as_deref(),
Some("foo")
);
}
#[test]
fn resolve_flag_picks_env_when_only_env_set() {
let _g = env_test_lock();
unsafe {
std::env::set_var("NMBRS_SESSION_NAME", "bar");
}
let v = resolve_flag(&[], "--session-name");
unsafe {
std::env::remove_var("NMBRS_SESSION_NAME");
}
assert_eq!(v.as_deref(), Some("bar"));
}
#[test]
fn forecast_keep_purge_counts_excess() {
let parent =
std::env::temp_dir().join(format!("nmbrs-forecast-{}", crate::scratch_suffix()));
std::fs::create_dir_all(&parent).unwrap();
for i in 0..5 {
std::fs::create_dir(parent.join(format!("s{i}"))).unwrap();
}
assert_eq!(forecast_keep_purge(&parent, 10), 0);
assert_eq!(forecast_keep_purge(&parent, 5), 1);
assert_eq!(forecast_keep_purge(&parent, 3), 3);
assert_eq!(forecast_keep_purge(&parent, 0), 0);
let _ = std::fs::remove_dir_all(&parent);
}
#[test]
fn resolve_flag_returns_none_when_neither_set() {
let _g = env_test_lock();
unsafe {
std::env::remove_var("NMBRS_SESSION_NAME");
}
assert!(resolve_flag(&[], "--session-name").is_none());
}
#[test]
fn parse_duration_units() {
use std::time::Duration;
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
assert_eq!(parse_duration("3d").unwrap(), Duration::from_secs(259200));
assert_eq!(parse_duration("4w").unwrap(), Duration::from_secs(2419200));
assert_eq!(parse_duration("60").unwrap(), Duration::from_secs(60));
}
#[test]
fn parse_duration_rejects_garbage() {
assert!(parse_duration("").is_err());
assert!(parse_duration("not-a-number").is_err());
assert!(parse_duration("abc4w").is_err());
}
#[test]
fn purge_keeps_latest_n_sessions() {
let parent =
std::env::temp_dir().join(format!("nmbrs-purge-test-{}", crate::scratch_suffix()));
std::fs::create_dir_all(&parent).unwrap();
for i in 0..5 {
let d = parent.join(format!("sess_{i}"));
std::fs::create_dir(&d).unwrap();
let now = std::time::SystemTime::now() - std::time::Duration::from_secs(60 * (5 - i));
std::fs::write(d.join("metrics.db"), "x").unwrap();
let _ = now;
}
purge_stale_sessions(&parent, 2, std::time::Duration::ZERO);
let surviving: Vec<_> = std::fs::read_dir(&parent)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
assert!(
surviving.len() <= 2,
"expected ≤2 survivors, got {}: {:?}",
surviving.len(),
surviving
);
let _ = std::fs::remove_dir_all(&parent);
}
#[test]
fn purge_skips_logs_latest_symlink() {
let parent =
std::env::temp_dir().join(format!("nmbrs-purge-symlink-{}", crate::scratch_suffix()));
std::fs::create_dir_all(&parent).unwrap();
let active = parent.join("active_session");
std::fs::create_dir(&active).unwrap();
std::fs::write(active.join("metrics.db"), "x").unwrap();
if symlink_any(&active, &parent.join("latest")).is_err() {
eprintln!("skipping: cannot create symlinks here");
let _ = std::fs::remove_dir_all(&parent);
return;
}
purge_stale_sessions(&parent, 0, std::time::Duration::from_secs(1));
assert!(
active.exists(),
"active session pointed-at by logs/latest must survive purge"
);
let _ = std::fs::remove_dir_all(&parent);
}
}
#[cfg(test)]
mod confine_tests {
use super::confine_to_dir;
use std::path::Path;
#[test]
fn relative_paths_land_inside_the_base() {
let base = Path::new("/sess");
assert_eq!(
confine_to_dir(base, "traces.jsonl").unwrap(),
Path::new("/sess/traces.jsonl")
);
assert_eq!(
confine_to_dir(base, "./sub/x.log").unwrap(),
Path::new("/sess/./sub/x.log")
);
}
#[test]
fn escapes_and_absolutes_are_refused() {
let base = Path::new("/sess");
assert!(confine_to_dir(base, "../x").unwrap_err().contains(".."));
assert!(
confine_to_dir(base, "a/../../x")
.unwrap_err()
.contains("..")
);
assert!(
confine_to_dir(base, "/etc/passwd")
.unwrap_err()
.contains("absolute")
);
assert!(confine_to_dir(base, "").unwrap_err().contains("empty"));
}
}