From 2bab4080d6af20bdb3bec7ca3d30ad82fb0eb953 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Sun, 5 Apr 2026 23:01:50 +0000 Subject: [PATCH] Keep resumed /status JSON aligned with live status output The resumed slash-command path built a reduced status JSON payload by hand, so it drifted from the fresh status schema and dropped metadata like model, permission mode, workspace counters, and sandbox details. Reuse a shared status JSON builder for both code paths and tighten the resume regression tests to lock parity in place. Constraint: Resume mode does not carry an active runtime model, so restored sessions continue to report the existing restored-session sentinel value Rejected: Copy the fresh status JSON shape into the resume path again | would recreate the same schema drift risk Confidence: high Scope-risk: narrow Directive: Keep resumed and fresh /status JSON on the same helper so future schema changes stay in parity Tested: Reproduced failure in temporary HEAD worktree with strengthened resumed_status_command_emits_structured_json_when_requested Tested: cargo test -p rusty-claude-cli resumed_status_command_emits_structured_json_when_requested --test resume_slash_commands -- --exact --nocapture Tested: cargo test -p rusty-claude-cli doctor_and_resume_status_emit_json_when_requested --test output_format_contract -- --exact --nocapture Tested: cargo test --workspace Tested: cargo fmt --check Tested: cargo clippy --workspace --all-targets -- -D warnings --- rust/crates/rusty-claude-cli/src/main.rs | 129 ++++++++++-------- .../tests/output_format_contract.rs | 5 +- .../tests/resume_slash_commands.rs | 9 +- 3 files changed, 81 insertions(+), 62 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index aa164c9..20ecf03 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1570,24 +1570,19 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu && matches!(command, SlashCommand::Status) { let tracker = UsageTracker::from_session(&session); - let usage = tracker.cumulative_usage(); let context = status_context(Some(&resolved_path)).expect("status context"); - let value = json!({ - "kind": "status", - "messages": session.messages.len(), - "turns": tracker.turns(), - "latest_total": tracker.current_turn_usage().total_tokens(), - "cumulative_input": usage.input_tokens, - "cumulative_output": usage.output_tokens, - "cumulative_total": usage.total_tokens(), - "workspace": { - "cwd": context.cwd, - "project_root": context.project_root, - "git_branch": context.git_branch, - "git_state": context.git_summary.headline(), - "session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()), - } - }); + let value = status_json_value( + "restored-session", + StatusUsage { + message_count: session.messages.len(), + turns: tracker.turns(), + latest: tracker.current_turn_usage(), + cumulative: tracker.cumulative_usage(), + estimated_tokens: 0, + }, + default_permission_mode().as_str(), + &context, + ); println!( "{}", serde_json::to_string_pretty(&value).expect("status json") @@ -3845,54 +3840,68 @@ fn print_status_snapshot( ), CliOutputFormat::Json => println!( "{}", - serde_json::to_string_pretty(&json!({ - "kind": "status", - "model": model, - "permission_mode": permission_mode.as_str(), - "usage": { - "messages": usage.message_count, - "turns": usage.turns, - "latest_total": usage.latest.total_tokens(), - "cumulative_input": usage.cumulative.input_tokens, - "cumulative_output": usage.cumulative.output_tokens, - "cumulative_total": usage.cumulative.total_tokens(), - "estimated_tokens": usage.estimated_tokens, - }, - "workspace": { - "cwd": context.cwd, - "project_root": context.project_root, - "git_branch": context.git_branch, - "git_state": context.git_summary.headline(), - "changed_files": context.git_summary.changed_files, - "staged_files": context.git_summary.staged_files, - "unstaged_files": context.git_summary.unstaged_files, - "untracked_files": context.git_summary.untracked_files, - "session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()), - "loaded_config_files": context.loaded_config_files, - "discovered_config_files": context.discovered_config_files, - "memory_file_count": context.memory_file_count, - }, - "sandbox": { - "enabled": context.sandbox_status.enabled, - "active": context.sandbox_status.active, - "supported": context.sandbox_status.supported, - "in_container": context.sandbox_status.in_container, - "requested_namespace": context.sandbox_status.requested.namespace_restrictions, - "active_namespace": context.sandbox_status.namespace_active, - "requested_network": context.sandbox_status.requested.network_isolation, - "active_network": context.sandbox_status.network_active, - "filesystem_mode": context.sandbox_status.filesystem_mode.as_str(), - "filesystem_active": context.sandbox_status.filesystem_active, - "allowed_mounts": context.sandbox_status.allowed_mounts, - "markers": context.sandbox_status.container_markers, - "fallback_reason": context.sandbox_status.fallback_reason, - } - }))? + serde_json::to_string_pretty(&status_json_value( + model, + usage, + permission_mode.as_str(), + &context, + ))? ), } Ok(()) } +fn status_json_value( + model: &str, + usage: StatusUsage, + permission_mode: &str, + context: &StatusContext, +) -> serde_json::Value { + json!({ + "kind": "status", + "model": model, + "permission_mode": permission_mode, + "usage": { + "messages": usage.message_count, + "turns": usage.turns, + "latest_total": usage.latest.total_tokens(), + "cumulative_input": usage.cumulative.input_tokens, + "cumulative_output": usage.cumulative.output_tokens, + "cumulative_total": usage.cumulative.total_tokens(), + "estimated_tokens": usage.estimated_tokens, + }, + "workspace": { + "cwd": context.cwd, + "project_root": context.project_root, + "git_branch": context.git_branch, + "git_state": context.git_summary.headline(), + "changed_files": context.git_summary.changed_files, + "staged_files": context.git_summary.staged_files, + "unstaged_files": context.git_summary.unstaged_files, + "untracked_files": context.git_summary.untracked_files, + "session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()), + "loaded_config_files": context.loaded_config_files, + "discovered_config_files": context.discovered_config_files, + "memory_file_count": context.memory_file_count, + }, + "sandbox": { + "enabled": context.sandbox_status.enabled, + "active": context.sandbox_status.active, + "supported": context.sandbox_status.supported, + "in_container": context.sandbox_status.in_container, + "requested_namespace": context.sandbox_status.requested.namespace_restrictions, + "active_namespace": context.sandbox_status.namespace_active, + "requested_network": context.sandbox_status.requested.network_isolation, + "active_network": context.sandbox_status.network_active, + "filesystem_mode": context.sandbox_status.filesystem_mode.as_str(), + "filesystem_active": context.sandbox_status.filesystem_active, + "allowed_mounts": context.sandbox_status.allowed_mounts, + "markers": context.sandbox_status.container_markers, + "fallback_reason": context.sandbox_status.fallback_reason, + } + }) +} + fn status_context( session_path: Option<&Path>, ) -> Result> { diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index 7b6848e..74e81f5 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -130,7 +130,10 @@ fn doctor_and_resume_status_emit_json_when_requested() { ], ); assert_eq!(resumed["kind"], "status"); - assert_eq!(resumed["messages"], 1); + assert_eq!(resumed["model"], "restored-session"); + assert_eq!(resumed["usage"]["messages"], 1); + assert!(resumed["workspace"]["cwd"].as_str().is_some()); + assert!(resumed["sandbox"]["filesystem_mode"].as_str().is_some()); } fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value { diff --git a/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs b/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs index 1d060c2..273cfa7 100644 --- a/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs +++ b/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs @@ -261,11 +261,18 @@ fn resumed_status_command_emits_structured_json_when_requested() { let parsed: Value = serde_json::from_str(stdout.trim()).expect("resume status output should be json"); assert_eq!(parsed["kind"], "status"); - assert_eq!(parsed["messages"], 1); + assert_eq!(parsed["model"], "restored-session"); + assert_eq!(parsed["permission_mode"], "danger-full-access"); + assert_eq!(parsed["usage"]["messages"], 1); + assert!(parsed["usage"]["turns"].is_number()); + assert!(parsed["workspace"]["cwd"].as_str().is_some()); assert_eq!( parsed["workspace"]["session"], session_path.to_str().expect("utf8 path") ); + assert!(parsed["workspace"]["changed_files"].is_number()); + assert_eq!(parsed["workspace"]["loaded_config_files"].as_u64(), Some(0)); + assert!(parsed["sandbox"]["filesystem_mode"].as_str().is_some()); } fn run_claw(current_dir: &Path, args: &[&str]) -> Output {