Coverage Report

Created: 2026-04-05 07:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
crates/flotilla-core/src/providers/discovery/detectors/codex.rs
Line
Count
Source
1
//! Codex auth file host detector.
2
//!
3
//! Checks whether the Codex auth file (`auth.json`) exists under `$CODEX_HOME`
4
//! (or `~/.codex` by default), indicating that the user has authenticated with
5
//! the Codex CLI.
6
7
use std::path::PathBuf;
8
9
use async_trait::async_trait;
10
11
use crate::providers::{
12
    discovery::{EnvVars, EnvironmentAssertion, HostDetector},
13
    CommandRunner,
14
};
15
16
/// Returns the Codex home directory: `$CODEX_HOME` or `$HOME/.codex`.
17
/// Returns `None` when neither env var is available.
18
5
fn codex_home(env: &dyn EnvVars) -> Option<PathBuf> {
19
5
    if let Some(
val2
) = env.get("CODEX_HOME") {
20
2
        Some(PathBuf::from(val))
21
    } else {
22
3
        env.get("HOME").map(|h| 
PathBuf::from(h)1
.
join1
(".codex"))
23
    }
24
5
}
25
26
/// Detects whether a Codex auth file exists.
27
pub struct CodexAuthDetector;
28
29
#[async_trait]
30
impl HostDetector for CodexAuthDetector {
31
5
    async fn detect(&self, runner: &dyn CommandRunner, env: &dyn EnvVars) -> Vec<EnvironmentAssertion> {
32
        let Some(home) = codex_home(env) else {
33
            return vec![];
34
        };
35
        let auth_path = home.join("auth.json");
36
        // Check existence via runner (test -f) rather than local filesystem
37
        let Some(path_str) = auth_path.to_str() else {
38
            return vec![]; // non-UTF-8 path — can't pass to runner
39
        };
40
        if runner.exists("test", &["-f", path_str]).await {
41
            vec![EnvironmentAssertion::auth_file("codex", auth_path)]
42
        } else {
43
            vec![]
44
        }
45
5
    }
46
}
47
48
#[cfg(test)]
49
mod tests {
50
    use std::path::Path;
51
52
    use super::*;
53
    use crate::providers::discovery::test_support::{DiscoveryMockRunner, TestEnvVars};
54
55
    #[tokio::test]
56
1
    async fn codex_auth_detector_found_via_codex_home() {
57
1
        let runner = DiscoveryMockRunner::builder().tool_exists("test", true).build();
58
1
        let env = TestEnvVars::new([("CODEX_HOME", "/mock/codex")]);
59
1
        let assertions = CodexAuthDetector.detect(&runner, &env).await;
60
61
1
        assert_eq!(assertions.len(), 1);
62
1
        match &assertions[0] {
63
1
            EnvironmentAssertion::AuthFileExists { provider, path } => {
64
1
                assert_eq!(provider, "codex");
65
1
                assert_eq!(path.as_path(), Path::new("/mock/codex/auth.json"));
66
1
            }
67
1
            
other0
=>
panic!0
("expected AuthFileExists, got {other:?}"),
68
1
        }
69
1
    }
70
71
    #[tokio::test]
72
1
    async fn codex_auth_detector_found_via_home() {
73
1
        let runner = DiscoveryMockRunner::builder().tool_exists("test", true).build();
74
1
        let env = TestEnvVars::new([("HOME", "/mock/home")]);
75
1
        let assertions = CodexAuthDetector.detect(&runner, &env).await;
76
77
1
        assert_eq!(assertions.len(), 1);
78
1
        match &assertions[0] {
79
1
            EnvironmentAssertion::AuthFileExists { provider, path } => {
80
1
                assert_eq!(provider, "codex");
81
1
                assert_eq!(path.as_path(), Path::new("/mock/home/.codex/auth.json"));
82
1
            }
83
1
            
other0
=>
panic!0
("expected AuthFileExists, got {other:?}"),
84
1
        }
85
1
    }
86
87
    #[tokio::test]
88
1
    async fn codex_auth_detector_not_found() {
89
        // test -f returns false
90
1
        let runner = DiscoveryMockRunner::builder().tool_exists("test", false).build();
91
1
        let env = TestEnvVars::new([("CODEX_HOME", "/mock/codex")]);
92
1
        let assertions = CodexAuthDetector.detect(&runner, &env).await;
93
94
1
        assert!(assertions.is_empty());
95
1
    }
96
97
    #[tokio::test]
98
1
    async fn codex_auth_detector_no_home() {
99
        // Neither CODEX_HOME nor HOME set
100
1
        let runner = DiscoveryMockRunner::builder().build();
101
1
        let assertions = CodexAuthDetector.detect(&runner, &TestEnvVars::default()).await;
102
103
1
        assert!(assertions.is_empty());
104
1
    }
105
}