claude_storage 1.0.0

CLI tool for exploring Claude Code filesystem storage
Documentation
//! Bug fix test for project parameter handling
//!
//! ## Root Cause
//!
//! The `show_routine` at line 239 always calls `ProjectId::uuid(proj_id)`
//! regardless of whether `proj_id` is a path or UUID. This causes path projects
//! to fail with "Project not found" because it tries to find a UUID-named
//! directory instead of decoding the path.
//!
//! ## Why Not Caught
//!
//! No tests exercised `.show` command with path projects. All test scenarios
//! used UUID projects or relied on current directory detection.
//!
//! ## Fix Applied
//!
//! Added `parse_project_parameter()` helper that intelligently detects:
//! - Paths starting with `/` → `ProjectId::Path`
//! - Path-encoded starting with `-` → decode then `ProjectId::Path`
//! - Debug format `Path("...")` → extract and use `ProjectId::Path`
//! - Otherwise → `ProjectId::Uuid`
//!
//! ## Prevention
//!
//! Added test coverage for all project parameter formats:
//! - Absolute paths
//! - Path-encoded
//! - UUIDs
//! - Debug format from .list output
//! - Mixed scenarios (path project + UUID session)
//!
//! ## Pitfall
//!
//! Always assuming a string parameter is one type leads to silent failures
//! when user provides a different type. Smart detection with explicit format
//! rules prevents this class of bug.
//!
//! ## Isolation Note
//!
//! Integration tests use `CLAUDE_STORAGE_ROOT` + `TempDir` to avoid writing to
//! the real `~/.claude/`. This prevents race conditions in workspace-wide
//! `cargo nextest run --workspace`.
//!
//! Fix(issue-project-bug-fix-isolation)
//! Root cause: original integration tests wrote to real `~/.claude/` with
//! comment "CLI doesn't support custom storage root" — that comment was stale;
//! `CLAUDE_STORAGE_ROOT` support was added in the hygiene sprint.
//! Pitfall: stale comments documenting missing features can cause isolation
//! anti-patterns to persist long after the feature is added.

mod common;

use std::fs;
use tempfile::TempDir;

#[ test ]
fn test_show_with_path_project()
{
  use claude_storage_core::encode_path;

  let storage = TempDir::new().unwrap();
  let project_path = TempDir::new().unwrap();
  let encoded = encode_path( project_path.path() ).unwrap();
  let project_dir = storage.path().join( "projects" ).join( &encoded );
  fs::create_dir_all( &project_dir ).unwrap();

  let session_id = "test-session-12345";
  let session_file = project_dir.join( format!( "{session_id}.jsonl" ) );
  fs::write( &session_file, r#"{"type":"user","text":"test"}"# ).unwrap();

  let output = common::clg_cmd()
    .env( "CLAUDE_STORAGE_ROOT", storage.path() )
    .args( [
      ".show",
      &format!( "session_id::{session_id}" ),
      &format!( "project::{}", project_path.path().display() )
    ] )
    .output()
    .unwrap();

  let stdout = String::from_utf8_lossy( &output.stdout );
  let stderr = String::from_utf8_lossy( &output.stderr );

  // Should NOT fail with "Project not found"
  assert!(
    !stderr.contains( "Project not found" ),
    "Bug: Path project treated as UUID. stderr: {stderr}"
  );

  // Should show session details
  assert!(
    stdout.contains( "Session:" ) || stdout.contains( session_id ),
    "Expected session details in output. stdout: {stdout}"
  );
}

#[ test ]
fn test_show_with_uuid_project_still_works()
{
  // Ensure our fix doesn't break UUID projects (regression test)

  let storage = TempDir::new().unwrap();
  let project_uuid = "test-uuid-project-123";
  let project_dir = storage.path().join( "projects" ).join( project_uuid );
  fs::create_dir_all( &project_dir ).unwrap();

  let session_id = "test-session-uuid-789";
  let session_file = project_dir.join( format!( "{session_id}.jsonl" ) );
  fs::write( &session_file, r#"{"type":"user","text":"test"}"# ).unwrap();

  let output = common::clg_cmd()
    .env( "CLAUDE_STORAGE_ROOT", storage.path() )
    .args( [
      ".show",
      &format!( "session_id::{session_id}" ),
      &format!( "project::{project_uuid}" )
    ] )
    .output()
    .unwrap();

  let stdout = String::from_utf8_lossy( &output.stdout );
  let stderr = String::from_utf8_lossy( &output.stderr );

  assert!(
    !stderr.contains( "Project not found" ),
    "Regression: UUID project support broken. stderr: {stderr}"
  );

  assert!(
    stdout.contains( "Session:" ),
    "Expected session details. stdout: {stdout}"
  );
}

#[ test ]
fn test_parse_project_parameter_unit()
{
  use claude_storage::parse_project_parameter;
  use claude_storage_core::ProjectId;

  // Test absolute path
  let result = parse_project_parameter( "/home/user/project" ).unwrap();
  assert!(
    matches!( result, ProjectId::Path( _ ) ),
    "Absolute path should be ProjectId::Path"
  );

  // Test path-encoded
  let result = parse_project_parameter( "-home-user-project" ).unwrap();
  assert!(
    matches!( result, ProjectId::Path( _ ) ),
    "Path-encoded should be ProjectId::Path"
  );

  // Test UUID
  let result = parse_project_parameter( "abc-123-def" ).unwrap();
  assert!(
    matches!( result, ProjectId::Uuid( _ ) ),
    "UUID format should be ProjectId::Uuid"
  );

  // Test Debug format from .list output
  let result = parse_project_parameter( r#"Path("/home/user/project")"# ).unwrap();
  assert!(
    matches!( result, ProjectId::Path( _ ) ),
    "Debug format should extract path"
  );
}