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 search_path = plist_text(&service_path());
130 let text = format!(
131 r#"<?xml version="1.0" encoding="UTF-8"?>
132<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
133<plist version="1.0"><dict>
134 <key>Label</key><string>{label}</string>
135 <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>
136 <key>EnvironmentVariables</key><dict><key>SUPERCODE_HOME</key><string>{home}</string><key>SUPERCODE_BIN</key><string>{supercode}</string><key>PATH</key><string>{search_path}</string></dict>
137 <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
138 <key>StandardOutPath</key><string>{}/service/{label}.out.log</string>
139 <key>StandardErrorPath</key><string>{}/service/{label}.err.log</string>
140</dict></plist>
141"#,
142 plist_text(&teams_home_text),
143 plist_text(&teams_home_text)
144 );
145 Ok(ServiceUnit {
146 kind: "launchd",
147 path: path.clone(),
148 text,
149 install_command: format!("launchctl bootstrap gui/$(id -u) {}", path.display()),
150 })
151 } else {
152 let environment_home = systemd_arg(&format!("SUPERCODE_HOME={home}"));
153 let environment_bin = systemd_arg(&format!("SUPERCODE_BIN={supercode}"));
154 let environment_path = systemd_arg(&format!("PATH={}", service_path()));
155 let node = systemd_arg(&node);
156 let entry = systemd_arg(&entry);
157 let workspace = systemd_arg(&workspace);
158 let context_description = context.replace('%', "%%").replace('$', "$$");
159 let context = systemd_arg(context);
160 let text = format!("[Unit]\nDescription=supercode Teams connector ({context_description})\nAfter=network.target\n\n[Service]\nEnvironment={environment_home}\nEnvironment={environment_bin}\nEnvironment={environment_path}\nExecStart={node} {entry} teams connect --context {context} --cwd {workspace}\nRestart=on-failure\nKillSignal=SIGTERM\n\n[Install]\nWantedBy=default.target\n");
161 Ok(ServiceUnit {
162 kind: "systemd",
163 path: path.clone(),
164 text,
165 install_command: format!(
166 "systemctl --user link {} && systemctl --user enable --now {label}",
167 path.display()
168 ),
169 })
170 }
171}
172
173#[derive(Debug, thiserror::Error)]
175pub enum TeamsError {
176 #[error("no teams entry found (looked for `sdk/teams/{TEAMS_ENTRY}` under: {searched}); install it with `npm install -g {TEAMS_PACKAGE}`")]
178 NoEntry {
179 searched: String,
181 },
182 #[error("teams service: {action} failed: {detail}")]
184 Service {
185 action: &'static str,
187 detail: String,
189 },
190 #[error("teams file `{}`: {source}", path.display())]
192 File {
193 path: PathBuf,
195 source: std::io::Error,
197 },
198}
199
200fn service_path() -> String {
204 std::env::var("PATH")
205 .ok()
206 .filter(|path| !path.trim().is_empty())
207 .unwrap_or_else(|| "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin".into())
208}
209
210pub fn teams_home() -> PathBuf {
215 if let Ok(home) = std::env::var("SUPERCODE_TEAMS_HOME") {
216 if !home.is_empty() {
217 return PathBuf::from(home);
218 }
219 }
220 crate::agent::global_instructions_dir().join("teams")
221}
222
223pub fn teams_entry() -> Result<PathBuf, TeamsError> {
233 let mut searched = Vec::new();
234 if let Some(explicit) = std::env::var_os("SUPERCODE_TEAMS_ENTRY") {
235 let path = PathBuf::from(explicit);
236 if path.is_file() {
237 return Ok(path);
238 }
239 searched.push(path.display().to_string());
240 }
241 let mut roots: Vec<PathBuf> = Vec::new();
242 if let Ok(exe) = std::env::current_exe() {
243 roots.extend(exe.ancestors().skip(1).take(4).map(Path::to_path_buf));
245 }
246 if let Some(workspace) = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2) {
251 roots.push(workspace.to_path_buf());
252 }
253 for root in roots {
254 let candidate = root.join("sdk/teams").join(TEAMS_ENTRY);
255 if candidate.is_file() {
256 return Ok(candidate);
257 }
258 searched.push(candidate.display().to_string());
259 }
260 if let Some(global) = global_npm_root() {
261 let candidate = global.join(TEAMS_PACKAGE).join(TEAMS_ENTRY);
262 if candidate.is_file() {
263 return Ok(candidate);
264 }
265 searched.push(candidate.display().to_string());
266 }
267 Err(TeamsError::NoEntry {
268 searched: searched.join(", "),
269 })
270}
271
272fn global_npm_root() -> Option<PathBuf> {
274 let output = std::process::Command::new("npm")
275 .args(["root", "-g"])
276 .stdin(std::process::Stdio::null())
277 .stderr(std::process::Stdio::null())
278 .output()
279 .ok()?;
280 if !output.status.success() {
281 return None;
282 }
283 let text = String::from_utf8_lossy(&output.stdout);
284 let root = text.trim();
285 if root.is_empty() {
286 return None;
287 }
288 Some(PathBuf::from(root))
289}
290
291pub fn service_unit(home: &Path, entry: &Path, node: &str) -> ServiceUnit {
298 let home_display = home.display().to_string();
299 let entry_display = entry.display().to_string();
300 if cfg!(target_os = "macos") {
301 let path = home.join(SERVICE_DIR).join(format!("{SERVICE_NAME}.plist"));
302 let text = format!(
303 r#"<?xml version="1.0" encoding="UTF-8"?>
304<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
305<plist version="1.0">
306<dict>
307 <key>Label</key><string>{SERVICE_NAME}</string>
308 <key>ProgramArguments</key>
309 <array>
310 <string>{node}</string>
311 <string>{entry_display}</string>
312 <string>machine</string>
313 <string>start</string>
314 </array>
315 <key>EnvironmentVariables</key>
316 <dict>
317 <key>SUPERCODE_TEAMS_HOME</key><string>{home_display}</string>
318 </dict>
319 <key>RunAtLoad</key><true/>
320 <key>KeepAlive</key><true/>
321 <key>StandardOutPath</key><string>{home_display}/service/teams-machine.out.log</string>
322 <key>StandardErrorPath</key><string>{home_display}/service/teams-machine.err.log</string>
323</dict>
324</plist>
325"#
326 );
327 let install = format!("launchctl bootstrap gui/$(id -u) {}", path.display());
328 ServiceUnit {
329 kind: "launchd",
330 path,
331 text,
332 install_command: install,
333 }
334 } else {
335 let path = home
336 .join(SERVICE_DIR)
337 .join(format!("{SERVICE_NAME}.service"));
338 let text = format!(
339 "[Unit]\n\
340 Description=supercode teams machine daemon ({home_display})\n\
341 After=network.target\n\
342 \n\
343 [Service]\n\
344 Environment=SUPERCODE_TEAMS_HOME={home_display}\n\
345 ExecStart={node} {entry_display} machine start\n\
346 Restart=on-failure\n\
347 KillSignal=SIGTERM\n\
348 \n\
349 [Install]\n\
350 WantedBy=default.target\n"
351 );
352 let install = format!(
353 "systemctl --user link {} && systemctl --user enable --now {SERVICE_NAME}",
354 path.display()
355 );
356 ServiceUnit {
357 kind: "systemd",
358 path,
359 text,
360 install_command: install,
361 }
362 }
363}
364
365pub fn write_unit(unit: &ServiceUnit) -> Result<(), TeamsError> {
367 if let Some(parent) = unit.path.parent() {
368 std::fs::create_dir_all(parent).map_err(|source| TeamsError::File {
369 path: unit.path.clone(),
370 source,
371 })?;
372 }
373 std::fs::write(&unit.path, &unit.text).map_err(|source| TeamsError::File {
374 path: unit.path.clone(),
375 source,
376 })
377}
378
379fn run_tool(program: &str, args: &[&str]) -> Result<(bool, String), std::io::Error> {
381 let output = std::process::Command::new(program).args(args).output()?;
382 let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
383 text.push_str(&String::from_utf8_lossy(&output.stderr));
384 Ok((output.status.success(), text.trim().to_string()))
385}
386
387#[cfg(target_os = "macos")]
388fn gui_domain() -> String {
389 format!("gui/{}", unsafe { libc::getuid() })
391}
392
393pub fn service_status() -> ServiceState {
397 platform_status()
398}
399
400#[cfg(target_os = "macos")]
401fn platform_status() -> ServiceState {
402 let label = SERVICE_NAME.to_string();
403 let target = format!("{}/{SERVICE_NAME}", gui_domain());
404 match run_tool("launchctl", &["print", &target]) {
405 Ok((true, text)) => ServiceState {
406 kind: "launchd",
407 label,
408 installed: true,
409 pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
410 detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
411 },
412 Ok((false, _)) => ServiceState {
413 kind: "launchd",
414 label,
415 installed: false,
416 pid: None,
417 detail: format!("not bootstrapped in {}", gui_domain()),
418 },
419 Err(error) => ServiceState {
420 kind: "launchd",
421 label,
422 installed: false,
423 pid: None,
424 detail: format!("launchctl unavailable: {error}"),
425 },
426 }
427}
428
429#[cfg(all(unix, not(target_os = "macos")))]
430fn platform_status() -> ServiceState {
431 let label = SERVICE_NAME.to_string();
432 match run_tool("systemctl", &["--user", "is-active", SERVICE_NAME]) {
433 Ok((active, text)) => {
434 let known = run_tool("systemctl", &["--user", "is-enabled", SERVICE_NAME])
435 .map(|(ok, _)| ok)
436 .unwrap_or(false);
437 ServiceState {
438 kind: "systemd",
439 label,
440 installed: active || known,
441 pid: None,
442 detail: if text.is_empty() {
443 "unknown".into()
444 } else {
445 text
446 },
447 }
448 }
449 Err(error) => ServiceState {
450 kind: "systemd",
451 label,
452 installed: false,
453 pid: None,
454 detail: format!("systemctl unavailable: {error}"),
455 },
456 }
457}
458
459#[cfg(not(unix))]
460fn platform_status() -> ServiceState {
461 ServiceState {
462 kind: "none",
463 label: SERVICE_NAME.to_string(),
464 installed: false,
465 pid: None,
466 detail: "no service manager on this platform".into(),
467 }
468}
469
470#[cfg(target_os = "macos")]
472fn field_of(text: &str, key: &str) -> Option<String> {
473 text.lines()
474 .find_map(|line| line.trim().strip_prefix(key))
475 .map(|value| value.trim().to_string())
476}
477
478fn unit_file_name() -> String {
480 if cfg!(target_os = "macos") {
481 format!("{SERVICE_NAME}.plist")
482 } else {
483 format!("{SERVICE_NAME}.service")
484 }
485}
486
487pub fn install_service(
493 home: &Path,
494 entry: &Path,
495 node: &str,
496) -> Result<(ServiceUnit, ServiceState), TeamsError> {
497 let existing = service_status();
498 if existing.installed {
499 return Err(TeamsError::Service {
500 action: "install",
501 detail: format!(
502 "`{}` is already installed ({}); `supercode teams machine uninstall` first",
503 existing.label, existing.detail
504 ),
505 });
506 }
507 let unit = service_unit(home, entry, &absolute_program(node));
508 write_unit(&unit)?;
509 platform_install(&unit)?;
510 Ok((unit, service_status()))
511}
512
513pub fn install_connector_service(
519 unit: &ServiceUnit,
520 label: &str,
521) -> Result<ServiceState, TeamsError> {
522 let existing = named_service_status(label);
523 if unit.path.exists() {
524 let old = std::fs::read_to_string(&unit.path).map_err(|source| TeamsError::File {
525 path: unit.path.clone(),
526 source,
527 })?;
528 if old == unit.text && existing.installed {
529 return Ok(existing);
530 }
531 if old != unit.text && existing.installed {
532 named_platform_uninstall(label)?;
533 }
534 } else if existing.installed {
535 return Err(TeamsError::Service {
536 action: "install",
537 detail: format!(
538 "service manager already owns `{label}` without its expected unit file"
539 ),
540 });
541 }
542 write_unit(unit)?;
543 named_platform_install(unit, label)?;
544 Ok(named_service_status(label))
545}
546
547pub fn uninstall_connector_service(
549 teams_home: &Path,
550 label: &str,
551) -> Result<ServiceState, TeamsError> {
552 named_platform_uninstall(label)?;
553 let suffix = if cfg!(target_os = "macos") {
554 "plist"
555 } else {
556 "service"
557 };
558 let path = teams_home
559 .join(SERVICE_DIR)
560 .join(format!("{label}.{suffix}"));
561 match std::fs::remove_file(&path) {
562 Ok(()) => {}
563 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
564 Err(source) => return Err(TeamsError::File { path, source }),
565 }
566 Ok(named_service_status(label))
567}
568
569pub fn connector_service_status(label: &str) -> ServiceState {
571 named_service_status(label)
572}
573
574#[cfg(target_os = "macos")]
575fn named_service_status(label: &str) -> ServiceState {
576 let target = format!("{}/{label}", gui_domain());
577 match run_tool("launchctl", &["print", &target]) {
578 Ok((true, text)) => ServiceState {
579 kind: "launchd",
580 label: label.into(),
581 installed: true,
582 pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
583 detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
584 },
585 Ok((false, _)) => ServiceState {
586 kind: "launchd",
587 label: label.into(),
588 installed: false,
589 pid: None,
590 detail: format!("not bootstrapped in {}", gui_domain()),
591 },
592 Err(error) => ServiceState {
593 kind: "launchd",
594 label: label.into(),
595 installed: false,
596 pid: None,
597 detail: format!("launchctl unavailable: {error}"),
598 },
599 }
600}
601
602#[cfg(all(unix, not(target_os = "macos")))]
603fn named_service_status(label: &str) -> ServiceState {
604 match run_tool("systemctl", &["--user", "is-active", label]) {
605 Ok((active, text)) => {
606 let known = run_tool("systemctl", &["--user", "is-enabled", label])
607 .map(|(ok, _)| ok)
608 .unwrap_or(false);
609 ServiceState {
610 kind: "systemd",
611 label: label.into(),
612 installed: active || known,
613 pid: None,
614 detail: if text.is_empty() {
615 "unknown".into()
616 } else {
617 text
618 },
619 }
620 }
621 Err(error) => ServiceState {
622 kind: "systemd",
623 label: label.into(),
624 installed: false,
625 pid: None,
626 detail: format!("systemctl unavailable: {error}"),
627 },
628 }
629}
630
631#[cfg(not(unix))]
632fn named_service_status(label: &str) -> ServiceState {
633 ServiceState {
634 kind: "none",
635 label: label.into(),
636 installed: false,
637 pid: None,
638 detail: "no service manager on this platform".into(),
639 }
640}
641
642#[cfg(target_os = "macos")]
643fn named_platform_install(unit: &ServiceUnit, _label: &str) -> Result<(), TeamsError> {
644 platform_install(unit)
645}
646#[cfg(all(unix, not(target_os = "macos")))]
647fn named_platform_install(unit: &ServiceUnit, label: &str) -> Result<(), TeamsError> {
648 let path = unit.path.display().to_string();
649 for args in [
650 vec!["--user", "link", path.as_str()],
651 vec!["--user", "enable", "--now", label],
652 ] {
653 let (ok, text) = run_tool("systemctl", &args).map_err(|error| TeamsError::Service {
654 action: "install",
655 detail: format!("systemctl: {error}"),
656 })?;
657 if !ok {
658 return Err(TeamsError::Service {
659 action: "install",
660 detail: format!("systemctl {}: {text}", args.join(" ")),
661 });
662 }
663 }
664 Ok(())
665}
666#[cfg(not(unix))]
667fn named_platform_install(_unit: &ServiceUnit, _label: &str) -> Result<(), TeamsError> {
668 Err(TeamsError::Service {
669 action: "install",
670 detail: "no service manager on this platform".into(),
671 })
672}
673
674#[cfg(target_os = "macos")]
675fn named_platform_uninstall(label: &str) -> Result<(), TeamsError> {
676 let target = format!("{}/{label}", gui_domain());
677 let (ok, text) =
678 run_tool("launchctl", &["bootout", &target]).map_err(|error| TeamsError::Service {
679 action: "uninstall",
680 detail: format!("launchctl: {error}"),
681 })?;
682 if !ok && !text.contains("No such process") && !text.contains("not find") {
683 return Err(TeamsError::Service {
684 action: "uninstall",
685 detail: format!("launchctl bootout {target}: {text}"),
686 });
687 }
688 for _ in 0..50 {
691 if !named_service_status(label).installed {
692 break;
693 }
694 std::thread::sleep(std::time::Duration::from_millis(100));
695 }
696 Ok(())
697}
698#[cfg(all(unix, not(target_os = "macos")))]
699fn named_platform_uninstall(label: &str) -> Result<(), TeamsError> {
700 let _ = run_tool("systemctl", &["--user", "disable", "--now", label]);
701 Ok(())
702}
703#[cfg(not(unix))]
704fn named_platform_uninstall(_label: &str) -> Result<(), TeamsError> {
705 Ok(())
706}
707
708#[cfg(target_os = "macos")]
709fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
710 let path = unit.path.display().to_string();
711 let (ok, text) =
712 run_tool("launchctl", &["bootstrap", &gui_domain(), &path]).map_err(|error| {
713 TeamsError::Service {
714 action: "install",
715 detail: format!("launchctl: {error}"),
716 }
717 })?;
718 if !ok {
719 return Err(TeamsError::Service {
720 action: "install",
721 detail: format!("launchctl bootstrap {}: {text}", gui_domain()),
722 });
723 }
724 Ok(())
725}
726
727#[cfg(all(unix, not(target_os = "macos")))]
730fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
731 let path = unit.path.display().to_string();
732 for args in [
733 vec!["--user", "link", path.as_str()],
734 vec!["--user", "enable", "--now", SERVICE_NAME],
735 ] {
736 let (ok, text) = run_tool("systemctl", &args).map_err(|error| TeamsError::Service {
737 action: "install",
738 detail: format!("systemctl: {error}"),
739 })?;
740 if !ok {
741 return Err(TeamsError::Service {
742 action: "install",
743 detail: format!("systemctl {}: {text}", args.join(" ")),
744 });
745 }
746 }
747 Ok(())
748}
749
750#[cfg(not(unix))]
751fn platform_install(_unit: &ServiceUnit) -> Result<(), TeamsError> {
752 Err(TeamsError::Service {
753 action: "install",
754 detail: "no service manager on this platform".into(),
755 })
756}
757
758pub fn uninstall_service(home: &Path) -> Result<ServiceState, TeamsError> {
763 platform_uninstall()?;
764 let unit_path = home.join(SERVICE_DIR).join(unit_file_name());
765 match std::fs::remove_file(&unit_path) {
766 Ok(()) => {}
767 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
768 Err(source) => {
769 return Err(TeamsError::File {
770 path: unit_path,
771 source,
772 })
773 }
774 }
775 let mut state = service_status();
778 for _ in 0..40 {
779 if !state.installed {
780 break;
781 }
782 std::thread::sleep(std::time::Duration::from_millis(100));
783 state = service_status();
784 }
785 Ok(state)
786}
787
788#[cfg(target_os = "macos")]
789fn platform_uninstall() -> Result<(), TeamsError> {
790 let target = format!("{}/{SERVICE_NAME}", gui_domain());
791 let (ok, text) =
792 run_tool("launchctl", &["bootout", &target]).map_err(|error| TeamsError::Service {
793 action: "uninstall",
794 detail: format!("launchctl: {error}"),
795 })?;
796 if !ok && !text.contains("No such process") && !text.contains("not find") {
798 return Err(TeamsError::Service {
799 action: "uninstall",
800 detail: format!("launchctl bootout {target}: {text}"),
801 });
802 }
803 Ok(())
804}
805
806#[cfg(all(unix, not(target_os = "macos")))]
807fn platform_uninstall() -> Result<(), TeamsError> {
808 let _ = run_tool("systemctl", &["--user", "disable", "--now", SERVICE_NAME]);
809 Ok(())
810}
811
812#[cfg(not(unix))]
813fn platform_uninstall() -> Result<(), TeamsError> {
814 Ok(())
815}
816
817#[cfg(test)]
818mod connector_service_tests {
819 use super::*;
820
821 #[test]
822 fn connector_unit_is_context_scoped_and_contains_no_credential() {
823 let unit = connector_service_unit(
824 Path::new("/tmp/teams home"),
825 Path::new("/tmp/supercode home"),
826 Path::new("/tmp/sdk/teams/bin/teams.mjs"),
827 "/usr/bin/node",
828 Path::new("/tmp/bin/supercode"),
829 "work",
830 Path::new("/tmp/project with spaces"),
831 "srv_1",
832 "team_1",
833 )
834 .unwrap();
835 assert!(unit.text.contains("teams"));
836 assert!(unit.text.contains("connect"));
837 assert!(unit.text.contains("work"));
838 assert!(!unit.text.contains("credential"));
839 assert!(unit
840 .path
841 .file_name()
842 .unwrap()
843 .to_string_lossy()
844 .contains(&connector_service_name("srv_1", "team_1", "work")));
845 }
846
847 #[test]
848 fn connector_labels_separate_context_and_team() {
849 assert_ne!(
850 connector_service_name("srv", "team-a", "work"),
851 connector_service_name("srv", "team-b", "work")
852 );
853 assert_ne!(
854 connector_service_name("srv", "team-a", "work"),
855 connector_service_name("srv", "team-a", "personal")
856 );
857 }
858
859 #[test]
860 fn connector_unit_rejects_newlines_and_escapes_service_syntax() {
861 let unsafe_unit = connector_service_unit(
862 Path::new("/tmp/teams"),
863 Path::new("/tmp/home"),
864 Path::new("/tmp/entry"),
865 "/usr/bin/node",
866 Path::new("/tmp/supercode"),
867 "bad\ncontext",
868 Path::new("/tmp/work"),
869 "srv",
870 "team",
871 );
872 assert!(unsafe_unit.is_err());
873 let unit = connector_service_unit(
874 Path::new("/tmp/teams & logs"),
875 Path::new("/tmp/home $x"),
876 Path::new("/tmp/entry %i"),
877 "/usr/bin/node",
878 Path::new("/tmp/super\"code"),
879 "work",
880 Path::new("/tmp/a & b % $"),
881 "srv",
882 "team",
883 )
884 .unwrap();
885 if cfg!(target_os = "macos") {
886 assert!(unit.text.contains("&"));
887 } else {
888 assert!(unit.text.contains("%%"));
889 assert!(unit.text.contains("$$"));
890 assert!(unit.text.contains("\\\""));
891 }
892 }
893}