Skip to main content

sqlite_graphrag/commands/
namespace_detect.rs

1//! Handler for the `namespace-detect` CLI subcommand.
2
3use crate::errors::AppError;
4use crate::namespace;
5use crate::output;
6use serde::Serialize;
7
8#[derive(clap::Args)]
9#[command(after_long_help = "EXAMPLES:\n  \
10    # Resolve namespace using current environment and cwd\n  \
11    sqlite-graphrag namespace-detect\n\n  \
12    # Override with an explicit namespace flag\n  \
13    sqlite-graphrag namespace-detect --namespace my-project\n\n  \
14    # Resolve via SQLITE_GRAPHRAG_NAMESPACE env var\n  \
15    SQLITE_GRAPHRAG_NAMESPACE=ci-runner sqlite-graphrag namespace-detect")]
16pub struct NamespaceDetectArgs {
17    #[arg(long)]
18    pub namespace: Option<String>,
19    /// Explicit database path. Accepted as a no-op to preserve the global contract.
20    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
21    pub db: Option<String>,
22    /// Explicit JSON flag. Accepted as a no-op because output is already JSON by default.
23    #[arg(long, default_value_t = false)]
24    pub json: bool,
25}
26
27#[derive(Serialize)]
28struct NamespaceDetectResponse {
29    namespace: String,
30    source: namespace::NamespaceSource,
31    cwd: String,
32    /// Total execution time in milliseconds from handler start to serialisation.
33    elapsed_ms: u64,
34}
35
36pub fn run(args: NamespaceDetectArgs) -> Result<(), AppError> {
37    let inicio = std::time::Instant::now();
38    let _ = args.db;
39    let _ = args.json; // --json is a no-op because output is already JSON by default
40    let resolution = namespace::detect_namespace(args.namespace.as_deref())?;
41    output::emit_json(&NamespaceDetectResponse {
42        namespace: resolution.namespace,
43        source: resolution.source,
44        cwd: resolution.cwd,
45        elapsed_ms: inicio.elapsed().as_millis() as u64,
46    })?;
47    Ok(())
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::namespace::NamespaceSource;
54    use clap::Parser;
55    use serial_test::serial;
56
57    #[test]
58    #[serial]
59    fn namespace_detect_default_returns_global_via_detect() {
60        // Garante que sem flag e sem env, detect_namespace retorna "global"
61        std::env::remove_var("SQLITE_GRAPHRAG_NAMESPACE");
62        let resolution = namespace::detect_namespace(None).unwrap();
63        assert_eq!(resolution.namespace, "global");
64        assert_eq!(resolution.source, NamespaceSource::Default);
65    }
66
67    #[test]
68    #[serial]
69    fn namespace_detect_explicit_flag_overrides_env() {
70        std::env::set_var("SQLITE_GRAPHRAG_NAMESPACE", "env-namespace");
71        let resolution = namespace::detect_namespace(Some("flag-namespace")).unwrap();
72        assert_eq!(resolution.namespace, "flag-namespace");
73        assert_eq!(resolution.source, NamespaceSource::ExplicitFlag);
74        std::env::remove_var("SQLITE_GRAPHRAG_NAMESPACE");
75    }
76
77    #[test]
78    #[serial]
79    fn namespace_detect_env_var_used_when_no_flag() {
80        std::env::remove_var("SQLITE_GRAPHRAG_NAMESPACE");
81        std::env::set_var("SQLITE_GRAPHRAG_NAMESPACE", "namespace-de-env");
82        let resolution = namespace::detect_namespace(None).unwrap();
83        assert_eq!(resolution.namespace, "namespace-de-env");
84        assert_eq!(resolution.source, NamespaceSource::Environment);
85        std::env::remove_var("SQLITE_GRAPHRAG_NAMESPACE");
86    }
87
88    #[test]
89    fn namespace_detect_response_serializes_all_fields() {
90        let resp = NamespaceDetectResponse {
91            namespace: "meu-projeto".to_string(),
92            source: NamespaceSource::ExplicitFlag,
93            cwd: "/home/usuario/projeto".to_string(),
94            elapsed_ms: 3,
95        };
96        let json = serde_json::to_value(&resp).unwrap();
97        assert_eq!(json["namespace"], "meu-projeto");
98        assert_eq!(json["source"], "explicit_flag");
99        assert!(json["cwd"].is_string());
100        assert_eq!(json["elapsed_ms"], 3);
101    }
102
103    #[test]
104    fn namespace_source_serializes_in_snake_case() {
105        let cases = vec![
106            (NamespaceSource::ExplicitFlag, "explicit_flag"),
107            (NamespaceSource::Environment, "environment"),
108            (NamespaceSource::Default, "default"),
109        ];
110        for (source, expected) in cases {
111            let json = serde_json::to_value(source).unwrap();
112            assert_eq!(
113                json, expected,
114                "NamespaceSource::{source:?} must serialize as \"{expected}\""
115            );
116        }
117    }
118
119    #[test]
120    fn namespace_detect_accepts_db_as_noop() {
121        let cli = crate::cli::Cli::try_parse_from([
122            "sqlite-graphrag",
123            "namespace-detect",
124            "--db",
125            "/tmp/graphrag.sqlite",
126        ])
127        .expect("namespace-detect must accept --db as a no-op");
128
129        match cli.command {
130            crate::cli::Commands::NamespaceDetect(args) => {
131                assert_eq!(args.db.as_deref(), Some("/tmp/graphrag.sqlite"));
132            }
133            _ => unreachable!("unexpected command parsed"),
134        }
135    }
136}