#![ allow( clippy::doc_markdown ) ]
use assert_cmd::Command;
use std::fs;
use tempfile::TempDir;
fn cla() -> Command
{
Command::cargo_bin( "cla" ).unwrap()
}
fn write_source_file( src_dir : &std::path::Path, kind : &str, name : &str )
{
let dir = src_dir.join( format!( "{kind}s" ) );
fs::create_dir_all( &dir ).unwrap();
fs::write( dir.join( format!( "{name}.md" ) ), b"# test" ).unwrap();
}
#[ test ]
fn cli01_kinds_exits_0_with_six_kinds()
{
let dir = TempDir::new().unwrap();
let out = cla()
.args( [ ".kinds" ] )
.env( "PRO_CLAUDE", dir.path() )
.output()
.unwrap();
assert!( out.status.success(), "exit must be 0, got: {:?}", out.status );
let stdout = String::from_utf8_lossy( &out.stdout );
let lines : Vec< _ > = stdout.lines().filter( |l| !l.trim().is_empty() ).collect();
assert!( lines.len() >= 6, "must show at least 6 kinds, got {}: {stdout}", lines.len() );
assert!( stdout.contains( "rule" ), "must mention rule, got: {stdout}" );
assert!( stdout.contains( "command" ), "must mention command, got: {stdout}" );
}
#[ test ]
fn cli02_list_without_env_exits_2()
{
let out = cla()
.args( [ ".list" ] )
.env_remove( "PRO_CLAUDE" )
.env_remove( "PRO" )
.output()
.unwrap();
assert_eq!( out.status.code(), Some( 2 ), "exit must be 2, got: {:?}", out.status );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "PRO_CLAUDE" ),
"error must mention PRO_CLAUDE, got: {stderr}",
);
}
#[ test ]
fn cli03_list_empty_source_exits_0()
{
let dir = TempDir::new().unwrap();
let out = cla()
.args( [ ".list", "kind::rule" ] )
.env( "PRO_CLAUDE", dir.path() )
.output()
.unwrap();
assert!( out.status.success(), "exit must be 0, got: {:?}", out.status );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "No artifacts" ),
"must say 'No artifacts', got: {stdout}",
);
}
#[ test ]
fn cli04_install_creates_symlink()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
write_source_file( src.path(), "rule", "rust" );
let out = cla()
.args( [ ".install", "kind::rule", "name::rust" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert!( out.status.success(), "exit must be 0, got: {:?}", out.status.code() );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "Installed" ) || stdout.contains( "install" ), "must confirm install, got: {stdout}" );
let link = tgt.path().join( ".claude" ).join( "rules" ).join( "rust.md" );
assert!( fs::read_link( &link ).is_ok(), "target must be a symlink, got: {link:?}" );
}
#[ test ]
fn cli05_install_nonexistent_exits_2()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let out = cla()
.args( [ ".install", "kind::rule", "name::nonexistent" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert_eq!( out.status.code(), Some( 2 ), "exit must be 2, got: {:?}", out.status );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.to_lowercase().contains( "not found" ) || stderr.contains( "nonexistent" ),
"error must mention 'not found' or the name, got: {stderr}",
);
}
#[ test ]
fn cli06_install_idempotent_reinstalls()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
write_source_file( src.path(), "rule", "go" );
cla()
.args( [ ".install", "kind::rule", "name::go" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.assert()
.success();
let out = cla()
.args( [ ".install", "kind::rule", "name::go" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert!( out.status.success(), "second install must exit 0, got: {:?}", out.status );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "Reinstalled" ) || stdout.contains( "install" ),
"must confirm reinstall, got: {stdout}",
);
}
#[ test ]
fn cli07_uninstall_installed_artifact()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
write_source_file( src.path(), "rule", "rust" );
cla()
.args( [ ".install", "kind::rule", "name::rust" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.assert()
.success();
let link = tgt.path().join( ".claude" ).join( "rules" ).join( "rust.md" );
assert!( fs::symlink_metadata( &link ).is_ok(), "symlink must exist before uninstall" );
let out = cla()
.args( [ ".uninstall", "kind::rule", "name::rust" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert!( out.status.success(), "exit must be 0, got: {:?}", out.status );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "Uninstalled" ) || stdout.contains( "uninstall" ),
"must confirm uninstall, got: {stdout}",
);
assert!( !link.exists(), "symlink must be absent after uninstall" );
}
#[ test ]
fn cli08_uninstall_not_installed_exits_0()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let out = cla()
.args( [ ".uninstall", "kind::rule", "name::ghost" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert!( out.status.success(), "exit must be 0, got: {:?}", out.status );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "Not installed" ) || stdout.contains( "not installed" ),
"must say 'Not installed', got: {stdout}",
);
}
#[ test ]
fn cli09_list_shows_installed_and_available_markers()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
write_source_file( src.path(), "rule", "rust" );
write_source_file( src.path(), "rule", "python" );
cla()
.args( [ ".install", "kind::rule", "name::rust" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.assert()
.success();
let out = cla()
.args( [ ".list", "kind::rule" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert!( out.status.success(), "exit must be 0, got: {:?}", out.status );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( '●' ), "must show ● for installed, got: {stdout}" );
assert!( stdout.contains( '○' ), "must show ○ for available, got: {stdout}" );
}
#[ test ]
fn cli10_install_without_kind_exits_1()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let out = cla()
.args( [ ".install", "name::rust" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert_eq!( out.status.code(), Some( 1 ), "exit must be 1, got: {:?}", out.status );
}
#[ test ]
fn cli11_install_invalid_kind_exits_1()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let out = cla()
.args( [ ".install", "kind::invalid", "name::x" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert_eq!( out.status.code(), Some( 1 ), "exit must be 1, got: {:?}", out.status );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "unknown kind" ) || stderr.contains( "invalid" ),
"must mention unknown kind, got: {stderr}",
);
}
#[ test ]
fn cli12_installed_true_string_exits_1()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let out = cla()
.args( [ ".list", "installed::true" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert_eq!( out.status.code(), Some( 1 ), "exit must be 1, got: {:?}", out.status );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "expected 0 or 1" ),
"error must say 'expected 0 or 1', got: {stderr}",
);
}
#[ test ]
fn cli13_bare_command_without_dot_exits_1()
{
let src = TempDir::new().unwrap();
let tgt = TempDir::new().unwrap();
let out = cla()
.args( [ "list" ] )
.env( "PRO_CLAUDE", src.path() )
.current_dir( tgt.path() )
.output()
.unwrap();
assert_eq!( out.status.code(), Some( 1 ), "exit must be 1, got: {:?}", out.status );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "start with '.'" ) || stderr.contains( "list" ),
"error must mention dot prefix, got: {stderr}",
);
}