mod common;
use std::fs;
use std::path::PathBuf;
fn setup_history( home : &std::path::Path, project_path : &std::path::Path )
{
let encoded = claude_storage_core::encode_path( project_path )
.expect( "encode_path should succeed" );
let storage_dir = home.join( ".claude" ).join( "projects" ).join( &encoded );
fs::create_dir_all( &storage_dir ).unwrap();
fs::write( storage_dir.join( "session.jsonl" ), b"fake content\n" ).unwrap();
}
#[ test ]
fn it_path_default_cwd()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".path" ] )
.current_dir( project.path() )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}, stdout: {stdout}"
);
assert!(
!stdout.trim().is_empty(),
"Should output storage path. Got empty stdout"
);
assert!(
stdout.contains( ".claude" ) || stdout.contains( "projects" ),
"Output should contain storage path components. Got: {stdout}"
);
}
#[ test ]
fn it_path_explicit_path()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let project_path = project.path().to_str().unwrap().to_string();
let output = common::clg_cmd()
.args( [ ".path", &format!( "path::{project_path}" ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}, stdout: {stdout}"
);
assert!(
stdout.contains( ".claude" ) || stdout.contains( "projects" ),
"Output should contain storage path components. Got: {stdout}"
);
}
#[ test ]
fn it_path_with_topic()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let project_path = project.path().to_str().unwrap().to_string();
let output = common::clg_cmd()
.args( [ ".path", &format!( "path::{project_path}" ), "topic::work" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}, stdout: {stdout}"
);
assert!(
stdout.contains( "work" ),
"Output should contain topic name. Got: {stdout}"
);
}
#[ test ]
fn it_path_nonexistent_exits_0()
{
let home = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".path", "path::/tmp/nonexistent-path-for-test-xyz-abc" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stderr = String::from_utf8_lossy( &output.stderr );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!(
output.status.success(),
"Should exit 0 for nonexistent path (path computation is filesystem-independent). stderr: {stderr}, stdout: {stdout}"
);
}
#[ test ]
fn it_path_empty_topic_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".path", "topic::" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stderr = String::from_utf8_lossy( &output.stderr );
let stdout = String::from_utf8_lossy( &output.stdout );
let combined = format!( "{stderr}{stdout}" );
assert!(
!output.status.success(),
"Should fail with empty topic. Got: {combined}"
);
}
#[ test ]
fn it_path_slash_in_topic_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".path", "topic::sub/dir" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stderr = String::from_utf8_lossy( &output.stderr );
let stdout = String::from_utf8_lossy( &output.stdout );
let combined = format!( "{stderr}{stdout}" );
assert!(
!output.status.success(),
"Should fail with slash in topic. Got: {combined}"
);
}
#[ test ]
fn it_exists_with_history_exits_0()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
setup_history( home.path(), project.path() );
let output = common::clg_cmd()
.args( [ ".exists", &format!( "path::{}", project.path().display() ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0 when history exists. stderr: {stderr}, stdout: {stdout}"
);
assert!(
stdout.contains( "sessions exist" ),
"Should print 'sessions exist'. Got stdout: {stdout}"
);
assert!(
stderr.is_empty(),
"stderr should be empty on success. Got: {stderr}"
);
}
#[ test ]
fn it_exists_without_history_exits_1()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".exists", &format!( "path::{}", project.path().display() ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
!output.status.success(),
"Should exit non-zero when no history. stdout: {stdout}, stderr: {stderr}"
);
assert!(
stderr.contains( "no sessions" ),
"Should print 'no sessions' to stderr. Got stderr: {stderr}"
);
assert!(
stdout.is_empty(),
"stdout should be empty on no-history. Got: {stdout}"
);
}
#[ test ]
fn it_exists_stderr_exact_when_no_history()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".exists", &format!( "path::{}", project.path().display() ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stderr = String::from_utf8_lossy( &output.stderr );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!(
!output.status.success(),
"Should exit non-zero. stdout: {stdout}"
);
assert_eq!(
stderr.as_ref(),
"no sessions\n",
"stderr must be exactly 'no sessions\\n' (spec: Exit 1: 'no sessions' on stderr)"
);
}
#[ test ]
fn it_exists_stdout_exact_when_found()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
setup_history( home.path(), project.path() );
let output = common::clg_cmd()
.args( [ ".exists", &format!( "path::{}", project.path().display() ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!(
output.status.success(),
"Should exit 0"
);
assert_eq!(
stdout.as_ref(),
"sessions exist\n",
"stdout should be exactly 'sessions exist\\n'"
);
}
#[ test ]
fn it_exists_topic_checks_topic_storage()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let session_dir : PathBuf = project.path().join( "-work" );
fs::create_dir_all( &session_dir ).unwrap();
setup_history( home.path(), &session_dir );
let output = common::clg_cmd()
.args( [ ".exists", &format!( "path::{}", project.path().display() ), "topic::work" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0 when topic storage exists. stderr: {stderr}, stdout: {stdout}"
);
assert!(
stdout.contains( "sessions exist" ),
"Should report 'sessions exist'. Got: {stdout}"
);
}
#[ test ]
fn it_exists_empty_topic_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".exists", "topic::" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stderr = String::from_utf8_lossy( &output.stderr );
let stdout = String::from_utf8_lossy( &output.stdout );
let combined = format!( "{stderr}{stdout}" );
assert!(
!output.status.success(),
"Should fail with empty topic. Got: {combined}"
);
}
#[ test ]
fn it_session_dir_default_topic()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.dir", &format!( "path::{base}" ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}, stdout: {stdout}"
);
let expected = format!( "{base}/-default_topic\n" );
assert_eq!(
stdout.as_ref(),
expected,
"Output should be {{base}}/-default_topic"
);
}
#[ test ]
fn it_session_dir_custom_topic()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.dir", &format!( "path::{base}" ), "topic::work" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}, stdout: {stdout}"
);
let expected = format!( "{base}/-work\n" );
assert_eq!(
stdout.as_ref(),
expected,
"Output should be {{base}}/-work"
);
}
#[ test ]
fn it_session_dir_missing_path_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".session.dir" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stderr = String::from_utf8_lossy( &output.stderr );
let stdout = String::from_utf8_lossy( &output.stdout );
let combined = format!( "{stderr}{stdout}" );
assert!(
!output.status.success(),
"Should fail without path. Got: {combined}"
);
}
#[ test ]
fn it_session_dir_does_not_create_directory()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.dir", &format!( "path::{base}" ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
assert!(
output.status.success(),
"Should exit 0"
);
let session_dir = project.path().join( "-default_topic" );
assert!(
!session_dir.exists(),
".session.dir must not create the directory. Found: {session_dir:?}"
);
}
#[ test ]
fn it_session_dir_empty_topic_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.dir", &format!( "path::{base}" ), "topic::" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let combined = format!(
"{}{}",
String::from_utf8_lossy( &output.stderr ),
String::from_utf8_lossy( &output.stdout )
);
assert!(
!output.status.success(),
"Should fail with empty topic. Got: {combined}"
);
}
#[ test ]
fn it_session_dir_slash_in_topic_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.dir", &format!( "path::{base}" ), "topic::sub/dir" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let combined = format!(
"{}{}",
String::from_utf8_lossy( &output.stderr ),
String::from_utf8_lossy( &output.stdout )
);
assert!(
!output.status.success(),
"Should fail with slash in topic. Got: {combined}"
);
}
#[ test ]
fn it_session_ensure_creates_directory()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stderr = String::from_utf8_lossy( &output.stderr );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}, stdout: {stdout}"
);
let session_dir = project.path().join( "-default_topic" );
assert!(
session_dir.exists(),
".session.ensure should create the session directory. Path: {session_dir:?}"
);
}
#[ test ]
fn it_session_ensure_strategy_fresh_when_no_history()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}"
);
let lines : Vec< &str > = stdout.lines().collect();
assert_eq!( lines.len(), 2, "Should output exactly 2 lines. Got: {stdout}" );
assert_eq!(
lines[ 1 ],
"fresh",
"Line 2 should be 'fresh' when no history. Got: {stdout}"
);
}
#[ test ]
fn it_session_ensure_strategy_resume_when_history_exists()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let session_dir = project.path().join( "-default_topic" );
fs::create_dir_all( &session_dir ).unwrap();
setup_history( home.path(), &session_dir );
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}"
);
let lines : Vec< &str > = stdout.lines().collect();
assert_eq!( lines.len(), 2, "Should output exactly 2 lines. Got: {stdout}" );
assert_eq!(
lines[ 1 ],
"resume",
"Line 2 should be 'resume' when history exists. Got: {stdout}"
);
}
#[ test ]
fn it_session_ensure_line1_is_session_dir_path()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ), "topic::work" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!( output.status.success(), "Should exit 0" );
let lines : Vec< &str > = stdout.lines().collect();
assert_eq!( lines.len(), 2, "Should output exactly 2 lines. Got: {stdout}" );
let expected_dir = format!( "{base}/-work" );
assert_eq!(
lines[ 0 ],
expected_dir,
"Line 1 should be the absolute session directory path. Got: {stdout}"
);
}
#[ test ]
fn it_session_ensure_force_resume_overrides_fresh()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ), "strategy::resume" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}"
);
let lines : Vec< &str > = stdout.lines().collect();
assert_eq!( lines.len(), 2, "Should output 2 lines. Got: {stdout}" );
assert_eq!(
lines[ 1 ],
"resume",
"Forced resume should override auto-detect fresh. Got: {stdout}"
);
}
#[ test ]
fn it_session_ensure_force_fresh_overrides_resume()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let session_dir = project.path().join( "-default_topic" );
fs::create_dir_all( &session_dir ).unwrap();
setup_history( home.path(), &session_dir );
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ), "strategy::fresh" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}"
);
let lines : Vec< &str > = stdout.lines().collect();
assert_eq!( lines.len(), 2, "Should output 2 lines. Got: {stdout}" );
assert_eq!(
lines[ 1 ],
"fresh",
"Forced fresh should override auto-detect resume. Got: {stdout}"
);
}
#[ test ]
fn it_session_ensure_idempotent()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let out1 = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute (first call)" );
assert!( out1.status.success(), "First call should succeed" );
let out2 = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ) ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute (second call)" );
let stderr2 = String::from_utf8_lossy( &out2.stderr );
let stdout2 = String::from_utf8_lossy( &out2.stdout );
assert!(
out2.status.success(),
"Second call should also succeed. stderr: {stderr2}, stdout: {stdout2}"
);
}
#[ test ]
fn it_session_ensure_missing_path_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let output = common::clg_cmd()
.args( [ ".session.ensure" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let combined = format!(
"{}{}",
String::from_utf8_lossy( &output.stderr ),
String::from_utf8_lossy( &output.stdout )
);
assert!(
!output.status.success(),
"Should fail without path. Got: {combined}"
);
}
#[ test ]
fn it_session_ensure_invalid_strategy_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ), "strategy::auto" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stderr = String::from_utf8_lossy( &output.stderr );
let stdout = String::from_utf8_lossy( &output.stdout );
let combined = format!( "{stderr}{stdout}" );
assert!(
!output.status.success(),
"Should fail with invalid strategy. Got: {combined}"
);
assert!(
combined.contains( "strategy" ),
"Error should mention 'strategy'. Got: {combined}"
);
}
#[ test ]
fn it_session_ensure_empty_topic_rejected()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ), "topic::" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let combined = format!(
"{}{}",
String::from_utf8_lossy( &output.stderr ),
String::from_utf8_lossy( &output.stdout )
);
assert!(
!output.status.success(),
"Should fail with empty topic. Got: {combined}"
);
}
#[ test ]
fn it_session_ensure_custom_topic_in_output()
{
let home = tempfile::TempDir::new().unwrap();
let project = tempfile::TempDir::new().unwrap();
let base = project.path().to_str().unwrap();
let output = common::clg_cmd()
.args( [ ".session.ensure", &format!( "path::{base}" ), "topic::my_session" ] )
.env( "HOME", home.path() )
.output()
.expect( "Failed to execute" );
let stdout = String::from_utf8_lossy( &output.stdout );
let stderr = String::from_utf8_lossy( &output.stderr );
assert!(
output.status.success(),
"Should exit 0. stderr: {stderr}"
);
let lines : Vec< &str > = stdout.lines().collect();
assert_eq!( lines.len(), 2, "Should output 2 lines. Got: {stdout}" );
assert!(
lines[ 0 ].ends_with( "/-my_session" ),
"Line 1 should end with '/-my_session'. Got: {}",
lines[ 0 ]
);
let session_dir = project.path().join( "-my_session" );
assert!(
session_dir.exists(),
"Session dir with custom topic should be created. Path: {session_dir:?}"
);
}