csusage-adapter-opencode 0.0.2

Coding agent CLI usage reports (ccusage fork with Claude Science support)
use std::{collections::HashSet, env, path::PathBuf};

use crate::Result;

const OPENCODE_DATA_DIR_ENV: &str = "OPENCODE_DATA_DIR";
const XDG_DATA_HOME_ENV: &str = "XDG_DATA_HOME";

pub(super) fn paths() -> Result<Vec<PathBuf>> {
    let mut paths = Vec::new();
    let mut seen = HashSet::new();
    if let Some(env_paths) = env::var_os(OPENCODE_DATA_DIR_ENV) {
        let configured_paths = match env_paths.into_string() {
            Ok(env_paths) => env_paths
                .split(',')
                .map(str::trim)
                .filter(|path| !path.is_empty())
                .map(PathBuf::from)
                .collect(),
            Err(env_path) => vec![PathBuf::from(env_path)],
        };
        for path in configured_paths {
            if path.is_dir() && seen.insert(path.clone()) {
                paths.push(path);
            }
        }
        return Ok(paths);
    }

    let data_home = match env::var_os(XDG_DATA_HOME_ENV)
        .map(PathBuf::from)
        .filter(|path| path.is_absolute())
    {
        Some(path) => path,
        None => {
            let home = crate::home::home_dir()
                .ok_or_else(|| crate::cli_error("home directory is not set"))?;
            home.join(".local/share")
        }
    };
    let path = data_home.join("opencode");
    if path.is_dir() && seen.insert(path.clone()) {
        paths.push(path);
    }
    Ok(paths)
}

#[cfg(test)]
mod tests {
    use std::ffi::OsString;

    use csusage_test_support::{EnvVarsGuard, fs_fixture};

    use super::{OPENCODE_DATA_DIR_ENV, XDG_DATA_HOME_ENV, paths};

    fn isolated_env(
        opencode_data_dir: Option<OsString>,
        xdg_data_home: Option<OsString>,
        home: Option<OsString>,
    ) -> EnvVarsGuard {
        EnvVarsGuard::set_many([
            (OPENCODE_DATA_DIR_ENV, opencode_data_dir),
            (XDG_DATA_HOME_ENV, xdg_data_home),
            ("HOME", home),
            ("USERPROFILE", None),
            ("HOMEDRIVE", None),
            ("HOMEPATH", None),
        ])
    }

    #[test]
    fn uses_xdg_data_home_for_default_path() {
        let fixture = fs_fixture!({
            "xdg/opencode/opencode.db": "",
            "home/.local/share/opencode/opencode.db": "",
        });
        let _guard = isolated_env(
            None,
            Some(fixture.path("xdg").into_os_string()),
            Some(fixture.path("home").into_os_string()),
        );

        assert_eq!(paths().unwrap(), vec![fixture.path("xdg/opencode")]);
    }

    #[test]
    fn uses_xdg_data_home_without_a_home_directory() {
        let fixture = fs_fixture!({
            "xdg/opencode/opencode.db": "",
        });
        let _guard = isolated_env(None, Some(fixture.path("xdg").into_os_string()), None);

        assert_eq!(paths().unwrap(), vec![fixture.path("xdg/opencode")]);
    }

    #[test]
    fn keeps_configured_data_dirs_before_default_paths() {
        let fixture = fs_fixture!({
            "configured/first/opencode.db": "",
            "configured/second/opencode.db": "",
            "xdg/opencode/opencode.db": "",
            "home/.local/share/opencode/opencode.db": "",
        });
        let first = fixture.path("configured/first");
        let second = fixture.path("configured/second");
        let raw = format!(
            " {}, {}, {}, {} ",
            first.display(),
            second.display(),
            first.display(),
            fixture.path("missing").display()
        );
        let _guard = isolated_env(
            Some(OsString::from(raw)),
            Some(fixture.path("xdg").into_os_string()),
            Some(fixture.path("home").into_os_string()),
        );

        assert_eq!(paths().unwrap(), vec![first, second]);
    }

    #[cfg(unix)]
    #[test]
    fn does_not_fall_back_when_configured_data_dir_is_non_utf8() {
        use std::os::unix::ffi::OsStringExt;

        let _guard = isolated_env(
            Some(OsString::from_vec(b"opencode-\xFF".to_vec())),
            None,
            None,
        );

        assert!(paths().unwrap().is_empty());
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn discovers_non_utf8_configured_data_directory() {
        use std::{os::unix::ffi::OsStringExt, path::PathBuf};

        let fixture = fs_fixture!({});
        let configured_dir =
            fixture.create_dir_all(PathBuf::from(OsString::from_vec(b"opencode-\xFF".to_vec())));
        let _guard = isolated_env(Some(configured_dir.clone().into_os_string()), None, None);

        assert_eq!(paths().unwrap(), vec![configured_dir]);
    }

    #[test]
    fn does_not_fall_back_when_configured_data_dir_is_empty() {
        let fixture = fs_fixture!({
            "home/.local/share/opencode/opencode.db": "",
        });
        let _guard = isolated_env(
            Some(OsString::new()),
            None,
            Some(fixture.path("home").into_os_string()),
        );

        assert!(paths().unwrap().is_empty());
    }

    #[test]
    fn falls_back_to_home_data_directory_when_xdg_data_home_is_unset() {
        let fixture = fs_fixture!({
            "home/.local/share/opencode/opencode.db": "",
        });
        let _guard = isolated_env(None, None, Some(fixture.path("home").into_os_string()));

        assert_eq!(
            paths().unwrap(),
            vec![fixture.path("home/.local/share/opencode")]
        );
    }

    #[test]
    fn falls_back_to_home_data_directory_when_xdg_data_home_is_empty() {
        let fixture = fs_fixture!({
            "home/.local/share/opencode/opencode.db": "",
        });
        let _guard = isolated_env(
            None,
            Some(OsString::new()),
            Some(fixture.path("home").into_os_string()),
        );

        assert_eq!(
            paths().unwrap(),
            vec![fixture.path("home/.local/share/opencode")]
        );
    }

    #[test]
    fn falls_back_to_home_data_directory_when_xdg_data_home_is_relative() {
        let fixture = fs_fixture!({
            "home/.local/share/opencode/opencode.db": "",
        });
        let _guard = isolated_env(
            None,
            Some(OsString::from("relative-xdg-data-home")),
            Some(fixture.path("home").into_os_string()),
        );

        assert_eq!(
            paths().unwrap(),
            vec![fixture.path("home/.local/share/opencode")]
        );
    }

    #[test]
    fn does_not_fall_back_to_home_when_xdg_data_home_is_set() {
        let fixture = fs_fixture!({
            "home/.local/share/opencode/opencode.db": "",
        });
        let xdg_data_home = fixture.create_dir_all("xdg");
        let _guard = isolated_env(
            None,
            Some(xdg_data_home.into_os_string()),
            Some(fixture.path("home").into_os_string()),
        );

        assert!(paths().unwrap().is_empty());
    }
}