Skip to main content

gobby_code/
savings.rs

1//! Daemon-based savings tracking for gcode.
2//!
3//! Reports token savings to the Gobby daemon via HTTP POST when gcode returns
4//! compact symbol/outline data instead of full file contents.
5
6/// Calculate savings percentage.
7pub fn savings_pct(original_chars: usize, actual_chars: usize) -> f64 {
8    if original_chars == 0 {
9        return 0.0;
10    }
11    (1.0 - actual_chars as f64 / original_chars as f64) * 100.0
12}
13
14/// Report a savings event to the Gobby daemon via HTTP POST.
15///
16/// Best-effort: all errors are silently ignored. The daemon being down
17/// should never break gcode functionality.
18pub fn report_savings(base_url: &str, original_chars: usize, actual_chars: usize) {
19    let url = format!("{}/api/admin/savings/record", base_url);
20    let payload = serde_json::json!({
21        "category": "code_index",
22        "original_chars": original_chars,
23        "actual_chars": actual_chars,
24        "metadata": { "strategy": "outline" }
25    });
26    let _ = ureq::post(&url)
27        .timeout(std::time::Duration::from_secs(1))
28        .send_json(payload);
29}
30
31/// Resolve the daemon URL from config or environment.
32///
33/// Resolution order: config `daemon_url` → `GOBBY_PORT` env → default port 60887
34pub fn resolve_daemon_url(config_url: Option<&str>) -> Option<String> {
35    if let Some(url) = config_url {
36        // Expand ${GOBBY_PORT} if present
37        if url.contains("${GOBBY_PORT}") {
38            if let Ok(port) = std::env::var("GOBBY_PORT") {
39                return Some(url.replace("${GOBBY_PORT}", &port));
40            }
41            // Fall through to defaults if GOBBY_PORT not set
42        } else {
43            return Some(url.to_string());
44        }
45    }
46
47    // Fall back to GOBBY_PORT env var
48    if let Ok(port) = std::env::var("GOBBY_PORT") {
49        return Some(format!("http://localhost:{}", port));
50    }
51
52    // Default to well-known Gobby daemon (matches bootstrap.yaml defaults)
53    Some("http://localhost:60887".to_string())
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn test_savings_pct_basic() {
62        let pct = savings_pct(1000, 200);
63        assert!((pct - 80.0).abs() < 0.01);
64    }
65
66    #[test]
67    fn test_savings_pct_zero_original() {
68        assert_eq!(savings_pct(0, 0), 0.0);
69    }
70
71    #[test]
72    fn test_savings_pct_no_savings() {
73        assert!((savings_pct(100, 100)).abs() < 0.01);
74    }
75
76    #[test]
77    fn test_resolve_daemon_url_config_value() {
78        let url = resolve_daemon_url(Some("http://custom:9999"));
79        assert_eq!(url, Some("http://custom:9999".to_string()));
80    }
81
82    #[test]
83    #[serial_test::serial]
84    fn test_resolve_daemon_url_env_var() {
85        unsafe { std::env::set_var("GOBBY_PORT", "12345") };
86        let url = resolve_daemon_url(None);
87        assert_eq!(url, Some("http://localhost:12345".to_string()));
88        unsafe { std::env::remove_var("GOBBY_PORT") };
89    }
90
91    #[test]
92    #[serial_test::serial]
93    fn test_resolve_daemon_url_default() {
94        unsafe { std::env::remove_var("GOBBY_PORT") };
95        let url = resolve_daemon_url(None);
96        assert_eq!(url, Some("http://localhost:60887".to_string()));
97    }
98
99    #[test]
100    #[serial_test::serial]
101    fn test_resolve_daemon_url_expand_port() {
102        unsafe { std::env::set_var("GOBBY_PORT", "54321") };
103        let url = resolve_daemon_url(Some("http://myhost:${GOBBY_PORT}"));
104        assert_eq!(url, Some("http://myhost:54321".to_string()));
105        unsafe { std::env::remove_var("GOBBY_PORT") };
106    }
107}