1use std::path::{Path, PathBuf};
23
24use crate::orchestrator::{absolute_program, ServiceState, ServiceUnit};
25
26pub const TEAMS_ENTRY: &str = "bin/teams.mjs";
28
29pub const TEAMS_PACKAGE: &str = "@volter-ai-dev/supercode-teams";
31
32pub const SERVICE_DIR: &str = "service";
34
35pub const SERVICE_NAME: &str = "dev.volter.supercode-teams-machine";
37
38pub fn connector_service_name(server_id: &str, team_id: &str, context: &str) -> String {
40 let mut hash = 0xcbf29ce484222325_u64;
43 for byte in [server_id, team_id, context].join("\0").bytes() {
44 hash ^= u64::from(byte);
45 hash = hash.wrapping_mul(0x100000001b3);
46 }
47 format!("dev.volter.supercode-teams-connector-{hash:016x}")
48}
49
50fn plist_text(value: &str) -> String {
51 value
52 .replace('&', "&")
53 .replace('<', "<")
54 .replace('>', ">")
55}
56
57fn service_text(value: &str) -> Result<&str, TeamsError> {
58 if value.chars().any(char::is_control) {
59 return Err(TeamsError::Service {
60 action: "render",
61 detail: "service parameters cannot contain control characters".into(),
62 });
63 }
64 Ok(value)
65}
66
67fn systemd_arg(value: &str) -> String {
68 format!(
69 "\"{}\"",
70 value
71 .replace('\\', "\\\\")
72 .replace('"', "\\\"")
73 .replace('%', "%%")
74 .replace('$', "$$")
75 )
76}
77
78pub fn connector_service_unit(
80 teams_home: &Path,
81 supercode_home: &Path,
82 entry: &Path,
83 node: &str,
84 supercode: &Path,
85 context: &str,
86 cwd: &Path,
87 server_id: &str,
88 team_id: &str,
89) -> Result<ServiceUnit, TeamsError> {
90 let teams_home_text = teams_home.display().to_string();
91 let supercode_home_text = supercode_home.display().to_string();
92 let entry_text = entry.display().to_string();
93 let supercode_text = supercode.display().to_string();
94 let workspace_text = cwd.display().to_string();
95 for value in [
96 teams_home_text.as_str(),
97 supercode_home_text.as_str(),
98 entry_text.as_str(),
99 node,
100 supercode_text.as_str(),
101 context,
102 workspace_text.as_str(),
103 server_id,
104 team_id,
105 ] {
106 service_text(value)?;
107 }
108 let label = connector_service_name(server_id, team_id, context);
109 let suffix = if cfg!(target_os = "macos") {
110 "plist"
111 } else {
112 "service"
113 };
114 let path = teams_home
115 .join(SERVICE_DIR)
116 .join(format!("{label}.{suffix}"));
117 let node = absolute_program(node);
118 let entry = entry_text;
119 let workspace = workspace_text;
120 let home = supercode_home_text;
121 let supercode = supercode_text;
122 if cfg!(target_os = "macos") {
123 let node = plist_text(&node);
124 let entry = plist_text(&entry);
125 let workspace = plist_text(&workspace);
126 let home = plist_text(&home);
127 let supercode = plist_text(&supercode);
128 let context = plist_text(context);
129 let text = format!(
130 r#"<?xml version="1.0" encoding="UTF-8"?>
131<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
132<plist version="1.0"><dict>
133 <key>Label</key><string>{label}</string>
134 <key>ProgramArguments</key><array><string>{node}</string><string>{entry}</string><string>teams</string><string>connect</string><string>--context</string><string>{context}</string><string>--cwd</string><string>{workspace}</string></array>
135 <key>EnvironmentVariables</key><dict><key>SUPERCODE_HOME</key><string>{home}</string><key>SUPERCODE_BIN</key><string>{supercode}</string></dict>
136 <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
137 <key>StandardOutPath</key><string>{}/service/{label}.out.log</string>
138 <key>StandardErrorPath</key><string>{}/service/{label}.err.log</string>
139</dict></plist>
140"#,
141 plist_text(&teams_home_text),
142 plist_text(&teams_home_text)
143 );
144 Ok(ServiceUnit {
145 kind: "launchd",
146 path: path.clone(),
147 text,
148 install_command: format!("launchctl bootstrap gui/$(id -u) {}", path.display()),
149 })
150 } else {
151 let environment_home = systemd_arg(&format!("SUPERCODE_HOME={home}"));
152 let environment_bin = systemd_arg(&format!("SUPERCODE_BIN={supercode}"));
153 let node = systemd_arg(&node);
154 let entry = systemd_arg(&entry);
155 let workspace = systemd_arg(&workspace);
156 let context_description = context.replace('%', "%%").replace('$', "$$");
157 let context = systemd_arg(context);
158 let text = format!("[Unit]\nDescription=supercode Teams connector ({context_description})\nAfter=network.target\n\n[Service]\nEnvironment={environment_home}\nEnvironment={environment_bin}\nExecStart={node} {entry} teams connect --context {context} --cwd {workspace}\nRestart=on-failure\nKillSignal=SIGTERM\n\n[Install]\nWantedBy=default.target\n");
159 Ok(ServiceUnit {
160 kind: "systemd",
161 path: path.clone(),
162 text,
163 install_command: format!(
164 "systemctl --user link {} && systemctl --user enable --now {label}",
165 path.display()
166 ),
167 })
168 }
169}
170
171#[derive(Debug, thiserror::Error)]
173pub enum TeamsError {
174 #[error("no teams entry found (looked for `sdk/teams/{TEAMS_ENTRY}` under: {searched}); install it with `npm install -g {TEAMS_PACKAGE}`")]
176 NoEntry {
177 searched: String,
179 },
180 #[error("teams service: {action} failed: {detail}")]
182 Service {
183 action: &'static str,
185 detail: String,
187 },
188 #[error("teams file `{}`: {source}", path.display())]
190 File {
191 path: PathBuf,
193 source: std::io::Error,
195 },
196}
197
198pub fn teams_home() -> PathBuf {
203 if let Ok(home) = std::env::var("SUPERCODE_TEAMS_HOME") {
204 if !home.is_empty() {
205 return PathBuf::from(home);
206 }
207 }
208 crate::agent::global_instructions_dir().join("teams")
209}
210
211pub fn teams_entry() -> Result<PathBuf, TeamsError> {
220 let mut searched = Vec::new();
221 if let Some(explicit) = std::env::var_os("SUPERCODE_TEAMS_ENTRY") {
222 let path = PathBuf::from(explicit);
223 if path.is_file() {
224 return Ok(path);
225 }
226 searched.push(path.display().to_string());
227 }
228 let mut roots: Vec<PathBuf> = Vec::new();
229 if let Ok(exe) = std::env::current_exe() {
230 roots.extend(exe.ancestors().skip(1).take(4).map(Path::to_path_buf));
232 }
233 if let Ok(cwd) = std::env::current_dir() {
234 roots.push(cwd);
235 }
236 if let Some(workspace) = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2) {
241 roots.push(workspace.to_path_buf());
242 }
243 for root in roots {
244 let candidate = root.join("sdk/teams").join(TEAMS_ENTRY);
245 if candidate.is_file() {
246 return Ok(candidate);
247 }
248 searched.push(candidate.display().to_string());
249 }
250 if let Some(global) = global_npm_root() {
251 let candidate = global.join(TEAMS_PACKAGE).join(TEAMS_ENTRY);
252 if candidate.is_file() {
253 return Ok(candidate);
254 }
255 searched.push(candidate.display().to_string());
256 }
257 Err(TeamsError::NoEntry {
258 searched: searched.join(", "),
259 })
260}
261
262fn global_npm_root() -> Option<PathBuf> {
264 let output = std::process::Command::new("npm")
265 .args(["root", "-g"])
266 .stdin(std::process::Stdio::null())
267 .stderr(std::process::Stdio::null())
268 .output()
269 .ok()?;
270 if !output.status.success() {
271 return None;
272 }
273 let text = String::from_utf8_lossy(&output.stdout);
274 let root = text.trim();
275 if root.is_empty() {
276 return None;
277 }
278 Some(PathBuf::from(root))
279}
280
281pub fn service_unit(home: &Path, entry: &Path, node: &str) -> ServiceUnit {
288 let home_display = home.display().to_string();
289 let entry_display = entry.display().to_string();
290 if cfg!(target_os = "macos") {
291 let path = home.join(SERVICE_DIR).join(format!("{SERVICE_NAME}.plist"));
292 let text = format!(
293 r#"<?xml version="1.0" encoding="UTF-8"?>
294<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
295<plist version="1.0">
296<dict>
297 <key>Label</key><string>{SERVICE_NAME}</string>
298 <key>ProgramArguments</key>
299 <array>
300 <string>{node}</string>
301 <string>{entry_display}</string>
302 <string>machine</string>
303 <string>start</string>
304 </array>
305 <key>EnvironmentVariables</key>
306 <dict>
307 <key>SUPERCODE_TEAMS_HOME</key><string>{home_display}</string>
308 </dict>
309 <key>RunAtLoad</key><true/>
310 <key>KeepAlive</key><true/>
311 <key>StandardOutPath</key><string>{home_display}/service/teams-machine.out.log</string>
312 <key>StandardErrorPath</key><string>{home_display}/service/teams-machine.err.log</string>
313</dict>
314</plist>
315"#
316 );
317 let install = format!("launchctl bootstrap gui/$(id -u) {}", path.display());
318 ServiceUnit {
319 kind: "launchd",
320 path,
321 text,
322 install_command: install,
323 }
324 } else {
325 let path = home
326 .join(SERVICE_DIR)
327 .join(format!("{SERVICE_NAME}.service"));
328 let text = format!(
329 "[Unit]\n\
330 Description=supercode teams machine daemon ({home_display})\n\
331 After=network.target\n\
332 \n\
333 [Service]\n\
334 Environment=SUPERCODE_TEAMS_HOME={home_display}\n\
335 ExecStart={node} {entry_display} machine start\n\
336 Restart=on-failure\n\
337 KillSignal=SIGTERM\n\
338 \n\
339 [Install]\n\
340 WantedBy=default.target\n"
341 );
342 let install = format!(
343 "systemctl --user link {} && systemctl --user enable --now {SERVICE_NAME}",
344 path.display()
345 );
346 ServiceUnit {
347 kind: "systemd",
348 path,
349 text,
350 install_command: install,
351 }
352 }
353}
354
355pub fn write_unit(unit: &ServiceUnit) -> Result<(), TeamsError> {
357 if let Some(parent) = unit.path.parent() {
358 std::fs::create_dir_all(parent).map_err(|source| TeamsError::File {
359 path: unit.path.clone(),
360 source,
361 })?;
362 }
363 std::fs::write(&unit.path, &unit.text).map_err(|source| TeamsError::File {
364 path: unit.path.clone(),
365 source,
366 })
367}
368
369fn run_tool(program: &str, args: &[&str]) -> Result<(bool, String), std::io::Error> {
371 let output = std::process::Command::new(program).args(args).output()?;
372 let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
373 text.push_str(&String::from_utf8_lossy(&output.stderr));
374 Ok((output.status.success(), text.trim().to_string()))
375}
376
377#[cfg(target_os = "macos")]
378fn gui_domain() -> String {
379 format!("gui/{}", unsafe { libc::getuid() })
381}
382
383pub fn service_status() -> ServiceState {
387 platform_status()
388}
389
390#[cfg(target_os = "macos")]
391fn platform_status() -> ServiceState {
392 let label = SERVICE_NAME.to_string();
393 let target = format!("{}/{SERVICE_NAME}", gui_domain());
394 match run_tool("launchctl", &["print", &target]) {
395 Ok((true, text)) => ServiceState {
396 kind: "launchd",
397 label,
398 installed: true,
399 pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
400 detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
401 },
402 Ok((false, _)) => ServiceState {
403 kind: "launchd",
404 label,
405 installed: false,
406 pid: None,
407 detail: format!("not bootstrapped in {}", gui_domain()),
408 },
409 Err(error) => ServiceState {
410 kind: "launchd",
411 label,
412 installed: false,
413 pid: None,
414 detail: format!("launchctl unavailable: {error}"),
415 },
416 }
417}
418
419#[cfg(all(unix, not(target_os = "macos")))]
420fn platform_status() -> ServiceState {
421 let label = SERVICE_NAME.to_string();
422 match run_tool("systemctl", &["--user", "is-active", SERVICE_NAME]) {
423 Ok((active, text)) => {
424 let known = run_tool("systemctl", &["--user", "is-enabled", SERVICE_NAME])
425 .map(|(ok, _)| ok)
426 .unwrap_or(false);
427 ServiceState {
428 kind: "systemd",
429 label,
430 installed: active || known,
431 pid: None,
432 detail: if text.is_empty() {
433 "unknown".into()
434 } else {
435 text
436 },
437 }
438 }
439 Err(error) => ServiceState {
440 kind: "systemd",
441 label,
442 installed: false,
443 pid: None,
444 detail: format!("systemctl unavailable: {error}"),
445 },
446 }
447}
448
449#[cfg(not(unix))]
450fn platform_status() -> ServiceState {
451 ServiceState {
452 kind: "none",
453 label: SERVICE_NAME.to_string(),
454 installed: false,
455 pid: None,
456 detail: "no service manager on this platform".into(),
457 }
458}
459
460#[cfg(target_os = "macos")]
462fn field_of(text: &str, key: &str) -> Option<String> {
463 text.lines()
464 .find_map(|line| line.trim().strip_prefix(key))
465 .map(|value| value.trim().to_string())
466}
467
468fn unit_file_name() -> String {
470 if cfg!(target_os = "macos") {
471 format!("{SERVICE_NAME}.plist")
472 } else {
473 format!("{SERVICE_NAME}.service")
474 }
475}
476
477pub fn install_service(
483 home: &Path,
484 entry: &Path,
485 node: &str,
486) -> Result<(ServiceUnit, ServiceState), TeamsError> {
487 let existing = service_status();
488 if existing.installed {
489 return Err(TeamsError::Service {
490 action: "install",
491 detail: format!(
492 "`{}` is already installed ({}); `supercode teams machine uninstall` first",
493 existing.label, existing.detail
494 ),
495 });
496 }
497 let unit = service_unit(home, entry, &absolute_program(node));
498 write_unit(&unit)?;
499 platform_install(&unit)?;
500 Ok((unit, service_status()))
501}
502
503pub fn install_connector_service(
506 unit: &ServiceUnit,
507 label: &str,
508) -> Result<ServiceState, TeamsError> {
509 let existing = named_service_status(label);
510 if unit.path.exists() {
511 let old = std::fs::read_to_string(&unit.path).map_err(|source| TeamsError::File {
512 path: unit.path.clone(),
513 source,
514 })?;
515 if old != unit.text {
516 return Err(TeamsError::Service { action: "install", detail: format!("`{label}` exists with different context or service parameters; disconnect it first") });
517 }
518 if existing.installed {
519 return Ok(existing);
520 }
521 } else if existing.installed {
522 return Err(TeamsError::Service {
523 action: "install",
524 detail: format!(
525 "service manager already owns `{label}` without its expected unit file"
526 ),
527 });
528 }
529 write_unit(unit)?;
530 named_platform_install(unit, label)?;
531 Ok(named_service_status(label))
532}
533
534pub fn uninstall_connector_service(
536 teams_home: &Path,
537 label: &str,
538) -> Result<ServiceState, TeamsError> {
539 named_platform_uninstall(label)?;
540 let suffix = if cfg!(target_os = "macos") {
541 "plist"
542 } else {
543 "service"
544 };
545 let path = teams_home
546 .join(SERVICE_DIR)
547 .join(format!("{label}.{suffix}"));
548 match std::fs::remove_file(&path) {
549 Ok(()) => {}
550 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
551 Err(source) => return Err(TeamsError::File { path, source }),
552 }
553 Ok(named_service_status(label))
554}
555
556pub fn connector_service_status(label: &str) -> ServiceState {
558 named_service_status(label)
559}
560
561#[cfg(target_os = "macos")]
562fn named_service_status(label: &str) -> ServiceState {
563 let target = format!("{}/{label}", gui_domain());
564 match run_tool("launchctl", &["print", &target]) {
565 Ok((true, text)) => ServiceState {
566 kind: "launchd",
567 label: label.into(),
568 installed: true,
569 pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
570 detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
571 },
572 Ok((false, _)) => ServiceState {
573 kind: "launchd",
574 label: label.into(),
575 installed: false,
576 pid: None,
577 detail: format!("not bootstrapped in {}", gui_domain()),
578 },
579 Err(error) => ServiceState {
580 kind: "launchd",
581 label: label.into(),
582 installed: false,
583 pid: None,
584 detail: format!("launchctl unavailable: {error}"),
585 },
586 }
587}
588
589#[cfg(all(unix, not(target_os = "macos")))]
590fn named_service_status(label: &str) -> ServiceState {
591 match run_tool("systemctl", &["--user", "is-active", label]) {
592 Ok((active, text)) => {
593 let known = run_tool("systemctl", &["--user", "is-enabled", label])
594 .map(|(ok, _)| ok)
595 .unwrap_or(false);
596 ServiceState {
597 kind: "systemd",
598 label: label.into(),
599 installed: active || known,
600 pid: None,
601 detail: if text.is_empty() {
602 "unknown".into()
603 } else {
604 text
605 },
606 }
607 }
608 Err(error) => ServiceState {
609 kind: "systemd",
610 label: label.into(),
611 installed: false,
612 pid: None,
613 detail: format!("systemctl unavailable: {error}"),
614 },
615 }
616}
617
618#[cfg(not(unix))]
619fn named_service_status(label: &str) -> ServiceState {
620 ServiceState {
621 kind: "none",
622 label: label.into(),
623 installed: false,
624 pid: None,
625 detail: "no service manager on this platform".into(),
626 }
627}
628
629#[cfg(target_os = "macos")]
630fn named_platform_install(unit: &ServiceUnit, _label: &str) -> Result<(), TeamsError> {
631 platform_install(unit)
632}
633#[cfg(all(unix, not(target_os = "macos")))]
634fn named_platform_install(unit: &ServiceUnit, label: &str) -> Result<(), TeamsError> {
635 let path = unit.path.display().to_string();
636 for args in [
637 vec!["--user", "link", path.as_str()],
638 vec!["--user", "enable", "--now", label],
639 ] {
640 let (ok, text) = run_tool("systemctl", &args).map_err(|error| TeamsError::Service {
641 action: "install",
642 detail: format!("systemctl: {error}"),
643 })?;
644 if !ok {
645 return Err(TeamsError::Service {
646 action: "install",
647 detail: format!("systemctl {}: {text}", args.join(" ")),
648 });
649 }
650 }
651 Ok(())
652}
653#[cfg(not(unix))]
654fn named_platform_install(_unit: &ServiceUnit, _label: &str) -> Result<(), TeamsError> {
655 Err(TeamsError::Service {
656 action: "install",
657 detail: "no service manager on this platform".into(),
658 })
659}
660
661#[cfg(target_os = "macos")]
662fn named_platform_uninstall(label: &str) -> Result<(), TeamsError> {
663 let target = format!("{}/{label}", gui_domain());
664 let (ok, text) =
665 run_tool("launchctl", &["bootout", &target]).map_err(|error| TeamsError::Service {
666 action: "uninstall",
667 detail: format!("launchctl: {error}"),
668 })?;
669 if !ok && !text.contains("No such process") && !text.contains("not find") {
670 return Err(TeamsError::Service {
671 action: "uninstall",
672 detail: format!("launchctl bootout {target}: {text}"),
673 });
674 }
675 Ok(())
676}
677#[cfg(all(unix, not(target_os = "macos")))]
678fn named_platform_uninstall(label: &str) -> Result<(), TeamsError> {
679 let _ = run_tool("systemctl", &["--user", "disable", "--now", label]);
680 Ok(())
681}
682#[cfg(not(unix))]
683fn named_platform_uninstall(_label: &str) -> Result<(), TeamsError> {
684 Ok(())
685}
686
687#[cfg(target_os = "macos")]
688fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
689 let path = unit.path.display().to_string();
690 let (ok, text) =
691 run_tool("launchctl", &["bootstrap", &gui_domain(), &path]).map_err(|error| {
692 TeamsError::Service {
693 action: "install",
694 detail: format!("launchctl: {error}"),
695 }
696 })?;
697 if !ok {
698 return Err(TeamsError::Service {
699 action: "install",
700 detail: format!("launchctl bootstrap {}: {text}", gui_domain()),
701 });
702 }
703 Ok(())
704}
705
706#[cfg(all(unix, not(target_os = "macos")))]
709fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
710 let path = unit.path.display().to_string();
711 for args in [
712 vec!["--user", "link", path.as_str()],
713 vec!["--user", "enable", "--now", SERVICE_NAME],
714 ] {
715 let (ok, text) = run_tool("systemctl", &args).map_err(|error| TeamsError::Service {
716 action: "install",
717 detail: format!("systemctl: {error}"),
718 })?;
719 if !ok {
720 return Err(TeamsError::Service {
721 action: "install",
722 detail: format!("systemctl {}: {text}", args.join(" ")),
723 });
724 }
725 }
726 Ok(())
727}
728
729#[cfg(not(unix))]
730fn platform_install(_unit: &ServiceUnit) -> Result<(), TeamsError> {
731 Err(TeamsError::Service {
732 action: "install",
733 detail: "no service manager on this platform".into(),
734 })
735}
736
737pub fn uninstall_service(home: &Path) -> Result<ServiceState, TeamsError> {
742 platform_uninstall()?;
743 let unit_path = home.join(SERVICE_DIR).join(unit_file_name());
744 match std::fs::remove_file(&unit_path) {
745 Ok(()) => {}
746 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
747 Err(source) => {
748 return Err(TeamsError::File {
749 path: unit_path,
750 source,
751 })
752 }
753 }
754 let mut state = service_status();
757 for _ in 0..40 {
758 if !state.installed {
759 break;
760 }
761 std::thread::sleep(std::time::Duration::from_millis(100));
762 state = service_status();
763 }
764 Ok(state)
765}
766
767#[cfg(target_os = "macos")]
768fn platform_uninstall() -> Result<(), TeamsError> {
769 let target = format!("{}/{SERVICE_NAME}", gui_domain());
770 let (ok, text) =
771 run_tool("launchctl", &["bootout", &target]).map_err(|error| TeamsError::Service {
772 action: "uninstall",
773 detail: format!("launchctl: {error}"),
774 })?;
775 if !ok && !text.contains("No such process") && !text.contains("not find") {
777 return Err(TeamsError::Service {
778 action: "uninstall",
779 detail: format!("launchctl bootout {target}: {text}"),
780 });
781 }
782 Ok(())
783}
784
785#[cfg(all(unix, not(target_os = "macos")))]
786fn platform_uninstall() -> Result<(), TeamsError> {
787 let _ = run_tool("systemctl", &["--user", "disable", "--now", SERVICE_NAME]);
788 Ok(())
789}
790
791#[cfg(not(unix))]
792fn platform_uninstall() -> Result<(), TeamsError> {
793 Ok(())
794}
795
796#[cfg(test)]
797mod connector_service_tests {
798 use super::*;
799
800 #[test]
801 fn connector_unit_is_context_scoped_and_contains_no_credential() {
802 let unit = connector_service_unit(
803 Path::new("/tmp/teams home"),
804 Path::new("/tmp/supercode home"),
805 Path::new("/tmp/sdk/teams/bin/teams.mjs"),
806 "/usr/bin/node",
807 Path::new("/tmp/bin/supercode"),
808 "work",
809 Path::new("/tmp/project with spaces"),
810 "srv_1",
811 "team_1",
812 )
813 .unwrap();
814 assert!(unit.text.contains("teams"));
815 assert!(unit.text.contains("connect"));
816 assert!(unit.text.contains("work"));
817 assert!(!unit.text.contains("credential"));
818 assert!(unit
819 .path
820 .file_name()
821 .unwrap()
822 .to_string_lossy()
823 .contains(&connector_service_name("srv_1", "team_1", "work")));
824 }
825
826 #[test]
827 fn connector_labels_separate_context_and_team() {
828 assert_ne!(
829 connector_service_name("srv", "team-a", "work"),
830 connector_service_name("srv", "team-b", "work")
831 );
832 assert_ne!(
833 connector_service_name("srv", "team-a", "work"),
834 connector_service_name("srv", "team-a", "personal")
835 );
836 }
837
838 #[test]
839 fn connector_unit_rejects_newlines_and_escapes_service_syntax() {
840 let unsafe_unit = connector_service_unit(
841 Path::new("/tmp/teams"),
842 Path::new("/tmp/home"),
843 Path::new("/tmp/entry"),
844 "/usr/bin/node",
845 Path::new("/tmp/supercode"),
846 "bad\ncontext",
847 Path::new("/tmp/work"),
848 "srv",
849 "team",
850 );
851 assert!(unsafe_unit.is_err());
852 let unit = connector_service_unit(
853 Path::new("/tmp/teams & logs"),
854 Path::new("/tmp/home $x"),
855 Path::new("/tmp/entry %i"),
856 "/usr/bin/node",
857 Path::new("/tmp/super\"code"),
858 "work",
859 Path::new("/tmp/a & b % $"),
860 "srv",
861 "team",
862 )
863 .unwrap();
864 if cfg!(target_os = "macos") {
865 assert!(unit.text.contains("&"));
866 } else {
867 assert!(unit.text.contains("%%"));
868 assert!(unit.text.contains("$$"));
869 assert!(unit.text.contains("\\\""));
870 }
871 }
872}