mod common;
use tempfile::TempDir;
fn stdout( out : &std::process::Output ) -> String
{
String::from_utf8_lossy( &out.stdout ).into_owned()
}
fn stderr( out : &std::process::Output ) -> String
{
String::from_utf8_lossy( &out.stderr ).into_owned()
}
fn assert_exit( out : &std::process::Output, code : i32 )
{
assert_eq!(
out.status.code().unwrap_or( -1 ),
code,
"expected exit {code}, got {:?}; stderr: {}",
out.status.code(),
stderr( out )
);
}
#[ test ]
fn it_24_decode_display_includes_hyphen_prefixed_topic_dir()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "src" ).join( "-default_topic" );
std::fs::create_dir_all( &project ).expect( "create src/-default_topic dir" );
common::write_path_project_session( &storage_root, &project, "session-topic-dir-test", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "-default_topic" ),
"display path must include hyphen-prefixed topic dir '-default_topic'; got:\n{s}"
);
assert!(
!s.lines().any( | l | l.trim_end().ends_with( "src:" ) ),
"display path must NOT be truncated to 'src' when '-default_topic' exists; got:\n{s}"
);
assert!( s.contains( "session-topic-dir-test" ), "session ID must appear; got:\n{s}" );
}
#[ test ]
fn it_23_decode_display_preserves_underscore_named_dirs()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "my_project" ).join( "myproject" );
std::fs::create_dir_all( &project ).expect( "create project dir with underscore component" );
common::write_path_project_session( &storage_root, &project, "session-underscore-test", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "my_project" ),
"display path must preserve underscore: 'my_project' not 'wip/core'; got:\n{s}"
);
assert!(
!s.lines().any( | l | l.contains( "wip/core" ) ),
"display path must NOT split my_project into wip/core; got:\n{s}"
);
assert!( s.contains( "session-underscore-test" ), "session ID must appear; got:\n{s}" );
}
#[ test ]
fn it_25_scope_under_excludes_underscore_named_sibling()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let base = root.path().join( "base" );
let child = base.join( "sub" );
let sibling = root.path().join( "base_extra" );
std::fs::create_dir_all( &child ).expect( "create child dir" );
std::fs::create_dir_all( &sibling ).expect( "create sibling dir" );
common::write_path_project_session( &storage_root, &child, "session-it25-child", 2 );
common::write_path_project_session( &storage_root, &sibling, "session-it25-sibling", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::under" )
.arg( format!( "path::{}", base.display() ) )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "session-it25-child" ),
"must contain session-it25-child (child base/sub is under base); got:\n{s}"
);
assert!(
!s.contains( "session-it25-sibling" ),
"must NOT contain session-it25-sibling (sibling base_extra is NOT under base); got:\n{s}"
);
}
#[ test ]
fn it_26_scope_relevant_excludes_underscore_named_sibling()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let sibling = root.path().join( "base" );
let target = root.path().join( "base_extra" );
std::fs::create_dir_all( &sibling ).expect( "create sibling dir" );
std::fs::create_dir_all( &target ).expect( "create target dir" );
common::write_path_project_session( &storage_root, &sibling, "session-it26-sibling", 2 );
common::write_path_project_session( &storage_root, &target, "session-it26-target", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::relevant" )
.arg( format!( "path::{}", target.display() ) )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "session-it26-target" ),
"must contain session-it26-target (current project at base_extra); got:\n{s}"
);
assert!(
!s.contains( "session-it26-sibling" ),
"must NOT contain session-it26-sibling (/base is NOT an ancestor of /base_extra); got:\n{s}"
);
}
#[ test ]
fn projects_shows_topic_path_when_topic_dir_absent()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "myproject" );
std::fs::create_dir_all( &project ).expect( "create project base dir" );
let encoded_base = claude_storage_core::encode_path( &project ).expect( "encode project path" );
let topic_project_id = format!( "{encoded_base}--commit" );
common::write_test_session( &storage_root, &topic_project_id, "session-t01-absent-commit", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "/-commit" ),
"display path must include topic '/-commit' even when dir is absent from disk; got:\n{s}"
);
assert!(
!s.lines().any( | l | l.trim_end().ends_with( "myproject:" ) ),
"display path must NOT be truncated to 'myproject:' when topic dir is absent; got:\n{s}"
);
assert!( s.contains( "session-t01-absent-commit" ), "session must appear in output; got:\n{s}" );
}
#[ test ]
fn projects_shows_topic_path_when_topic_dir_present()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "myproject" );
let topic_dir = project.join( "-commit" );
std::fs::create_dir_all( &topic_dir ).expect( "create myproject/-commit dir" );
let encoded_base = claude_storage_core::encode_path( &project ).expect( "encode project path" );
let topic_project_id = format!( "{encoded_base}--commit" );
common::write_test_session( &storage_root, &topic_project_id, "session-t02-present-commit", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "/-commit" ),
"display path must include topic '/-commit' when dir is present on disk; got:\n{s}"
);
assert!( s.contains( "session-t02-present-commit" ), "session must appear; got:\n{s}" );
}
#[ test ]
fn projects_shows_default_topic_path_when_topic_dir_absent()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "myproject" );
std::fs::create_dir_all( &project ).expect( "create project base dir" );
let encoded_base = claude_storage_core::encode_path( &project ).expect( "encode project path" );
let topic_project_id = format!( "{encoded_base}--default-topic" );
common::write_test_session( &storage_root, &topic_project_id, "session-t03-absent-default-topic", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "/-default_topic" ),
"display path must include '/-default_topic' even when dir is absent; got:\n{s}"
);
assert!(
s.contains( "session-t03-absent-default-topic" ),
"session must appear in output; got:\n{s}"
);
}
#[ test ]
fn projects_shows_base_path_with_no_topic()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "myproject" );
std::fs::create_dir_all( &project ).expect( "create project dir" );
common::write_path_project_session( &storage_root, &project, "session-t04-no-topic", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "session-t04-no-topic" ), "session must appear; got:\n{s}" );
assert!(
!s.contains( "/-commit" ),
"no topic in storage key — must not show /-commit; got:\n{s}"
);
assert!(
!s.contains( "/-default_topic" ),
"no topic in storage key — must not show /-default_topic; got:\n{s}"
);
}
#[ test ]
fn projects_shows_both_topic_components_for_double_topic_key()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "myproject" );
std::fs::create_dir_all( &project ).expect( "create project base dir" );
let encoded_base = claude_storage_core::encode_path( &project ).expect( "encode project path" );
let topic_project_id = format!( "{encoded_base}--default-topic--commit" );
common::write_test_session( &storage_root, &topic_project_id, "session-t05-double-topic", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "/-default_topic" ),
"display path must include first topic '/-default_topic'; got:\n{s}"
);
assert!(
s.contains( "/-commit" ),
"display path must include second topic '/-commit'; got:\n{s}"
);
assert!( s.contains( "session-t05-double-topic" ), "session must appear in output; got:\n{s}" );
}