From ac8a24b30b5bc4ec1e1bd00dbf44670db8195df2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 5 May 2026 04:50:33 +0900 Subject: [PATCH] fix(config): emit section and section_value in JSON output for config subcommands (#2990) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `claw config model --output-format json` and all other section subcommands (`env`, `hooks`, `plugins`) returned identical output with no section field — the section arg was parsed but discarded (_section parameter). Fix: render_config_json now: - Passes section through to handler - Looks up the section value via runtime_config.get(), converting the internal JsonValue to serde_json::Value via render()+parse - Emits `section` (string) and `section_value` (JSON value or null) in the response envelope - Returns ok:false + error for unsupported section tokens Test: config_section_json_emits_section_and_value asserts: - No section field when no section arg - section + section_value fields present for all known sections - ok:false + error for unknown section Pinpoint: ROADMAP #126 --- rust/crates/rusty-claude-cli/src/main.rs | 45 +++++++++++++++++-- .../tests/output_format_contract.rs | 38 ++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 1b4acf8f..ecbf3edd 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -6210,7 +6210,7 @@ fn render_config_report(section: Option<&str>) -> Result, + section: Option<&str>, ) -> Result> { let cwd = env::current_dir()?; let loader = ConfigLoader::default_for(&cwd); @@ -6243,13 +6243,52 @@ fn render_config_json( }) .collect(); - Ok(serde_json::json!({ + let base = serde_json::json!({ "kind": "config", "cwd": cwd.display().to_string(), "loaded_files": loaded_paths.len(), "merged_keys": runtime_config.merged().len(), "files": files, - })) + }); + + if let Some(section) = section { + let section_rendered: Option = match section { + "env" => runtime_config.get("env").map(|v| v.render()), + "hooks" => runtime_config.get("hooks").map(|v| v.render()), + "model" => runtime_config.get("model").map(|v| v.render()), + "plugins" => runtime_config + .get("plugins") + .or_else(|| runtime_config.get("enabledPlugins")) + .map(|v| v.render()), + other => { + return Ok(serde_json::json!({ + "kind": "config", + "section": other, + "ok": false, + "error": format!("Unsupported config section '{other}'. Use env, hooks, model, or plugins."), + "cwd": cwd.display().to_string(), + "loaded_files": loaded_paths.len(), + "files": files, + })); + } + }; + // Parse the rendered JSON string back into serde_json::Value so that + // section_value is a real JSON object/array in the envelope, not a quoted string. + let section_value: serde_json::Value = section_rendered + .as_deref() + .and_then(|s| serde_json::from_str(s).ok()) + .unwrap_or(serde_json::Value::Null); + let mut obj = base; + let map = obj.as_object_mut().expect("base is object"); + map.insert( + "section".to_string(), + serde_json::Value::String(section.to_string()), + ); + map.insert("section_value".to_string(), section_value); + return Ok(obj); + } + + Ok(base) } fn render_memory_report() -> 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 5aaafab2..3c7d1f90 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -433,6 +433,44 @@ fn resumed_version_and_init_emit_structured_json_when_requested() { assert!(root.join("CLAUDE.md").exists()); } +#[test] +fn config_section_json_emits_section_and_value() { + let root = unique_temp_dir("config-section-json"); + fs::create_dir_all(&root).expect("temp dir should exist"); + + // Without a section: should return base envelope (no section field). + let base = assert_json_command(&root, &["--output-format", "json", "config"]); + assert_eq!(base["kind"], "config"); + assert!(base["loaded_files"].is_number()); + assert!(base["merged_keys"].is_number()); + assert!( + base.get("section").is_none(), + "no section field without section arg" + ); + + // With a known section: should add section + section_value fields. + for section in &["model", "env", "hooks", "plugins"] { + let result = assert_json_command(&root, &["--output-format", "json", "config", section]); + assert_eq!(result["kind"], "config", "section={section}"); + assert_eq!( + result["section"].as_str(), + Some(*section), + "section field must match requested section, got {result:?}" + ); + assert!( + result.get("section_value").is_some(), + "section_value field must be present for section={section}" + ); + } + + // With an unsupported section: should return ok:false + error field. + let bad = assert_json_command(&root, &["--output-format", "json", "config", "unknown"]); + assert_eq!(bad["kind"], "config"); + assert_eq!(bad["ok"], false); + assert!(bad["error"].as_str().is_some()); + assert!(bad["section"].as_str().is_some()); +} + fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value { assert_json_command_with_env(current_dir, args, &[]) }