claude_storage 1.0.0

CLI tool for exploring Claude Code filesystem storage
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
//! Status Command Path Parameter Tests
//!
//! ## Purpose
//!
//! Validates path parameter functionality for .status command.
//! Tests ensure proper handling of default path, custom paths, and error cases.
//!
//! ## Coverage
//!
//! Validates path parameter behavior:
//! - Default path usage (no parameter specified)
//! - Custom path support (`path::` parameter)
//! - Nonexistent path handling (error case)
//! - Empty path validation (error case)
//! - Path with verbosity interaction (parameter combination)
//!
//! ## Testing Strategy
//!
//! - Feature tests: Run immediately (verify existing path parameter functionality)
//! - Uses real filesystem with tempfile crate for integration testing
//! - Follows same pattern as `search_command_test.rs` and `export_command_test.rs`
//!
//! ## Related Requirements
//!
//! .status path parameter documentation (spec.md:272-276)

mod common;

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

/// Test `.status` uses `CLAUDE_STORAGE_ROOT` when no path parameter specified
///
/// ## Purpose
/// Validates that `.status` command respects the `CLAUDE_STORAGE_ROOT` env var
/// when no path parameter is provided (covers the default-path code path).
///
/// ## Coverage
/// Tests default parameter behavior with isolated storage. Creates 2 known
/// projects in temp storage, verifies `.status` reports them correctly.
///
/// ## Validation Strategy
/// Write 2 projects to temp storage. Run `.status` with `CLAUDE_STORAGE_ROOT`.
/// Assert exit 0 and output shows project information.
///
/// ## Related Requirements
/// `.status` path parameter: default behavior uses `CLAUDE_STORAGE_ROOT` or `~/.claude/`
#[test]
fn test_status_default_path()
{
  let storage = TempDir::new().expect( "create temp storage" );

  // Create 2 projects so we have something meaningful to report
  common::write_test_session( storage.path(), "status-proj-alpha", "s001", 2 );
  common::write_test_session( storage.path(), "status-proj-beta", "s001", 2 );

  let output = common::clg_cmd()
    .args( [ ".status" ] )
    .env( "CLAUDE_STORAGE_ROOT", storage.path() )
    .output()
    .expect( "Failed to execute .status" );

  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 succeed with default path via CLAUDE_STORAGE_ROOT. Got: {combined}"
  );

  assert!(
    combined.to_lowercase().contains( "project" ) ||
    combined.to_lowercase().contains( "storage" ),
    "Should show storage information. Got: {combined}"
  );
}

/// Test .status accepts custom path parameter
///
/// ## Purpose
/// Validates that .status command can use custom storage path via `path::` parameter.
///
/// ## Coverage
/// Tests custom path parameter functionality. Should successfully execute
/// using specified storage location instead of default.
///
/// ## Validation Strategy
/// Setup: Create temp directory with projects/ subdirectory structure
/// Execute .status with `path::{temp_dir`}. Assert:
/// - Command succeeds (zero exit)
/// - Output shows 0 projects (empty storage)
///
/// ## Related Requirements
/// .status path parameter: accepts custom storage location
#[test]
fn test_status_custom_path()
{
  // Create temp directory structure
  let temp_dir = TempDir::new().expect( "Failed to create temp dir" );
  let storage_path = temp_dir.path();
  let projects_dir = storage_path.join( "projects" );
  fs::create_dir_all( &projects_dir ).expect( "Failed to create projects dir" );

  let output = common::clg_cmd()
    .args( [ ".status", &format!( "path::{}", storage_path.display() ) ] )
    .current_dir( env!( "CARGO_MANIFEST_DIR" ) )
    .output()
    .expect( "Failed to execute command" );

  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 succeed with custom path. Got: {combined}"
  );

  // Should report 0 projects in empty storage
  assert!(
    combined.contains( '0' ) && combined.to_lowercase().contains( "project" ),
    "Should show 0 projects in empty storage. Got: {combined}"
  );
}

/// Test .status handles nonexistent path gracefully
///
/// ## Purpose
/// Validates that .status command succeeds with nonexistent path and reports
/// empty storage (0 projects) rather than failing.
///
/// ## Coverage
/// Tests graceful handling of nonexistent paths. Command should succeed and
/// report 0 projects for nonexistent paths, allowing users to check status
/// of new/empty storage locations.
///
/// ## Validation Strategy
/// Execute .status with `path::/nonexistent/path/12345`. Assert:
/// - Command succeeds (zero exit)
/// - Reports 0 projects (empty storage)
///
/// ## Related Requirements
/// .status path parameter: gracefully handles nonexistent paths
#[test]
fn test_status_nonexistent_path()
{
  let output = common::clg_cmd()
    .args( [ ".status", "path::/nonexistent/path/12345" ] )
    .current_dir( env!( "CARGO_MANIFEST_DIR" ) )
    .output()
    .expect( "Failed to execute command" );

  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 succeed with nonexistent path. Got: {combined}"
  );

  // Should report 0 projects for nonexistent path
  assert!(
    combined.contains( '0' ) && combined.to_lowercase().contains( "project" ),
    "Should show 0 projects for nonexistent path. Got: {combined}"
  );
}

/// Test .status rejects empty path parameter
///
/// ## Purpose
/// Validates that .status command rejects empty path parameter value.
///
/// ## Coverage
/// Tests empty string edge case. Empty parameter values should be rejected
/// with clear error message.
///
/// ## Validation Strategy
/// Execute .status with `path::` (empty value). Assert:
/// - Command fails (non-zero exit)
/// - Error mentions "path" or "expected value"
///
/// ## Related Requirements
/// .status path parameter: rejects empty path values
#[test]
fn test_status_empty_path()
{
  let output = common::clg_cmd()
    .args( [ ".status", "path::" ] )
    .current_dir( env!( "CARGO_MANIFEST_DIR" ) )
    .output()
    .expect( "Failed to execute command" );

  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 path. Got: {combined}"
  );

  assert!(
    combined.to_lowercase().contains( "path" ) ||
    combined.to_lowercase().contains( "expected value" ),
    "Error should mention path validation. Got: {combined}"
  );
}

/// Test .status `path::`. resolves to current directory (Finding #014)
///
/// ## Root Cause
/// `status_routine` at line 74 passes path directly to `Storage::with_root()` without
/// resolving special path markers. While `list_routine` uses `resolve_path_parameter()`,
/// `status_routine` does not, causing ".", "..", "~" to be used literally.
///
/// ## Why Not Caught
/// Existing tests used only explicit full paths or temp directories. No tests
/// exercised special path markers (".", "..", "~") in the path parameter.
///
/// ## Fix Applied
/// Added `resolve_path_parameter()` call in `status_routine` before passing to
/// `Storage::with_root()`, consistent with `list_routine` pattern.
///
/// ## Prevention
/// When multiple commands share similar parameters, ensure they use the same
/// helper functions. The `resolve_path_parameter()` helper exists specifically
/// for this purpose but was not consistently applied.
///
/// ## Pitfall
/// Adding new commands by copying existing code without understanding shared
/// utilities leads to inconsistent behavior between commands.
#[test]
fn test_status_path_dot_resolves_to_cwd()
{
  let manifest_dir = env!( "CARGO_MANIFEST_DIR" );

  let output = common::clg_cmd()
    .args( [ ".status", "path::." ] )
    .current_dir( manifest_dir )
    .output()
    .expect( "Failed to execute command" );

  let stdout = String::from_utf8_lossy( &output.stdout );
  let stderr = String::from_utf8_lossy( &output.stderr );
  let combined = format!( "{stdout}{stderr}" );

  // Bug behavior: Shows Storage: "."
  // Fixed behavior: Shows resolved path like Storage: "/home/.../claude_storage"

  let has_literal_dot = combined.contains( r#"Storage: ".""# );

  assert!(
    !has_literal_dot,
    "Bug: path::. not resolved, shows literal '.' in Storage.\n\
    Expected: Resolved absolute path\n\
    Got: {combined}"
  );
}

/// Test .status `path::`~ resolves to home directory (Finding #014)
///
/// ## Purpose
/// Validates that `status_routine` resolves ~ to home directory.
#[test]
fn test_status_path_tilde_resolves_to_home()
{
  let manifest_dir = env!( "CARGO_MANIFEST_DIR" );

  let output = common::clg_cmd()
    .args( [ ".status", "path::~" ] )
    .current_dir( manifest_dir )
    .output()
    .expect( "Failed to execute command" );

  let stdout = String::from_utf8_lossy( &output.stdout );
  let stderr = String::from_utf8_lossy( &output.stderr );
  let combined = format!( "{stdout}{stderr}" );

  // Bug behavior: Shows Storage: "~"
  // Fixed behavior: Shows resolved path like Storage: "/home/user"

  let has_literal_tilde = combined.contains( r#"Storage: "~""# );

  assert!(
    !has_literal_tilde,
    "Bug: path::~ not resolved, shows literal '~' in Storage.\n\
    Expected: Resolved home directory path\n\
    Got: {combined}"
  );
}

/// Test .status path parameter works with verbosity parameter
///
/// ## Purpose
/// Validates that .status command correctly handles path and verbosity
/// parameters together, ensuring no parameter interaction issues.
///
/// ## Coverage
/// Tests parameter combination. Path and verbosity should work independently
/// without conflicts.
///
/// ## Validation Strategy
/// Setup: Create temp directory with projects/ subdirectory
/// Execute .status with `path::{temp`} and `verbosity::2`. Assert:
/// - Command succeeds (zero exit)
/// - Output shows storage information
/// - Both parameters are respected
///
/// ## Related Requirements
/// .status path parameter: works with other parameters
#[test]
fn test_status_path_with_verbosity()
{
  // Create temp directory structure
  let temp_dir = TempDir::new().expect( "Failed to create temp dir" );
  let storage_path = temp_dir.path();
  let projects_dir = storage_path.join( "projects" );
  fs::create_dir_all( &projects_dir ).expect( "Failed to create projects dir" );

  let output = common::clg_cmd()
    .args( [ ".status", &format!( "path::{}", storage_path.display() ), "verbosity::2" ] )
    .current_dir( env!( "CARGO_MANIFEST_DIR" ) )
    .output()
    .expect( "Failed to execute command" );

  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 succeed with path and verbosity. Got: {combined}"
  );

  // Should show storage information
  assert!(
    combined.to_lowercase().contains( "project" ) ||
    combined.to_lowercase().contains( "storage" ),
    "Should show storage information. Got: {combined}"
  );
}

/// Test .status `v::2` correctly reports User/Assistant entry breakdown (`bug_reproducer` issue-020)
///
/// ## Root Cause
/// `global_stats()` in `storage.rs` aggregated `total_entries` from `project_stats()`
/// but never aggregated `total_user_entries` or `total_assistant_entries`. The comment
/// "For now, we approximate this from total entries" was in the code but no approximation
/// was implemented — both fields were left at their zero-initialized defaults.
/// `ProjectStats` had no user/assistant breakdown fields at all, so `global_stats()`
/// had no source to aggregate them from.
///
/// ## Why Not Caught
/// The existing `test_status_path_with_verbosity` only checked that `verbosity::2` exits 0
/// and contains "project"/"storage" — it never asserted the entry breakdown values.
/// A test that doesn't check output values cannot catch wrong output values.
///
/// ## Fix Applied
/// Added `total_user_entries` / `total_assistant_entries` fields to `ProjectStats`.
/// Populated them in `project.project_stats()` from session stats.
/// Aggregated them in `storage.global_stats()`.
///
/// ## Prevention
/// When adding stats output to a command, write a test that checks the NUMERIC VALUES
/// in that output, not just that the output exists or the command succeeds.
/// A stats test that doesn't verify numbers is not a stats test.
///
/// ## Pitfall
/// "For now, we approximate" comments in code are time-bombs. They signal incomplete
/// implementation. Always leave a failing test or a TODO compile error (via
/// `compile_error!`) rather than a silent wrong value.
#[test]
fn test_status_verbosity2_user_assistant_counts_bug_reproducer_issue_020()
{
  let storage = TempDir::new().expect( "create temp dir" );

  // 2 user + 2 assistant entries across 2 sessions
  common::write_test_session( storage.path(), "status-test-proj", "sess-count-a", 4 );

  let output = common::clg_cmd()
    .args( [ ".status", "v::2" ] )
    .env( "CLAUDE_STORAGE_ROOT", storage.path() )
    .output()
    .expect( "Failed to execute .status" );

  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(),
    ".status v::2 should succeed. Got: {combined}"
  );

  // Must NOT show "User: 0, Assistant: 0" — both must be non-zero for a session
  // with both user and assistant entries
  assert!(
    !combined.contains( "User: 0, Assistant: 0" ),
    ".status v::2 must report non-zero user/assistant counts. Got: {combined}"
  );

  // Must show correct breakdown: 2 user + 2 assistant = 4 total
  assert!(
    combined.contains( "User: 2" ),
    ".status v::2 must report User: 2. Got: {combined}"
  );

  assert!(
    combined.contains( "Assistant: 2" ),
    ".status v::2 must report Assistant: 2. Got: {combined}"
  );
}