bijux-dag-app 0.4.1

Application orchestration and response-shaping layer for Bijux DAG command surfaces.
Documentation
use crate::{read_file, read_run_id, ExitCode};
use bijux_dag_artifacts::RunDirLayout;
use serde_json::Value;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RunWorkspacePaths {
    requested_run_id: String,
    pub normalized_run_id: String,
    pub staging_path: PathBuf,
    pub final_path: PathBuf,
}

impl RunWorkspacePaths {
    pub(crate) fn for_run(root: &Path, run_id: &str) -> Result<Self, ExitCode> {
        let layout = RunDirLayout::preview(root, Some(run_id)).map_err(|_| ExitCode::from(2))?;
        Ok(Self {
            requested_run_id: run_id.trim().to_string(),
            normalized_run_id: layout.run_id,
            staging_path: layout.staging_path,
            final_path: layout.final_path,
        })
    }

    pub(crate) fn preferred_read_path(&self) -> PathBuf {
        if self.final_path.exists() {
            return self.final_path.clone();
        }
        if self.staging_path.exists() {
            return self.staging_path.clone();
        }
        let requested_path = self.requested_path();
        if requested_path.exists() {
            return requested_path;
        }
        self.final_path.clone()
    }

    pub(crate) fn active_run_path(&self) -> Option<PathBuf> {
        self.staging_path.exists().then(|| self.staging_path.clone())
    }

    pub(crate) fn stable_run_path(&self) -> Option<PathBuf> {
        if self.final_path.exists() {
            return Some(self.final_path.clone());
        }
        let requested_path = self.requested_path();
        requested_path.exists().then_some(requested_path)
    }

    fn requested_path(&self) -> PathBuf {
        self.final_path.parent().unwrap_or_else(|| Path::new(".")).join(&self.requested_run_id)
    }
}

pub(crate) fn read_manifest_json(run_dir: &Path) -> Result<Value, ExitCode> {
    let raw = read_file(&run_dir.join("manifest.json"))?;
    serde_json::from_str(&raw).map_err(|_| ExitCode::from(3))
}

pub(crate) fn read_run_identifier(run_dir: &Path) -> Result<String, ExitCode> {
    read_run_id(run_dir)
}

#[cfg(test)]
mod tests {
    use super::{read_manifest_json, read_run_identifier, RunWorkspacePaths};

    #[test]
    fn reads_manifest_and_run_id() {
        let tmp = tempfile::tempdir().expect("tempdir");
        std::fs::write(tmp.path().join("manifest.json"), r#"{"run_id":"r-123","status":"ok"}"#)
            .expect("write manifest");

        let manifest = read_manifest_json(tmp.path()).expect("read manifest");
        assert_eq!(manifest.get("status").and_then(|v| v.as_str()), Some("ok"));
        assert_eq!(read_run_identifier(tmp.path()).expect("read run id"), "r-123");
    }

    #[test]
    fn malformed_manifest_is_rejected_without_panic() {
        let tmp = tempfile::tempdir().expect("tempdir");
        std::fs::write(tmp.path().join("manifest.json"), b"{bad-json").expect("write manifest");
        assert!(read_manifest_json(tmp.path()).is_err());
    }

    #[test]
    fn missing_run_id_is_rejected() {
        let tmp = tempfile::tempdir().expect("tempdir");
        std::fs::write(tmp.path().join("manifest.json"), r#"{"status":"ok"}"#)
            .expect("write manifest");
        assert!(read_run_identifier(tmp.path()).is_err());
    }

    #[test]
    fn workspace_paths_accept_prefixed_and_canonical_run_ids() {
        let tmp = tempfile::tempdir().expect("tempdir");
        let prefixed = RunWorkspacePaths::for_run(tmp.path(), "run-abc").expect("prefixed paths");
        assert_eq!(prefixed.normalized_run_id, "abc");
        assert!(prefixed.staging_path.ends_with("run.tmp-abc"));
        assert!(prefixed.final_path.ends_with("run-abc"));

        let canonical = RunWorkspacePaths::for_run(tmp.path(), "abc").expect("canonical paths");
        assert_eq!(canonical.normalized_run_id, "abc");
        assert_eq!(canonical.staging_path, prefixed.staging_path);
        assert_eq!(canonical.final_path, prefixed.final_path);
    }

    #[test]
    fn workspace_paths_prefer_final_then_staging_then_requested_path() {
        let tmp = tempfile::tempdir().expect("tempdir");
        let paths = RunWorkspacePaths::for_run(tmp.path(), "run-history").expect("paths");

        assert_eq!(paths.preferred_read_path(), paths.final_path);

        std::fs::create_dir_all(&paths.staging_path).expect("create staging");
        assert_eq!(paths.preferred_read_path(), paths.staging_path);
        std::fs::remove_dir_all(&paths.staging_path).expect("remove staging");

        std::fs::create_dir_all(tmp.path().join("run-history")).expect("create requested");
        assert_eq!(paths.preferred_read_path(), tmp.path().join("run-history"));
    }
}