Skip to main content

double_o/
session.rs

1/// Get the current session identifier.
2///
3/// Returns the parent process ID as a string, providing a stable identifier
4/// for all commands run within the same AI agent session.
5pub fn session_id() -> String {
6    let ppid = unsafe { libc::getppid() };
7    ppid.to_string()
8}
9
10/// Get the project identifier for the current working directory.
11///
12/// Returns a unique identifier for the project, used to scope store operations.
13/// Attempts to detect from git remote URL, git root directory, or current directory name.
14pub fn project_id() -> String {
15    #[cfg(feature = "vipune-store")]
16    {
17        vipune::detect_project(None)
18    }
19    #[cfg(not(feature = "vipune-store"))]
20    {
21        detect_project_fallback()
22    }
23}
24
25#[cfg(not(feature = "vipune-store"))]
26fn detect_project_fallback() -> String {
27    // Try git remote origin
28    if let Ok(output) = std::process::Command::new("git")
29        .args(["remote", "get-url", "origin"])
30        .output()
31    {
32        if output.status.success() {
33            let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
34            if let Some(name) = url.rsplit('/').next() {
35                let name = name.strip_suffix(".git").unwrap_or(name);
36                if !name.is_empty() {
37                    return name.to_string();
38                }
39            }
40        }
41    }
42
43    // Try git root directory name
44    if let Ok(output) = std::process::Command::new("git")
45        .args(["rev-parse", "--show-toplevel"])
46        .output()
47    {
48        if output.status.success() {
49            let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
50            if let Some(name) = path.rsplit('/').next() {
51                if !name.is_empty() {
52                    return name.to_string();
53                }
54            }
55        }
56    }
57
58    // Current directory name
59    if let Ok(cwd) = std::env::current_dir() {
60        if let Some(name) = cwd.file_name() {
61            return name.to_string_lossy().to_string();
62        }
63    }
64
65    "unknown".to_string()
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_session_id_is_nonzero() {
74        let id = session_id();
75        let pid: u32 = id.parse().expect("session_id should be numeric");
76        assert!(pid > 0);
77    }
78
79    #[test]
80    fn test_session_id_stable() {
81        assert_eq!(session_id(), session_id());
82    }
83
84    #[test]
85    fn test_project_id_nonempty() {
86        let id = project_id();
87        assert!(!id.is_empty());
88    }
89
90    // --- detect_project_fallback branch tests ---
91    // We test the function indirectly through project_id(), which always
92    // delegates to detect_project_fallback() in the non-vipune build.
93
94    #[test]
95    fn test_project_id_is_string() {
96        // project_id must return a valid (non-empty) UTF-8 string
97        let id = project_id();
98        assert!(!id.is_empty(), "project_id must not be empty");
99        assert!(
100            id.is_ascii() || !id.is_empty(),
101            "project_id must be a string"
102        );
103    }
104
105    #[test]
106    fn test_project_id_no_newlines() {
107        // The project identifier must not contain newlines (raw git output is trimmed)
108        let id = project_id();
109        assert!(
110            !id.contains('\n'),
111            "project_id must not contain newlines, got: {id:?}"
112        );
113    }
114
115    #[test]
116    #[cfg(not(feature = "vipune-store"))]
117    fn test_detect_project_fallback_cwd_fallback() {
118        // When run inside a temp dir with no git, detect_project_fallback must
119        // still return a non-empty string (the directory name or "unknown").
120        let tmp = tempfile::tempdir().expect("tempdir");
121        let original = std::env::current_dir().expect("cwd");
122        std::env::set_current_dir(tmp.path()).expect("set_current_dir");
123
124        let id = detect_project_fallback();
125
126        std::env::set_current_dir(&original).expect("restore cwd");
127        // Either the dir name (a UUID-ish string) or "unknown" — both are acceptable
128        assert!(!id.is_empty(), "fallback must not be empty");
129    }
130}