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