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
This commit is contained in:
Yeachan-Heo
2026-04-05 23:01:50 +00:00
parent f7321ca05d
commit 2bab4080d6
3 changed files with 81 additions and 62 deletions

View File

@@ -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<StatusContext, Box<dyn std::error::Error>> {

View File

@@ -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 {

View File

@@ -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 {