Defer unknown TUI config fields to the shared strict-config policy (#54766)

## Why

Unknown fields in TUI keymaps and profile TUI settings fail deserialization even when strict config validation is disabled.

## What changed

Remove Serde's `deny_unknown_fields` from these settings so unknown fields are ignored during deserialization and reported by the shared strict-config policy. Keep `schemars(deny_unknown_fields)` so the generated schema continues to disallow unknown fields.

GitOrigin-RevId: 06985161bc3bbb35648712288c1d00bdd50953ba
This commit is contained in:
Eric Traut
2026-10-17 02:27:35 +00:00
committed by copyberry
parent d13db678cb
commit 3b8f43a06b
3 changed files with 2 additions and 98 deletions
-1
View File
@@ -75,7 +75,6 @@ pub struct ConfigProfile {
/// TUI settings supported inside a named profile.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct ProfileTui {
/// Preferred layout for resume/fork session picker results.
+2 -82
View File
@@ -6,8 +6,8 @@
//!
//! Responsibilities:
//!
//! 1. Define strongly typed config contexts/actions with unknown-field
//! rejection.
//! 1. Define strongly typed config contexts/actions. Unknown fields are ignored
//! during deserialization and reported by the shared strict-config policy.
//! 2. Normalize accepted key aliases into canonical names.
//! 3. Reject malformed bindings early with user-facing diagnostics.
//!
@@ -131,7 +131,6 @@ impl KeybindingsSpec {
/// Global keybindings. These are used when a context does not define an override.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiGlobalKeymap {
/// Prefix for persistent, context-aware shortcut menus.
@@ -168,7 +167,6 @@ pub struct TuiGlobalKeymap {
/// Chat context keybindings.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiChatKeymap {
/// Open saved prompts and slash commands.
@@ -197,7 +195,6 @@ pub struct TuiChatKeymap {
/// Composer context keybindings. These override corresponding `global` actions.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiComposerKeymap {
/// Submit the current composer draft.
@@ -214,7 +211,6 @@ pub struct TuiComposerKeymap {
/// Editor context keybindings for text editing inside text areas.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiEditorKeymap {
/// Insert a newline in the editor.
@@ -388,7 +384,6 @@ pub struct TuiVimOperatorKeymap {
/// Search motions shared by Vim normal and operator-pending input.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiVimSearchKeymap {
/// Search forward in the active buffer (`/`).
@@ -403,7 +398,6 @@ pub struct TuiVimSearchKeymap {
/// Vim text-object keybindings for modal editing inside text areas.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiVimTextObjectKeymap {
/// Text object: word.
@@ -428,7 +422,6 @@ pub struct TuiVimTextObjectKeymap {
/// Pager context keybindings for transcript and static overlays.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiPagerKeymap {
/// Scroll up by one row.
@@ -457,7 +450,6 @@ pub struct TuiPagerKeymap {
/// List selection context keybindings for popup-style selectable lists.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiListKeymap {
/// Move list selection up.
@@ -484,7 +476,6 @@ pub struct TuiListKeymap {
/// Shortcuts specific to the shared agents overview.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiAgentsKeymap {
/// Open the session resume picker.
@@ -515,7 +506,6 @@ pub struct TuiAgentsKeymap {
/// Approval overlay keybindings.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiApprovalKeymap {
/// Open the full-screen approval details view.
@@ -547,7 +537,6 @@ pub struct TuiApprovalKeymap {
/// `RuntimeKeymap` first so precedence, empty-list unbinding, and duplicate-key
/// validation are applied consistently.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiKeymap {
#[serde(default)]
@@ -767,75 +756,6 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn misplaced_action_at_keymap_root_is_rejected() {
// Actions placed directly under [tui.keymap] instead of a context
// sub-table (e.g. [tui.keymap.global]) must produce a parse error,
// not be silently ignored.
let toml_input = r#"
open_transcript = "ctrl-s"
"#;
let result = toml::from_str::<TuiKeymap>(toml_input);
assert!(
result.is_err(),
"expected error for action at keymap root, got: {result:?}"
);
}
#[test]
fn misspelled_action_under_context_is_rejected() {
let toml_input = r#"
[global]
open_transcrip = "ctrl-x"
"#;
let err = toml::from_str::<TuiKeymap>(toml_input)
.expect_err("expected unknown action under context");
assert!(
err.to_string().contains("open_transcrip"),
"expected error to mention misspelled field, got: {err}"
);
}
#[test]
fn misspelled_vim_text_object_action_is_rejected() {
let toml_input = r#"
[vim_text_object]
double_quotes = "shift-quote"
"#;
let err = toml::from_str::<TuiKeymap>(toml_input)
.expect_err("expected unknown vim text object action");
assert!(
err.to_string().contains("double_quotes"),
"expected error to mention misspelled field, got: {err}"
);
}
#[test]
fn removed_backtrack_actions_are_rejected() {
for (context, action) in [
("global", "edit_previous_message"),
("global", "confirm_edit_previous_message"),
("chat", "edit_previous_message"),
("chat", "confirm_edit_previous_message"),
("pager", "edit_previous_message"),
("pager", "edit_next_message"),
("pager", "confirm_edit_message"),
] {
let toml_input = format!(
r#"
[{context}]
{action} = "ctrl-x"
"#
);
let err = toml::from_str::<TuiKeymap>(&toml_input)
.expect_err("expected removed backtrack action to be rejected");
assert!(
err.to_string().contains(action),
"expected error to mention removed field {action}, got: {err}"
);
}
}
#[test]
fn action_under_global_context_is_accepted() {
let toml_input = r#"
-15
View File
@@ -4738,21 +4738,6 @@ async fn legacy_remote_thread_store_endpoint_is_rejected() {
assert!(err.to_string().contains("no longer supported"));
}
#[test]
fn profile_tui_rejects_unsupported_settings() {
let err = toml::from_str::<ConfigToml>(
r#"profile = "work"
[profiles.work.tui]
theme = "dark"
"#,
)
.expect_err("profile TUI config should only accept supported fields");
assert!(err.to_string().contains("unknown field"));
assert!(err.to_string().contains("theme"));
}
#[tokio::test]
async fn runtime_config_resolves_session_picker_view_default_and_override() {
let cfg = Config::load_from_base_config_with_overrides(