1use serde_json::Value;
16
17pub fn session_id_from_payload(payload: &Value) -> Option<String> {
20 payload
21 .get("session_id")
22 .and_then(|s| s.as_str())
23 .filter(|s| !s.is_empty())
24 .map(str::to_string)
25}
26
27pub fn session_id_from_env() -> Option<String> {
29 std::env::var("CLAUDE_CODE_SESSION_ID")
30 .ok()
31 .filter(|s| !s.is_empty())
32}
33
34pub fn live_session_id(payload: Option<&Value>) -> Option<String> {
37 payload
38 .and_then(session_id_from_payload)
39 .or_else(session_id_from_env)
40}
41
42pub fn stamp_session_id(meta: &mut Value, sid: Option<&str>) {
47 if let (Some(sid), Some(obj)) = (sid, meta.as_object_mut()) {
48 obj.insert("session_id".to_string(), Value::String(sid.to_string()));
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use serde_json::json;
56 use std::sync::Mutex;
57
58 static ENV_LOCK: Mutex<()> = Mutex::new(());
60
61 #[test]
62 fn payload_session_id_extracted() {
63 let p = json!({"session_id": "abc-123", "hook_event_name": "PostToolUse"});
64 assert_eq!(session_id_from_payload(&p).as_deref(), Some("abc-123"));
65 }
66
67 #[test]
68 fn payload_empty_or_missing_is_none() {
69 assert_eq!(session_id_from_payload(&json!({"session_id": ""})), None);
70 assert_eq!(session_id_from_payload(&json!({})), None);
71 assert_eq!(session_id_from_payload(&Value::Null), None);
72 }
73
74 #[test]
75 fn stamp_adds_to_object_meta() {
76 let mut meta = json!({"title": "Goal"});
77 stamp_session_id(&mut meta, Some("s-1"));
78 assert_eq!(meta["session_id"], json!("s-1"));
79 assert_eq!(meta["title"], json!("Goal"));
80 }
81
82 #[test]
83 fn stamp_none_is_noop() {
84 let mut meta = json!({"title": "Goal"});
85 stamp_session_id(&mut meta, None);
86 assert!(meta.get("session_id").is_none());
87 }
88
89 #[test]
90 fn stamp_on_non_object_is_noop() {
91 let mut meta = Value::Null;
92 stamp_session_id(&mut meta, Some("s-1"));
93 assert_eq!(meta, Value::Null);
94 }
95
96 #[test]
97 fn live_payload_wins_over_env() {
98 let _g = ENV_LOCK.lock().unwrap();
99 std::env::set_var("CLAUDE_CODE_SESSION_ID", "from-env");
100 let p = json!({"session_id": "from-payload"});
101 assert_eq!(live_session_id(Some(&p)).as_deref(), Some("from-payload"));
102 std::env::remove_var("CLAUDE_CODE_SESSION_ID");
103 }
104
105 #[test]
106 fn live_falls_back_to_env() {
107 let _g = ENV_LOCK.lock().unwrap();
108 std::env::set_var("CLAUDE_CODE_SESSION_ID", "from-env");
109 let p = json!({"hook_event_name": "Stop"});
110 assert_eq!(live_session_id(Some(&p)).as_deref(), Some("from-env"));
111 assert_eq!(live_session_id(None).as_deref(), Some("from-env"));
112 std::env::remove_var("CLAUDE_CODE_SESSION_ID");
113 }
114
115 #[test]
116 fn live_none_when_no_source() {
117 let _g = ENV_LOCK.lock().unwrap();
118 std::env::remove_var("CLAUDE_CODE_SESSION_ID");
119 assert_eq!(live_session_id(None), None);
120 assert_eq!(live_session_id(Some(&json!({}))), None);
121 }
122}