Skip to main content

xianyun_executor/exec/
mise_decorator.rs

1//! Wraps commands with `mise exec --` when `mise` is available in PATH.
2//!
3//! Ported from `internal/executor/mise_decorator.go`. Not yet implemented.
4
5use super::repository::{Command, RepositoryExecutorDecorator};
6use crate::exec::ExecError;
7
8/// Ported from `miseDecorator`.
9#[derive(Default)]
10pub struct MiseDecorator;
11
12impl RepositoryExecutorDecorator for MiseDecorator {
13    fn wrap(&self, original: Command) -> Result<Command, ExecError> {
14        if look_path("mise").is_none() {
15            // mise not found - gracefully skip decoration (not an error).
16            return Ok(original);
17        }
18
19        let mut new_args = vec!["exec".to_string(), "--".to_string(), original.name];
20        new_args.extend(original.args);
21
22        Ok(Command {
23            name: "mise".to_string(),
24            args: new_args,
25            env: original.env,
26            dir: original.dir,
27        })
28    }
29}
30
31/// Minimal `exec.LookPath`-equivalent PATH search, avoiding a dependency on
32/// an external `which`-style crate for this single narrow use.
33fn look_path(name: &str) -> Option<std::path::PathBuf> {
34    let path_var = std::env::var_os("PATH")?;
35    for dir in std::env::split_paths(&path_var) {
36        let candidate = dir.join(name);
37        if candidate.is_file() {
38            #[cfg(unix)]
39            {
40                use std::os::unix::fs::PermissionsExt;
41                let Ok(meta) = std::fs::metadata(&candidate) else {
42                    continue;
43                };
44                if meta.permissions().mode() & 0o111 == 0 {
45                    continue;
46                }
47            }
48            return Some(candidate);
49        }
50    }
51    None
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    fn mise_available() -> bool {
59        look_path("mise").is_some()
60    }
61
62    #[test]
63    fn implements_interface() {
64        let _: &dyn RepositoryExecutorDecorator = &MiseDecorator;
65    }
66
67    #[test]
68    fn with_mise_available() {
69        if !mise_available() {
70            return; // Go: t.Skip("mise not available in PATH, skipping test")
71        }
72
73        let decorator = MiseDecorator;
74        let original = Command {
75            name: "kubectl".into(),
76            args: vec!["get".into(), "pods".into()],
77            env: Some(vec!["TEST_VAR=value".into()]),
78            dir: Some("/test/dir".into()),
79        };
80
81        let result = decorator.wrap(original).unwrap();
82
83        assert_eq!(result.name, "mise");
84        assert_eq!(result.args, vec!["exec", "--", "kubectl", "get", "pods"]);
85        assert_eq!(result.env, Some(vec!["TEST_VAR=value".to_string()]));
86        assert_eq!(result.dir, Some("/test/dir".to_string()));
87    }
88
89    #[test]
90    fn with_mise_missing() {
91        if mise_available() {
92            return; // Go: t.Skip("mise is available in PATH, skipping test for missing mise")
93        }
94
95        let decorator = MiseDecorator;
96        let original = Command {
97            name: "kubectl".into(),
98            args: vec!["get".into(), "pods".into()],
99            env: None,
100            dir: None,
101        };
102
103        let result = decorator.wrap(original.clone()).unwrap();
104        assert_eq!(
105            result.name, original.name,
106            "should return original command unchanged when mise is not available"
107        );
108        assert_eq!(result.args, original.args);
109    }
110
111    #[test]
112    fn preserves_environment() {
113        if !mise_available() {
114            return;
115        }
116
117        let decorator = MiseDecorator;
118        let original = Command {
119            name: "test-command".into(),
120            args: vec![],
121            env: Some(vec![
122                "VAR1=value1".into(),
123                "VAR2=value2".into(),
124                format!("PATH={}", std::env::var("PATH").unwrap_or_default()),
125            ]),
126            dir: None,
127        };
128        let expected_env = original.env.clone();
129
130        let result = decorator.wrap(original).unwrap();
131        assert_eq!(
132            result.env, expected_env,
133            "environment variables should be preserved"
134        );
135    }
136
137    #[test]
138    fn preserves_directory() {
139        if !mise_available() {
140            return;
141        }
142
143        let decorator = MiseDecorator;
144        let original = Command {
145            name: "test-command".into(),
146            args: vec!["arg1".into(), "arg2".into()],
147            env: None,
148            dir: Some("/custom/working/directory".into()),
149        };
150
151        let result = decorator.wrap(original).unwrap();
152        assert_eq!(
153            result.dir,
154            Some("/custom/working/directory".to_string()),
155            "working directory should be preserved"
156        );
157    }
158
159    #[test]
160    fn handles_command_with_no_args() {
161        if !mise_available() {
162            return;
163        }
164
165        let decorator = MiseDecorator;
166        let original = Command {
167            name: "standalone-command".into(),
168            args: vec![],
169            env: None,
170            dir: None,
171        };
172
173        let result = decorator.wrap(original).unwrap();
174        assert_eq!(result.name, "mise");
175        assert_eq!(result.args, vec!["exec", "--", "standalone-command"]);
176    }
177
178    #[test]
179    fn handles_command_with_multiple_args() {
180        if !mise_available() {
181            return;
182        }
183
184        let decorator = MiseDecorator;
185        let original = Command {
186            name: "kubectl".into(),
187            args: vec![
188                "get".into(),
189                "pods".into(),
190                "--namespace".into(),
191                "default".into(),
192                "-o".into(),
193                "json".into(),
194            ],
195            env: None,
196            dir: None,
197        };
198
199        let result = decorator.wrap(original).unwrap();
200        assert_eq!(result.name, "mise");
201        assert_eq!(
202            result.args,
203            vec![
204                "exec",
205                "--",
206                "kubectl",
207                "get",
208                "pods",
209                "--namespace",
210                "default",
211                "-o",
212                "json"
213            ]
214        );
215    }
216}