ito_domain/audit/
context.rs1use std::path::Path;
10
11use super::event::EventContext;
12
13#[derive(Debug, Clone, Default)]
15pub struct GitContext {
16 pub branch: Option<String>,
18 pub worktree: Option<String>,
20 pub commit: Option<String>,
22}
23
24pub fn resolve_context(ito_path: &Path) -> EventContext {
29 let session_id = resolve_session_id(ito_path);
30 let harness_session_id = resolve_harness_session_id();
31 let git = resolve_git_context();
32
33 EventContext {
34 session_id,
35 harness_session_id,
36 branch: git.branch,
37 worktree: git.worktree,
38 commit: git.commit,
39 }
40}
41
42pub fn resolve_session_id(ito_path: &Path) -> String {
47 let session_dir = ito_path.join(".state").join("audit");
48 let session_file = session_dir.join(".session");
49
50 if let Ok(contents) = std::fs::read_to_string(&session_file) {
52 let contents = contents.trim().to_string();
53 if !contents.is_empty() {
54 return contents;
55 }
56 }
57
58 let id = uuid::Uuid::new_v4().to_string();
60
61 let _ = std::fs::create_dir_all(&session_dir);
63 let _ = std::fs::write(&session_file, &id);
64
65 id
66}
67
68pub fn resolve_harness_session_id() -> Option<String> {
73 let env_vars = [
74 "ITO_HARNESS_SESSION_ID",
75 "CLAUDE_SESSION_ID",
76 "OPENCODE_SESSION_ID",
77 "CODEX_SESSION_ID",
78 ];
79
80 for var in env_vars {
81 if let Ok(val) = std::env::var(var)
82 && !val.is_empty()
83 {
84 return Some(val);
85 }
86 }
87
88 None
89}
90
91pub fn resolve_git_context() -> GitContext {
96 let branch = run_git_command(&["symbolic-ref", "--short", "HEAD"]);
97 let commit = run_git_command(&["rev-parse", "--short=8", "HEAD"]);
98
99 let worktree = detect_worktree_name();
101
102 GitContext {
103 branch,
104 worktree,
105 commit,
106 }
107}
108
109pub fn resolve_user_identity() -> String {
114 let name = run_git_command(&["config", "user.name"])
115 .or_else(|| std::env::var("USER").ok())
116 .unwrap_or_else(|| "unknown".to_string());
117
118 format!("@{}", name.to_lowercase().replace(' ', "-"))
119}
120
121fn run_git_command(args: &[&str]) -> Option<String> {
123 let output = std::process::Command::new("git").args(args).output().ok()?;
124
125 if !output.status.success() {
126 return None;
127 }
128
129 let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
130 if s.is_empty() { None } else { Some(s) }
131}
132
133fn detect_worktree_name() -> Option<String> {
135 let git_dir = run_git_command(&["rev-parse", "--git-dir"])?;
136 let common_dir = run_git_command(&["rev-parse", "--git-common-dir"])?;
137
138 let git_dir_path = std::path::Path::new(&git_dir);
140 let common_dir_path = std::path::Path::new(&common_dir);
141
142 if git_dir_path.canonicalize().ok()? != common_dir_path.canonicalize().ok()? {
143 let toplevel = run_git_command(&["rev-parse", "--show-toplevel"])?;
145 let path = std::path::Path::new(&toplevel);
146 Some(path.file_name()?.to_string_lossy().to_string())
147 } else {
148 None
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn resolve_user_identity_returns_at_prefixed_string() {
158 let identity = resolve_user_identity();
159 assert!(identity.starts_with('@'));
160 assert!(!identity.contains(' '));
161 }
162
163 #[test]
164 fn resolve_harness_session_id_returns_none_without_env() {
165 let _result = resolve_harness_session_id();
168 }
169
170 #[test]
171 fn resolve_git_context_does_not_panic() {
172 let ctx = resolve_git_context();
174 let _ = ctx;
177 }
178
179 #[test]
180 fn resolve_session_id_generates_uuid() {
181 let tmp = tempfile::tempdir().expect("tempdir");
182 let ito_path = tmp.path().join(".ito");
183 std::fs::create_dir_all(&ito_path).expect("create ito dir");
184
185 let id = resolve_session_id(&ito_path);
186 assert!(!id.is_empty());
187 assert_eq!(id.len(), 36);
189 assert!(id.contains('-'));
190 }
191
192 #[test]
193 fn resolve_session_id_is_stable_across_calls() {
194 let tmp = tempfile::tempdir().expect("tempdir");
195 let ito_path = tmp.path().join(".ito");
196 std::fs::create_dir_all(&ito_path).expect("create ito dir");
197
198 let id1 = resolve_session_id(&ito_path);
199 let id2 = resolve_session_id(&ito_path);
200 assert_eq!(id1, id2);
201 }
202
203 #[test]
204 fn resolve_context_populates_session_id() {
205 let tmp = tempfile::tempdir().expect("tempdir");
206 let ito_path = tmp.path().join(".ito");
207 std::fs::create_dir_all(&ito_path).expect("create ito dir");
208
209 let ctx = resolve_context(&ito_path);
210 assert!(!ctx.session_id.is_empty());
211 }
212}