fix: surface rejected configuration in a floating startup diagnostic

This commit is contained in:
2026-10-09 19:49:50 +02:00
parent de6b4d47f3
commit 788638f34e
6 changed files with 301 additions and 129 deletions
+70 -20
View File
@@ -95,7 +95,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
});
}
config.bindings = bindings;
config.lsp = lsp;
config.lsp =
crate::lsp_config::resolve(lsp).map_err(|error| format!("Ruby config: {error}"))?;
config.leader = leader;
config.file_explorer_side = file_explorer_side;
config.file_picker_view = file_picker_view;
@@ -379,8 +380,9 @@ enum Response {
Config {
version: u8,
plugins: Vec<PluginConfig>,
#[serde(deserialize_with = "crate::lsp_config::deserialize")]
lsp: Vec<crate::lsp::ServerConfig>,
// Keep this wire field required; resolve its configuration after decoding.
#[serde(deserialize_with = "serde_json::Value::deserialize")]
lsp: serde_json::Value,
bindings: Vec<Binding>,
leader: String,
file_explorer_side: String,
@@ -1085,33 +1087,40 @@ end"#,
}
let path = fixture.file(
"invalid.rb",
r#"Vamp.configure { |config| config.lsp["unlisted"] = true }"#,
r#"Vamp.configure { |config| config.lsp["ruby"] = true }"#,
);
let error = load_config(&path).unwrap_err();
assert!(!error.contains("Malformed Ruby protocol frame"), "{error}");
assert!(
error.contains("unlisted") && error.contains("preset"),
error.contains("\"ruby\"") && error.contains("command") && error.contains("extensions"),
"{error}"
);
}
#[test]
fn lsp_wire_configuration_is_validated_before_being_consumed() {
fn lsp_wire_configuration_is_resolved_after_decoding() {
let mut frame = serde_json::json!({
"version": 1, "type": "config", "plugins": [], "lsp": {},
"bindings": [], "leader": "space", "file_explorer_side": "right",
"file_picker_view": "tree", "file_explorer_view": "tree",
"clipboard_mode": "system"
});
assert!(matches!(
serde_json::from_value::<Response>(frame.clone()).unwrap(),
Response::Config { lsp, .. } if lsp.is_empty()
));
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert!(crate::lsp_config::resolve(lsp).unwrap().is_empty());
frame["lsp"] = serde_json::json!({"rust": true});
assert!(matches!(
serde_json::from_value::<Response>(frame.clone()).unwrap(),
Response::Config { lsp, .. }
if lsp.len() == 1 && lsp[0].command == ["rust-analyzer"]
));
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert_eq!(
crate::lsp_config::resolve(lsp).unwrap()[0].command,
["rust-analyzer"]
);
for value in [
serde_json::json!(null),
serde_json::json!([]),
@@ -1127,10 +1136,12 @@ end"#,
serde_json::json!({"rust": {"unexpected": true}}),
] {
frame["lsp"] = value;
assert!(
serde_json::from_value::<Response>(frame.clone()).is_err(),
"{frame}"
);
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert!(crate::lsp_config::resolve(lsp).is_err(), "{frame}");
}
let languages: serde_json::Map<String, serde_json::Value> = (0..17)
.map(|index| {
@@ -1141,11 +1152,50 @@ end"#,
})
.collect();
frame["lsp"] = serde_json::Value::Object(languages);
assert!(serde_json::from_value::<Response>(frame.clone()).is_err());
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert!(crate::lsp_config::resolve(lsp).is_err());
frame.as_object_mut().unwrap().remove("lsp");
assert!(serde_json::from_value::<Response>(frame).is_err());
}
#[test]
fn malformed_config_wire_remains_a_protocol_error() {
let fixture = Fixture::new();
for mutation in [
"return '['",
"frame.delete('lsp')",
"frame['leader'] = 7",
"frame['unexpected'] = true",
] {
let source = format!(
r#"
Vamp.configure {{ |config| config.lsp["ruby"] = true }}
module JSON
class << self
alias_method :original_generate, :generate
def generate(frame)
if frame['type'] == 'config'
{mutation}
end
original_generate(frame)
end
end
end
"#
);
let path = fixture.file("config.rb", &source);
let error = load_config(&path).unwrap_err();
assert!(
error.starts_with("Malformed Ruby protocol frame: "),
"{mutation}: {error}"
);
}
}
#[test]
fn crash_hang_callback_failure_and_fresh_host_recovery() {
let fixture = Fixture::new();