sklink 0.2.4

Install skills into platform directories via a local store and symlinks
Documentation
use sklink::error::AppError;
use sklink::install::{ensure_correct_symlink, resolve_symlink_target, InstallOutcome};
use std::path::{Path, PathBuf};

fn canonical(path: &Path) -> PathBuf {
    std::fs::canonicalize(path).unwrap()
}

#[test]
fn ensure_correct_symlink_creates_when_missing() {
    let dir = tempfile::tempdir().unwrap();
    let link_path = dir.path().join("link");
    let link_target = dir.path().join("target");
    std::fs::create_dir_all(&link_target).unwrap();

    let outcome = ensure_correct_symlink(&link_path, &link_target).unwrap();
    assert_eq!(outcome, InstallOutcome::Created);

    let meta = std::fs::symlink_metadata(&link_path).unwrap();
    assert!(meta.file_type().is_symlink());

    let raw = std::fs::read_link(&link_path).unwrap();
    let resolved = resolve_symlink_target(&link_path, &raw);
    assert_eq!(canonical(&resolved), canonical(&link_target));
}

#[test]
fn ensure_correct_symlink_skips_when_correct() {
    let dir = tempfile::tempdir().unwrap();
    let link_path = dir.path().join("link");
    let link_target = dir.path().join("target");
    std::fs::create_dir_all(&link_target).unwrap();

    ensure_correct_symlink(&link_path, &link_target).unwrap();
    let outcome = ensure_correct_symlink(&link_path, &link_target).unwrap();
    assert_eq!(outcome, InstallOutcome::Skipped);
}

#[test]
fn ensure_correct_symlink_errors_when_existing_path_is_not_symlink() {
    let dir = tempfile::tempdir().unwrap();
    let link_path = dir.path().join("link");
    let link_target = dir.path().join("target");
    std::fs::create_dir_all(&link_target).unwrap();
    std::fs::write(&link_path, "not a symlink").unwrap();

    let err = ensure_correct_symlink(&link_path, &link_target).unwrap_err();
    match err {
        AppError::LinkPathNotSymlink { path } => assert_eq!(path, link_path),
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn ensure_correct_symlink_errors_when_symlink_points_elsewhere() {
    let dir = tempfile::tempdir().unwrap();
    let link_path = dir.path().join("link");
    let expected = dir.path().join("expected");
    let other = dir.path().join("other");
    std::fs::create_dir_all(&expected).unwrap();
    std::fs::create_dir_all(&other).unwrap();

    std::os::unix::fs::symlink(&other, &link_path).unwrap();
    let err = ensure_correct_symlink(&link_path, &expected).unwrap_err();
    match err {
        AppError::LinkPathWrongTarget {
            path,
            actual,
            expected,
        } => {
            assert_eq!(path, link_path);
            assert_eq!(actual, canonical(&other));
            assert_eq!(expected, canonical(&dir.path().join("expected")));
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn ensure_correct_symlink_accepts_relative_symlink_target() {
    let dir = tempfile::tempdir().unwrap();
    let repo = dir.path().join("repo");
    let targets = dir.path().join("targets");
    std::fs::create_dir_all(&repo).unwrap();
    std::fs::create_dir_all(&targets).unwrap();

    let link_target = repo.join("skill");
    std::fs::create_dir_all(&link_target).unwrap();

    let link_path = targets.join("skill");
    std::os::unix::fs::symlink("../repo/skill", &link_path).unwrap();

    let outcome = ensure_correct_symlink(&link_path, &link_target).unwrap();
    assert_eq!(outcome, InstallOutcome::Skipped);
}