Skip to main content

ao_core/
opencode_session_id.rs

1pub fn as_valid_opencode_session_id(value: impl AsRef<str>) -> Option<String> {
2    let s = value.as_ref().trim();
3    if s.is_empty() {
4        return None;
5    }
6    if !s.starts_with("ses_") {
7        return None;
8    }
9    // Allowed: A-Z a-z 0-9 _ - (at least one char required after the prefix)
10    let tail: Vec<u8> = s.bytes().skip(4).collect();
11    if !tail.is_empty()
12        && tail
13            .iter()
14            .all(|&b| matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-'))
15    {
16        Some(s.to_string())
17    } else {
18        None
19    }
20}
21
22#[cfg(test)]
23mod tests {
24    use super::*;
25
26    #[test]
27    fn valid_ids_are_accepted() {
28        assert_eq!(
29            as_valid_opencode_session_id("ses_abc123"),
30            Some("ses_abc123".to_string())
31        );
32        assert_eq!(
33            as_valid_opencode_session_id("ses_A-B_C"),
34            Some("ses_A-B_C".to_string())
35        );
36    }
37
38    #[test]
39    fn prefix_only_is_rejected() {
40        // TS regex requires at least one char after "ses_"
41        assert_eq!(as_valid_opencode_session_id("ses_"), None);
42    }
43
44    #[test]
45    fn empty_and_no_prefix_rejected() {
46        assert_eq!(as_valid_opencode_session_id(""), None);
47        assert_eq!(as_valid_opencode_session_id("abc123"), None);
48    }
49
50    #[test]
51    fn invalid_chars_rejected() {
52        assert_eq!(as_valid_opencode_session_id("ses_ab!c"), None);
53        assert_eq!(as_valid_opencode_session_id("ses_ x"), None);
54    }
55
56    #[test]
57    fn whitespace_is_trimmed() {
58        assert_eq!(
59            as_valid_opencode_session_id("  ses_ok  "),
60            Some("ses_ok".to_string())
61        );
62    }
63}