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