1#[cfg(unix)]
11use std::os::unix::fs::symlink;
12
13use std::path::{Path, PathBuf};
14
15use super::alc_toml::{self, add_package_entry, validate_package_name, PackageDep};
16use super::project::resolve_project_root;
17#[cfg(unix)]
18use super::resolve::packages_dir;
19use super::AppService;
20
21impl AppService {
22 pub async fn pkg_link(
33 &self,
34 path: String,
35 name: Option<String>,
36 force: Option<bool>,
37 scope: Option<String>,
38 project_root: Option<String>,
39 ) -> Result<String, String> {
40 let scope_str = scope.as_deref().unwrap_or("global");
41 match scope_str {
42 "global" => self.pkg_link_global(path, name, force).await,
43 "variant" => {
44 if force == Some(true) {
50 return Err(
51 "force is not supported with scope='variant' (variant scope writes \
52 alc.local.toml; there is no filesystem destination to overwrite)"
53 .to_string(),
54 );
55 }
56 self.pkg_link_variant(path, name, project_root).await
57 }
58 other => Err(format!(
59 "invalid scope: '{other}' (expected 'global' or 'variant')"
60 )),
61 }
62 }
63
64 async fn pkg_link_global(
66 &self,
67 path: String,
68 name: Option<String>,
69 force: Option<bool>,
70 ) -> Result<String, String> {
71 #[cfg(not(unix))]
72 {
73 let _ = (path, name, force);
74 return Err(
75 "pkg_link scope='global' is not supported on non-Unix platforms".to_string(),
76 );
77 }
78
79 #[cfg(unix)]
80 {
81 let force = force.unwrap_or(false);
82
83 let raw = Path::new(&path);
85 let source: PathBuf = if raw.is_absolute() {
86 raw.to_path_buf()
87 } else {
88 std::env::current_dir()
89 .map_err(|e| format!("Cannot determine cwd: {e}"))?
90 .join(raw)
91 };
92
93 if !source.is_dir() {
94 return Err(format!("Path is not a directory: {}", source.display()));
95 }
96
97 let mode = detect_mode(&source)?;
99
100 let pkgs = packages_dir()?;
102 std::fs::create_dir_all(&pkgs)
103 .map_err(|e| format!("Cannot create packages dir {}: {e}", pkgs.display()))?;
104
105 let mode_str;
107 let mut linked_names: Vec<String> = Vec::new();
108 let mut targets: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
109
110 match mode {
111 PackageMode::Single => {
112 mode_str = "single";
113 let pkg_name = if let Some(n) = name {
114 n
115 } else {
116 source
117 .file_name()
118 .ok_or_else(|| {
119 format!("Cannot determine package name from: {}", source.display())
120 })?
121 .to_string_lossy()
122 .to_string()
123 };
124 validate_package_name(&pkg_name)?;
125
126 let dest = pkgs.join(&pkg_name);
127 create_symlink(&source, &dest, force)?;
128
129 targets.insert(
130 pkg_name.clone(),
131 serde_json::Value::String(source.display().to_string()),
132 );
133 linked_names.push(pkg_name);
134 }
135 PackageMode::Collection => {
136 mode_str = "collection";
137 let entries = std::fs::read_dir(&source).map_err(|e| {
138 format!("Failed to read directory {}: {e}", source.display())
139 })?;
140
141 for entry in entries {
142 let entry =
143 entry.map_err(|e| format!("Failed to read directory entry: {e}"))?;
144 let pkg_path = entry.path();
145 if !pkg_path.is_dir() || !pkg_path.join("init.lua").exists() {
147 continue;
148 }
149 let pkg_name = entry.file_name().to_string_lossy().to_string();
150 validate_package_name(&pkg_name)?;
151
152 let dest = pkgs.join(&pkg_name);
153 create_symlink(&pkg_path, &dest, force)?;
154
155 targets.insert(
156 pkg_name.clone(),
157 serde_json::Value::String(pkg_path.display().to_string()),
158 );
159 linked_names.push(pkg_name);
160 }
161
162 if linked_names.is_empty() {
163 return Err(format!(
164 "No init.lua found in any subdirectory of: {}",
165 source.display()
166 ));
167 }
168
169 linked_names.sort();
170 }
171 }
172
173 Ok(serde_json::json!({
174 "linked": linked_names,
175 "mode": mode_str,
176 "targets": targets,
177 "scope": "global",
178 })
179 .to_string())
180 }
181 }
182
183 async fn pkg_link_variant(
185 &self,
186 path: String,
187 name: Option<String>,
188 project_root: Option<String>,
189 ) -> Result<String, String> {
190 let raw = Path::new(&path);
192 let source: PathBuf = if raw.is_absolute() {
193 raw.to_path_buf()
194 } else {
195 std::env::current_dir()
196 .map_err(|e| format!("Cannot determine cwd: {e}"))?
197 .join(raw)
198 };
199
200 if !source.is_dir() {
201 return Err(format!("Path is not a directory: {}", source.display()));
202 }
203
204 let mode = detect_mode(&source)?;
206
207 let root = resolve_project_root(project_root.as_deref()).ok_or_else(|| {
209 "No project root found. Pass project_root or set ALC_PROJECT_ROOT, or run from within a project containing alc.toml.".to_string()
210 })?;
211
212 let mut doc = match alc_toml::load_alc_local_toml_document(&root)? {
214 Some(d) => d,
215 None => "[packages]\n"
216 .parse::<toml_edit::DocumentMut>()
217 .map_err(|e| format!("Failed to create empty alc.local.toml document: {e}"))?,
218 };
219
220 let mode_str;
222 let mut linked_names: Vec<String> = Vec::new();
223 let mut targets: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
224
225 match mode {
226 PackageMode::Single => {
227 mode_str = "single";
228 let pkg_name = if let Some(n) = name {
229 n
230 } else {
231 source
232 .file_name()
233 .ok_or_else(|| {
234 format!("Cannot determine package name from: {}", source.display())
235 })?
236 .to_string_lossy()
237 .to_string()
238 };
239 validate_package_name(&pkg_name)?;
240
241 let abs = source.display().to_string();
242 let added = add_package_entry(
243 &mut doc,
244 &pkg_name,
245 &PackageDep::Path {
246 path: abs.clone(),
247 version: None,
248 },
249 );
250
251 targets.insert(pkg_name.clone(), serde_json::Value::String(abs));
252 if added {
253 linked_names.push(pkg_name);
254 }
255 }
256 PackageMode::Collection => {
257 mode_str = "collection";
258 let entries = std::fs::read_dir(&source)
259 .map_err(|e| format!("Failed to read directory {}: {e}", source.display()))?;
260
261 let mut candidates: Vec<(String, String)> = Vec::new();
262 for entry in entries {
263 let entry =
264 entry.map_err(|e| format!("Failed to read directory entry: {e}"))?;
265 let pkg_path = entry.path();
266 if !pkg_path.is_dir() || !pkg_path.join("init.lua").exists() {
267 continue;
268 }
269 let pkg_name = entry.file_name().to_string_lossy().to_string();
270 validate_package_name(&pkg_name)?;
271 candidates.push((pkg_name, pkg_path.display().to_string()));
272 }
273
274 if candidates.is_empty() {
275 return Err(format!(
276 "No init.lua found in any subdirectory of: {}",
277 source.display()
278 ));
279 }
280
281 candidates.sort();
282 for (pkg_name, abs) in candidates {
283 let added = add_package_entry(
284 &mut doc,
285 &pkg_name,
286 &PackageDep::Path {
287 path: abs.clone(),
288 version: None,
289 },
290 );
291 targets.insert(pkg_name.clone(), serde_json::Value::String(abs));
292 if added {
293 linked_names.push(pkg_name);
294 }
295 }
296 }
297 }
298
299 alc_toml::save_alc_local_toml(&root, &doc)?;
301
302 let alc_local_path = alc_toml::local_alc_toml_path(&root);
303
304 Ok(serde_json::json!({
305 "linked": linked_names,
306 "mode": mode_str,
307 "targets": targets,
308 "scope": "variant",
309 "alc_local_toml": alc_local_path.display().to_string(),
310 })
311 .to_string())
312 }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq)]
318enum PackageMode {
319 Single,
320 Collection,
321}
322
323fn detect_mode(path: &Path) -> Result<PackageMode, String> {
325 if path.join("init.lua").exists() {
326 return Ok(PackageMode::Single);
327 }
328
329 let entries = std::fs::read_dir(path).map_err(|e| format!("Failed to read directory: {e}"))?;
330
331 for entry in entries {
332 let entry = entry.map_err(|e| format!("Failed to read directory entry: {e}"))?;
333 let sub = entry.path();
334 if sub.is_dir() && sub.join("init.lua").exists() {
335 return Ok(PackageMode::Collection);
336 }
337 }
338
339 Err(format!(
340 "No init.lua found in {} or any of its subdirectories",
341 path.display()
342 ))
343}
344
345#[cfg(unix)]
351fn create_symlink(source: &Path, dest: &Path, force: bool) -> Result<(), String> {
352 let meta = dest.symlink_metadata();
354
355 if let Ok(m) = meta {
356 if m.file_type().is_symlink() {
357 std::fs::remove_file(dest).map_err(|e| {
359 format!("Failed to remove existing symlink {}: {e}", dest.display())
360 })?;
361 } else if m.is_dir() {
362 if !force {
364 return Err(format!(
365 "Destination '{}' is a real directory. Use force=true to overwrite.",
366 dest.display()
367 ));
368 }
369 std::fs::remove_dir_all(dest)
370 .map_err(|e| format!("Failed to remove directory {}: {e}", dest.display()))?;
371 } else {
372 std::fs::remove_file(dest)
374 .map_err(|e| format!("Failed to remove {}: {e}", dest.display()))?;
375 }
376 }
377
378 symlink(source, dest).map_err(|e| {
379 format!(
380 "Failed to create symlink {} -> {}: {e}",
381 dest.display(),
382 source.display()
383 )
384 })
385}
386
387#[cfg(all(test, unix))]
390mod tests {
391 use super::*;
392 use crate::service::test_support::{make_app_service, FakeHome};
393
394 #[tokio::test]
395 async fn pkg_link_single_creates_symlink() {
396 let env = FakeHome::new();
397 let home = &env.home;
398
399 let src = home.join("my_pkg");
400 std::fs::create_dir_all(&src).unwrap();
401 std::fs::write(src.join("init.lua"), "return {}").unwrap();
402
403 let svc = make_app_service().await;
404 let result = svc
405 .pkg_link(src.to_string_lossy().to_string(), None, None, None, None)
406 .await
407 .unwrap();
408
409 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
410 assert_eq!(json["mode"], "single");
411 assert_eq!(json["linked"], serde_json::json!(["my_pkg"]));
412 assert_eq!(json["targets"]["my_pkg"], src.to_string_lossy().as_ref());
413
414 let dest = home.join(".algocline").join("packages").join("my_pkg");
415 assert!(dest.symlink_metadata().unwrap().file_type().is_symlink());
416 assert_eq!(std::fs::read_link(&dest).unwrap(), src);
417 }
418
419 #[tokio::test]
420 async fn pkg_link_collection_creates_symlinks() {
421 let env = FakeHome::new();
422 let home = &env.home;
423
424 let coll = home.join("collection");
425 std::fs::create_dir_all(coll.join("pkg_a")).unwrap();
426 std::fs::create_dir_all(coll.join("pkg_b")).unwrap();
427 std::fs::write(coll.join("pkg_a").join("init.lua"), "return {}").unwrap();
428 std::fs::write(coll.join("pkg_b").join("init.lua"), "return {}").unwrap();
429
430 let svc = make_app_service().await;
431 let result = svc
432 .pkg_link(coll.to_string_lossy().to_string(), None, None, None, None)
433 .await
434 .unwrap();
435
436 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
437 assert_eq!(json["mode"], "collection");
438
439 let linked = json["linked"].as_array().unwrap();
440 let mut names: Vec<&str> = linked.iter().map(|v| v.as_str().unwrap()).collect();
441 names.sort();
442 assert_eq!(names, ["pkg_a", "pkg_b"]);
443
444 let pkgs = home.join(".algocline").join("packages");
445 assert!(pkgs
446 .join("pkg_a")
447 .symlink_metadata()
448 .unwrap()
449 .file_type()
450 .is_symlink());
451 assert!(pkgs
452 .join("pkg_b")
453 .symlink_metadata()
454 .unwrap()
455 .file_type()
456 .is_symlink());
457 }
458
459 #[tokio::test]
460 async fn pkg_link_overwrites_existing_symlink() {
461 let env = FakeHome::new();
462 let home = &env.home;
463
464 let src = home.join("my_pkg");
465 std::fs::create_dir_all(&src).unwrap();
466 std::fs::write(src.join("init.lua"), "return {}").unwrap();
467
468 let pkgs = home.join(".algocline").join("packages");
469 std::fs::create_dir_all(&pkgs).unwrap();
470 let dest = pkgs.join("my_pkg");
471 symlink(&src, &dest).unwrap();
472
473 let svc = make_app_service().await;
474 let result = svc
475 .pkg_link(src.to_string_lossy().to_string(), None, None, None, None)
476 .await
477 .unwrap();
478
479 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
480 assert_eq!(json["linked"], serde_json::json!(["my_pkg"]));
481 assert!(dest.symlink_metadata().unwrap().file_type().is_symlink());
482 }
483
484 #[tokio::test]
485 async fn pkg_link_real_dir_requires_force() {
486 let env = FakeHome::new();
487 let home = &env.home;
488
489 let src = home.join("my_pkg");
490 std::fs::create_dir_all(&src).unwrap();
491 std::fs::write(src.join("init.lua"), "return {}").unwrap();
492
493 let pkgs = home.join(".algocline").join("packages");
494 let dest = pkgs.join("my_pkg");
495 std::fs::create_dir_all(&dest).unwrap();
496
497 let svc = make_app_service().await;
498
499 let err = svc
500 .pkg_link(src.to_string_lossy().to_string(), None, None, None, None)
501 .await
502 .unwrap_err();
503 assert!(
504 err.contains("real directory"),
505 "expected real directory error, got: {err}"
506 );
507
508 let result = svc
509 .pkg_link(
510 src.to_string_lossy().to_string(),
511 None,
512 Some(true),
513 None,
514 None,
515 )
516 .await
517 .unwrap();
518 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
519 assert_eq!(json["linked"], serde_json::json!(["my_pkg"]));
520 assert!(dest.symlink_metadata().unwrap().file_type().is_symlink());
521 }
522
523 #[tokio::test]
524 async fn pkg_link_dangling_symlink_overwritten() {
525 let env = FakeHome::new();
526 let home = &env.home;
527
528 let src = home.join("my_pkg");
529 std::fs::create_dir_all(&src).unwrap();
530 std::fs::write(src.join("init.lua"), "return {}").unwrap();
531
532 let pkgs = home.join(".algocline").join("packages");
533 std::fs::create_dir_all(&pkgs).unwrap();
534 let dest = pkgs.join("my_pkg");
535 symlink(home.join("nonexistent"), &dest).unwrap();
536 assert!(!dest.exists()); let svc = make_app_service().await;
539 let result = svc
540 .pkg_link(src.to_string_lossy().to_string(), None, None, None, None)
541 .await
542 .unwrap();
543
544 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
545 assert_eq!(json["linked"], serde_json::json!(["my_pkg"]));
546 assert!(dest.symlink_metadata().unwrap().file_type().is_symlink());
547 assert!(dest.exists()); }
549
550 #[tokio::test]
551 async fn pkg_link_path_not_found_returns_error() {
552 let env = FakeHome::new();
553 let nonexistent = env.home.join("does_not_exist");
554
555 let svc = make_app_service().await;
556 let err = svc
557 .pkg_link(
558 nonexistent.to_string_lossy().to_string(),
559 None,
560 None,
561 None,
562 None,
563 )
564 .await
565 .unwrap_err();
566 assert!(err.contains("not a directory"), "got: {err}");
567 }
568
569 #[tokio::test]
572 async fn pkg_link_scope_variant_appends_to_alc_local_toml() {
573 let env = FakeHome::new();
574 let root = env.home.join("proj");
575 std::fs::create_dir_all(&root).unwrap();
576 let src = env.home.join("my_pkg");
578 std::fs::create_dir_all(&src).unwrap();
579 std::fs::write(src.join("init.lua"), "return {}").unwrap();
580
581 let svc = make_app_service().await;
582 let result = svc
583 .pkg_link(
584 src.to_string_lossy().to_string(),
585 None,
586 None,
587 Some("variant".to_string()),
588 Some(root.to_string_lossy().to_string()),
589 )
590 .await
591 .unwrap();
592
593 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
594 assert_eq!(json["scope"], "variant");
595 assert_eq!(json["mode"], "single");
596 assert_eq!(json["linked"], serde_json::json!(["my_pkg"]));
597
598 let local = root.join("alc.local.toml");
600 assert!(local.exists());
601 let content = std::fs::read_to_string(&local).unwrap();
602 assert!(content.contains("my_pkg"));
603 assert!(content.contains(src.to_string_lossy().as_ref()));
604 }
605
606 #[tokio::test]
607 async fn pkg_link_scope_variant_no_symlink_created() {
608 let env = FakeHome::new();
609 let root = env.home.join("proj");
610 std::fs::create_dir_all(&root).unwrap();
611 let src = env.home.join("my_pkg");
612 std::fs::create_dir_all(&src).unwrap();
613 std::fs::write(src.join("init.lua"), "return {}").unwrap();
614
615 let svc = make_app_service().await;
616 svc.pkg_link(
617 src.to_string_lossy().to_string(),
618 None,
619 None,
620 Some("variant".to_string()),
621 Some(root.to_string_lossy().to_string()),
622 )
623 .await
624 .unwrap();
625
626 let cache_link = env.home.join(".algocline").join("packages").join("my_pkg");
627 assert!(
628 cache_link.symlink_metadata().is_err(),
629 "variant scope must not create a symlink in ~/.algocline/packages/"
630 );
631 }
632
633 #[tokio::test]
634 async fn pkg_link_scope_variant_second_call_is_noop_for_existing_entry() {
635 let env = FakeHome::new();
636 let root = env.home.join("proj");
637 std::fs::create_dir_all(&root).unwrap();
638 let src = env.home.join("my_pkg");
639 std::fs::create_dir_all(&src).unwrap();
640 std::fs::write(src.join("init.lua"), "return {}").unwrap();
641
642 let svc = make_app_service().await;
643 svc.pkg_link(
644 src.to_string_lossy().to_string(),
645 None,
646 None,
647 Some("variant".to_string()),
648 Some(root.to_string_lossy().to_string()),
649 )
650 .await
651 .unwrap();
652
653 let result = svc
655 .pkg_link(
656 src.to_string_lossy().to_string(),
657 None,
658 None,
659 Some("variant".to_string()),
660 Some(root.to_string_lossy().to_string()),
661 )
662 .await
663 .unwrap();
664
665 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
666 assert_eq!(json["linked"], serde_json::json!([]));
667 assert_eq!(json["targets"]["my_pkg"], src.to_string_lossy().as_ref());
669
670 let local = root.join("alc.local.toml");
675 let content = std::fs::read_to_string(&local).unwrap();
676 let doc: toml_edit::DocumentMut = content.parse().unwrap();
677 let pkgs = doc["packages"].as_table().unwrap();
678 let key_count = pkgs.iter().filter(|(k, _)| *k == "my_pkg").count();
679 assert_eq!(key_count, 1, "duplicate entry written: {content}");
680 }
681
682 #[tokio::test]
683 async fn pkg_link_scope_variant_requires_project_root() {
684 let env = FakeHome::new();
685 let src = env.home.join("my_pkg");
686 std::fs::create_dir_all(&src).unwrap();
687 std::fs::write(src.join("init.lua"), "return {}").unwrap();
688
689 let svc = make_app_service().await;
690 let nonexistent = env.home.join("no_such_project_root_zzz");
694 let err = svc
695 .pkg_link(
696 src.to_string_lossy().to_string(),
697 None,
698 None,
699 Some("variant".to_string()),
700 Some(nonexistent.to_string_lossy().to_string()),
701 )
702 .await;
703 if let Err(e) = err {
708 assert!(e.contains("No project root found"), "unexpected err: {e}");
709 }
710 }
711
712 #[tokio::test]
713 async fn pkg_link_invalid_scope_returns_error() {
714 let env = FakeHome::new();
715 let src = env.home.join("my_pkg");
716 std::fs::create_dir_all(&src).unwrap();
717 std::fs::write(src.join("init.lua"), "return {}").unwrap();
718
719 let svc = make_app_service().await;
720 let err = svc
721 .pkg_link(
722 src.to_string_lossy().to_string(),
723 None,
724 None,
725 Some("unknown".to_string()),
726 None,
727 )
728 .await
729 .unwrap_err();
730 assert!(err.contains("invalid scope"), "got: {err}");
731 }
732
733 #[tokio::test]
734 async fn pkg_link_scope_global_default_matches_existing_behavior() {
735 let env = FakeHome::new();
738 let home = &env.home;
739
740 let src = home.join("my_pkg");
741 std::fs::create_dir_all(&src).unwrap();
742 std::fs::write(src.join("init.lua"), "return {}").unwrap();
743
744 let svc = make_app_service().await;
745 let result = svc
746 .pkg_link(
747 src.to_string_lossy().to_string(),
748 None,
749 None,
750 Some("global".to_string()),
751 None,
752 )
753 .await
754 .unwrap();
755
756 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
757 assert_eq!(json["scope"], "global");
758 assert_eq!(json["linked"], serde_json::json!(["my_pkg"]));
759 let dest = home.join(".algocline").join("packages").join("my_pkg");
760 assert!(dest.symlink_metadata().unwrap().file_type().is_symlink());
761 }
762
763 #[tokio::test]
764 async fn pkg_link_scope_variant_collection_appends_all() {
765 let env = FakeHome::new();
766 let root = env.home.join("proj");
767 std::fs::create_dir_all(&root).unwrap();
768 let coll = env.home.join("collection");
769 std::fs::create_dir_all(coll.join("pkg_a")).unwrap();
770 std::fs::create_dir_all(coll.join("pkg_b")).unwrap();
771 std::fs::write(coll.join("pkg_a").join("init.lua"), "return {}").unwrap();
772 std::fs::write(coll.join("pkg_b").join("init.lua"), "return {}").unwrap();
773
774 let svc = make_app_service().await;
775 let result = svc
776 .pkg_link(
777 coll.to_string_lossy().to_string(),
778 None,
779 None,
780 Some("variant".to_string()),
781 Some(root.to_string_lossy().to_string()),
782 )
783 .await
784 .unwrap();
785
786 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
787 assert_eq!(json["scope"], "variant");
788 assert_eq!(json["mode"], "collection");
789 let linked = json["linked"].as_array().unwrap();
790 let names: Vec<&str> = linked.iter().map(|v| v.as_str().unwrap()).collect();
791 assert_eq!(names, ["pkg_a", "pkg_b"]);
792
793 let local = root.join("alc.local.toml");
794 let content = std::fs::read_to_string(&local).unwrap();
795 assert!(content.contains("pkg_a"));
796 assert!(content.contains("pkg_b"));
797 }
798
799 #[tokio::test]
803 async fn pkg_link_scope_variant_rejects_force() {
804 let env = FakeHome::new();
805 let root = env.home.join("proj");
806 std::fs::create_dir_all(&root).unwrap();
807 let src = env.home.join("my_pkg");
808 std::fs::create_dir_all(&src).unwrap();
809 std::fs::write(src.join("init.lua"), "return {}").unwrap();
810
811 let svc = make_app_service().await;
812 let err = svc
813 .pkg_link(
814 src.to_string_lossy().to_string(),
815 None,
816 Some(true),
817 Some("variant".to_string()),
818 Some(root.to_string_lossy().to_string()),
819 )
820 .await
821 .unwrap_err();
822 assert!(
823 err.contains("force is not supported with scope='variant'"),
824 "got: {err}"
825 );
826
827 assert!(
829 !root.join("alc.local.toml").exists(),
830 "alc.local.toml must not be written when the call is rejected"
831 );
832 }
833
834 #[tokio::test]
836 async fn pkg_link_scope_variant_accepts_force_false() {
837 let env = FakeHome::new();
838 let root = env.home.join("proj");
839 std::fs::create_dir_all(&root).unwrap();
840 let src = env.home.join("my_pkg");
841 std::fs::create_dir_all(&src).unwrap();
842 std::fs::write(src.join("init.lua"), "return {}").unwrap();
843
844 let svc = make_app_service().await;
845 let result = svc
846 .pkg_link(
847 src.to_string_lossy().to_string(),
848 None,
849 Some(false),
850 Some("variant".to_string()),
851 Some(root.to_string_lossy().to_string()),
852 )
853 .await
854 .unwrap();
855
856 let json: serde_json::Value = serde_json::from_str(&result).unwrap();
857 assert_eq!(json["scope"], "variant");
858 assert_eq!(json["linked"], serde_json::json!(["my_pkg"]));
859 }
860}