#![ allow( clippy::doc_markdown ) ]
use claude_assets_core::{
artifact::ArtifactKind,
error::{ AssetError, AssetPathsError },
install::{ InstallOutcome, UninstallOutcome, install, uninstall },
paths::AssetPaths,
registry::{ InstallStatus, list_all, list_available, list_installed },
};
use std::fs;
use tempfile::TempDir;
static ENV_LOCK : std::sync::Mutex< () > = std::sync::Mutex::new( () );
fn make_paths( src_dir : &std::path::Path, tgt_dir : &std::path::Path ) -> AssetPaths
{
AssetPaths::new( src_dir.to_path_buf(), tgt_dir.to_path_buf() )
}
fn write_source( paths : &AssetPaths, kind : ArtifactKind, name : &str )
{
let dir = paths.source_dir( kind );
fs::create_dir_all( &dir ).unwrap();
match kind.layout()
{
claude_assets_core::artifact::ArtifactLayout::File =>
{
let ext = kind.file_extension().unwrap_or( "" );
fs::write( dir.join( format!( "{name}.{ext}" ) ), b"# test" ).unwrap();
}
claude_assets_core::artifact::ArtifactLayout::Directory =>
{
fs::create_dir_all( dir.join( name ) ).unwrap();
}
}
}
#[ test ]
fn inst01_install_creates_symlink()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Rule, "rust" );
let report = install( &paths, ArtifactKind::Rule, "rust" ).unwrap();
assert_eq!( report.action, InstallOutcome::Installed );
let tgt_path = paths.target_dir( ArtifactKind::Rule ).join( "rust.md" );
assert!( fs::read_link( &tgt_path ).is_ok(), "target must be a symlink" );
}
#[ test ]
fn inst02_install_creates_target_subdir()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Rule, "python" );
assert!( !paths.target_dir( ArtifactKind::Rule ).exists() );
install( &paths, ArtifactKind::Rule, "python" ).unwrap();
assert!( paths.target_dir( ArtifactKind::Rule ).is_dir() );
}
#[ test ]
fn inst03_install_is_idempotent()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Command, "commit" );
install( &paths, ArtifactKind::Command, "commit" ).unwrap();
let r2 = install( &paths, ArtifactKind::Command, "commit" ).unwrap();
assert_eq!( r2.action, InstallOutcome::Reinstalled );
let tgt_path = paths.target_dir( ArtifactKind::Command ).join( "commit.md" );
assert!( fs::read_link( &tgt_path ).is_ok() );
}
#[ test ]
fn inst04_install_refuses_non_symlink_target()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Rule, "go" );
let tgt_dir = paths.target_dir( ArtifactKind::Rule );
fs::create_dir_all( &tgt_dir ).unwrap();
fs::write( tgt_dir.join( "go.md" ), b"regular file" ).unwrap();
let err = install( &paths, ArtifactKind::Rule, "go" ).unwrap_err();
assert!(
matches!( err, AssetError::NotASymlink { .. } ),
"expected NotASymlink, got: {err}",
);
assert!( tgt_dir.join( "go.md" ).exists() );
}
#[ test ]
fn inst05_uninstall_removes_symlink()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Agent, "planner" );
install( &paths, ArtifactKind::Agent, "planner" ).unwrap();
let tgt_path = paths.target_dir( ArtifactKind::Agent ).join( "planner.md" );
assert!( fs::symlink_metadata( &tgt_path ).is_ok() );
let report = uninstall( &paths, ArtifactKind::Agent, "planner" ).unwrap();
assert_eq!( report.action, UninstallOutcome::Uninstalled );
assert!( !tgt_path.exists() );
}
#[ test ]
fn inst06_uninstall_absent_returns_not_installed()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
let report = uninstall( &paths, ArtifactKind::Rule, "nonexistent" ).unwrap();
assert_eq!( report.action, UninstallOutcome::NotInstalled );
}
#[ test ]
fn inst07_uninstall_refuses_regular_file()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
let tgt_dir = paths.target_dir( ArtifactKind::Hook );
fs::create_dir_all( &tgt_dir ).unwrap();
let regular_file = tgt_dir.join( "pre_commit.yaml" );
fs::write( ®ular_file, b"hooks:" ).unwrap();
let err = uninstall( &paths, ArtifactKind::Hook, "pre_commit" ).unwrap_err();
assert!(
matches!( err, AssetError::NotASymlink { .. } ),
"expected NotASymlink, got: {err}",
);
assert!( regular_file.exists(), "regular file must not be deleted" );
}
#[ test ]
fn inst08_list_available_empty_when_source_absent()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
let available = list_available( &paths, ArtifactKind::Rule ).unwrap();
assert!( available.is_empty() );
}
#[ test ]
fn inst09_list_available_returns_source_names()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Rule, "rust" );
write_source( &paths, ArtifactKind::Rule, "python" );
write_source( &paths, ArtifactKind::Rule, "go" );
let dir = paths.source_dir( ArtifactKind::Rule );
fs::write( dir.join( "ignore.txt" ), b"" ).unwrap();
let mut available = list_available( &paths, ArtifactKind::Rule ).unwrap();
available.sort();
assert_eq!( available, vec![ "go", "python", "rust" ] );
}
#[ test ]
fn inst10_list_installed_returns_only_symlinks()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Rule, "rust" );
write_source( &paths, ArtifactKind::Rule, "python" );
install( &paths, ArtifactKind::Rule, "rust" ).unwrap();
install( &paths, ArtifactKind::Rule, "python" ).unwrap();
let tgt_dir = paths.target_dir( ArtifactKind::Rule );
fs::write( tgt_dir.join( "stale.md" ), b"regular" ).unwrap();
let mut installed = list_installed( &paths, ArtifactKind::Rule ).unwrap();
installed.sort();
assert_eq!( installed, vec![ "python", "rust" ] );
}
#[ test ]
fn inst11_list_all_merges_with_correct_status()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Rule, "a" );
write_source( &paths, ArtifactKind::Rule, "b" );
install( &paths, ArtifactKind::Rule, "a" ).unwrap();
let all = list_all( &paths, ArtifactKind::Rule ).unwrap();
assert_eq!( all.len(), 2 );
let a_status = all.iter().find( | ( n, _ ) | n == "a" ).map( | ( _, s ) | *s );
let b_status = all.iter().find( | ( n, _ ) | n == "b" ).map( | ( _, s ) | *s );
assert_eq!( a_status, Some( InstallStatus::Installed ) );
assert_eq!( b_status, Some( InstallStatus::Available ) );
}
#[ test ]
fn inst12_from_env_errors_when_vars_unset()
{
let _guard = ENV_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
std::env::remove_var( "PRO_CLAUDE" );
std::env::remove_var( "PRO" );
let err = AssetPaths::from_env().unwrap_err();
assert!(
matches!( err, AssetPathsError::EnvVarNotSet ),
"expected EnvVarNotSet, got: {err}",
);
let msg = err.to_string();
assert!( msg.contains( "PRO_CLAUDE" ), "error must mention PRO_CLAUDE, got: {msg}" );
}
#[ test ]
fn inst13_install_directory_layout_creates_dir_symlink()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Skill, "tsk" );
let report = install( &paths, ArtifactKind::Skill, "tsk" ).unwrap();
assert_eq!( report.action, InstallOutcome::Installed );
let tgt_path = paths.target_dir( ArtifactKind::Skill ).join( "tsk" );
let link_target = fs::read_link( &tgt_path ).expect( "target must be a symlink" );
assert!( link_target.ends_with( "skills/tsk" ), "symlink must point to source dir, got: {link_target:?}" );
}
#[ test ]
fn inst14_install_plugin_directory_layout_creates_dir_symlink()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let paths = make_paths( src.path(), tgt.path() );
write_source( &paths, ArtifactKind::Plugin, "mcp_server" );
let report = install( &paths, ArtifactKind::Plugin, "mcp_server" ).unwrap();
assert_eq!( report.action, InstallOutcome::Installed );
let tgt_path = paths.target_dir( ArtifactKind::Plugin ).join( "mcp_server" );
let link_target = fs::read_link( &tgt_path ).expect( "target must be a symlink" );
assert!(
link_target.ends_with( "plugins/mcp_server" ),
"symlink must point to source dir, got: {link_target:?}",
);
}