From 1da4778e76b8f107d24fb61b3efc856e78370362 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 08:14:16 +0000 Subject: [PATCH 1/3] test(diffguard-lsp): cover server.rs edge cases Add 53 focused unit tests covering helper functions (normalize_option_string, parse_init_options, nth_string_arg, uri_to_file_path, explain_rule_message, findings_to_diagnostics), DocumentState behaviours (apply_changes / mark_saved variants), ServerState::from_initialize branches, is_config_uri / reload_config, and the request and notification dispatch paths (invalid params, unknown command, unsupported method, did-open with non-file URI, did-change for unknown document, did-close clears diagnostics, exit signal, did-save baseline reset) via Connection::memory(). Server.rs region coverage moves from 75.84% to 91.62%. Closes #1609. --- crates/diffguard-lsp/src/server.rs | 987 ++++++++++++++++++++++++++++- 1 file changed, 957 insertions(+), 30 deletions(-) diff --git a/crates/diffguard-lsp/src/server.rs b/crates/diffguard-lsp/src/server.rs index 51b37853..6ad89857 100644 --- a/crates/diffguard-lsp/src/server.rs +++ b/crates/diffguard-lsp/src/server.rs @@ -959,6 +959,86 @@ fn uri_to_file_path(uri: &Uri) -> Option { #[cfg(test)] mod tests { use super::*; + use lsp_types::{ + DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, + DidOpenTextDocumentParams, DidSaveTextDocumentParams, TextDocumentIdentifier, + TextDocumentItem, VersionedTextDocumentIdentifier, + }; + + fn sample_rule(id: &str, url: Option<&str>) -> diffguard_types::RuleConfig { + diffguard_types::RuleConfig { + id: id.to_string(), + description: String::new(), + severity: Severity::Warn, + message: "msg".to_string(), + languages: vec![], + patterns: vec!["x".to_string()], + paths: vec![], + exclude_paths: vec![], + ignore_comments: false, + ignore_strings: false, + match_mode: diffguard_types::MatchMode::Any, + multiline: false, + multiline_window: None, + context_patterns: vec![], + context_window: None, + escalate_patterns: vec![], + escalate_window: None, + escalate_to: None, + depends_on: vec![], + help: None, + url: url.map(|s| s.to_string()), + tags: vec![], + test_cases: vec![], + } + } + + fn config_with_rules(rules: Vec) -> ConfigFile { + ConfigFile { + includes: vec![], + defaults: diffguard_types::Defaults::default(), + rule: rules, + } + } + + fn parse_uri(uri_str: &str) -> Uri { + uri_str.parse().expect("uri parse") + } + + fn drain_messages(connection: &Connection) -> Vec { + let mut messages = Vec::new(); + while let Ok(msg) = connection.receiver.try_recv() { + messages.push(msg); + } + messages + } + + fn find_response(messages: &[Message]) -> Option<&Response> { + messages.iter().find_map(|m| match m { + Message::Response(response) => Some(response), + _ => None, + }) + } + + fn find_notification<'a>(messages: &'a [Message], method: &str) -> Option<&'a Notification> { + messages.iter().find_map(|m| match m { + Message::Notification(notif) if notif.method == method => Some(notif), + _ => None, + }) + } + + fn empty_state() -> ServerState { + ServerState { + workspace_root: None, + config_path: None, + no_default_rules: true, + max_findings: DEFAULT_MAX_FINDINGS, + force_language: None, + config: ConfigFile::built_in(), + documents: HashMap::new(), + git_support: GitSupport::Unknown, + } + } #[test] fn server_capabilities_include_text_sync_and_actions() { @@ -986,39 +1066,14 @@ mod tests { #[test] fn build_code_actions_contains_explain_action() { - let config = ConfigFile { - includes: vec![], - defaults: diffguard_types::Defaults::default(), - rule: vec![diffguard_types::RuleConfig { - id: "rust.no_unwrap".to_string(), - description: String::new(), - severity: Severity::Warn, - message: "Avoid unwrap".to_string(), - languages: vec![], - patterns: vec!["unwrap".to_string()], - paths: vec![], - exclude_paths: vec![], - ignore_comments: false, - ignore_strings: false, - match_mode: diffguard_types::MatchMode::Any, - multiline: false, - multiline_window: None, - context_patterns: vec![], - context_window: None, - escalate_patterns: vec![], - escalate_window: None, - escalate_to: None, - depends_on: vec![], - help: None, - url: Some("https://example.com/rule".to_string()), - tags: vec![], - test_cases: vec![], - }], - }; + let config = config_with_rules(vec![sample_rule( + "rust.no_unwrap", + Some("https://example.com/rule"), + )]); let params = CodeActionParams { text_document: lsp_types::TextDocumentIdentifier { - uri: "file:///tmp/test.rs".parse().expect("uri"), + uri: parse_uri("file:///tmp/test.rs"), }, range: Range::new(Position::new(0, 0), Position::new(0, 10)), context: lsp_types::CodeActionContext { @@ -1036,4 +1091,876 @@ mod tests { let actions = build_code_actions(&config, ¶ms); assert!(!actions.is_empty()); } + + // --------------------------------------------------------------------- + // Pure-function helpers + // --------------------------------------------------------------------- + + #[test] + fn normalize_option_string_returns_none_for_whitespace_input() { + assert_eq!(normalize_option_string(None), None); + assert_eq!(normalize_option_string(Some(String::new())), None); + assert_eq!(normalize_option_string(Some(" ".to_string())), None); + assert_eq!(normalize_option_string(Some("\t\n ".to_string())), None); + assert_eq!( + normalize_option_string(Some(" rust ".to_string())), + Some("rust".to_string()) + ); + } + + #[test] + fn parse_init_options_returns_default_when_missing_or_invalid() { + let none_opts = parse_init_options(None); + assert!(none_opts.config_path.is_none()); + assert!(!none_opts.no_default_rules); + assert!(none_opts.max_findings.is_none()); + + // An object that doesn't deserialize to InitOptions falls back to default. + let bogus = serde_json::json!({ "configPath": 42 }); + let defaulted = parse_init_options(Some(&bogus)); + assert!(defaulted.config_path.is_none()); + } + + #[test] + fn parse_init_options_reads_camel_case_fields() { + let value = serde_json::json!({ + "configPath": "config.toml", + "noDefaultRules": true, + "maxFindings": 17, + "forceLanguage": "rust", + }); + let opts = parse_init_options(Some(&value)); + assert_eq!(opts.config_path.as_deref(), Some("config.toml")); + assert!(opts.no_default_rules); + assert_eq!(opts.max_findings, Some(17)); + assert_eq!(opts.force_language.as_deref(), Some("rust")); + } + + #[test] + fn nth_string_arg_returns_none_for_missing_or_wrong_type() { + let args = vec![json!("first"), json!(42), json!(null)]; + assert_eq!(nth_string_arg(&args, 0), Some("first".to_string())); + // Non-string value + assert_eq!(nth_string_arg(&args, 1), None); + // null value + assert_eq!(nth_string_arg(&args, 2), None); + // out-of-bounds index + assert_eq!(nth_string_arg(&args, 5), None); + // empty list + assert_eq!(nth_string_arg(&[], 0), None); + } + + #[test] + fn uri_to_file_path_returns_none_for_malformed_uri() { + // A scheme without authority is not a valid file URL. + let uri: Uri = parse_uri("http://example.com/foo.rs"); + assert!(uri_to_file_path(&uri).is_none()); + } + + #[test] + fn uri_to_file_path_parses_valid_file_uri() { + let uri: Uri = parse_uri("file:///tmp/some/path.rs"); + let parsed = uri_to_file_path(&uri).expect("file path"); + assert_eq!(parsed, PathBuf::from("/tmp/some/path.rs")); + } + + #[test] + fn explain_rule_message_reports_found_when_rule_exists() { + let config = config_with_rules(vec![sample_rule("rust.no_unwrap", None)]); + let (message, found) = explain_rule_message(&config, "rust.no_unwrap"); + assert!(found); + assert!(message.contains("Rule: rust.no_unwrap")); + } + + #[test] + fn explain_rule_message_includes_did_you_mean_when_rule_missing() { + let config = config_with_rules(vec![sample_rule("rust.no_unwrap", None)]); + let (message, found) = explain_rule_message(&config, "rust.no_unwra"); + assert!(!found); + assert!(message.contains("not found")); + assert!(message.contains("Did you mean:")); + assert!(message.contains("rust.no_unwrap")); + } + + #[test] + fn explain_rule_message_omits_suggestions_when_no_similar_rules() { + let config = config_with_rules(vec![sample_rule("rust.no_unwrap", None)]); + let (message, found) = explain_rule_message(&config, "totally.unrelated_thing_xyz"); + assert!(!found); + assert!(message.contains("not found")); + assert!(!message.contains("Did you mean:")); + } + + #[test] + fn findings_to_diagnostics_maps_severity_and_sorts_by_position() { + let findings = vec![ + Finding { + rule_id: "r1".to_string(), + severity: Severity::Info, + message: "info-msg".to_string(), + path: "src/a.rs".to_string(), + line: 3, + column: Some(2), + match_text: "ab".to_string(), + snippet: String::new(), + }, + Finding { + rule_id: "r2".to_string(), + severity: Severity::Error, + message: "err-msg".to_string(), + path: "src/a.rs".to_string(), + line: 1, + column: Some(1), + match_text: "y".to_string(), + snippet: String::new(), + }, + Finding { + rule_id: "r3".to_string(), + severity: Severity::Warn, + message: "warn-msg".to_string(), + path: "src/a.rs".to_string(), + line: 2, + column: None, + match_text: String::new(), + snippet: String::new(), + }, + ]; + + let diagnostics = findings_to_diagnostics(&findings); + assert_eq!(diagnostics.len(), 3); + // Sorted ascending by line. + assert_eq!(diagnostics[0].range.start.line, 0); + assert_eq!(diagnostics[1].range.start.line, 1); + assert_eq!(diagnostics[2].range.start.line, 2); + assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR)); + assert_eq!(diagnostics[1].severity, Some(DiagnosticSeverity::WARNING)); + assert_eq!( + diagnostics[2].severity, + Some(DiagnosticSeverity::INFORMATION) + ); + // Empty match_text yields a span of length 1. + assert_eq!(diagnostics[1].range.end.character, 1); + } + + // --------------------------------------------------------------------- + // DocumentState behaviour + // --------------------------------------------------------------------- + + #[test] + fn document_state_apply_changes_handles_empty_change_list() { + let mut doc = DocumentState::new(PathBuf::from("a.rs"), 1, "hello\n".to_string()); + doc.apply_changes(&[]).expect("apply ok"); + assert_eq!(doc.text, "hello\n"); + assert!(doc.changed_lines.is_empty()); + } + + #[test] + fn document_state_apply_changes_replaces_text_on_full_change() { + let mut doc = DocumentState::new(PathBuf::from("a.rs"), 1, "before\n".to_string()); + let change = TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "after\n".to_string(), + }; + doc.apply_changes(&[change]).expect("apply ok"); + assert_eq!(doc.text, "after\n"); + assert!(!doc.changed_lines.is_empty()); + } + + #[test] + fn document_state_apply_changes_uses_last_full_replacement() { + let mut doc = DocumentState::new(PathBuf::from("a.rs"), 1, "orig\n".to_string()); + let changes = vec![ + TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "first\n".to_string(), + }, + TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "final\n".to_string(), + }, + ]; + doc.apply_changes(&changes).expect("apply ok"); + assert_eq!(doc.text, "final\n"); + } + + #[test] + fn document_state_mark_saved_with_text_updates_baseline_and_clears_changes() { + let mut doc = DocumentState::new(PathBuf::from("a.rs"), 1, "before\n".to_string()); + let change = TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "after\n".to_string(), + }; + doc.apply_changes(&[change]).expect("apply ok"); + assert!(!doc.changed_lines.is_empty()); + + doc.mark_saved(Some("saved\n".to_string())); + assert_eq!(doc.text, "saved\n"); + assert_eq!(doc.baseline_text, "saved\n"); + assert!(doc.changed_lines.is_empty()); + } + + #[test] + fn document_state_mark_saved_without_text_keeps_current_text() { + let mut doc = DocumentState::new(PathBuf::from("a.rs"), 1, "before\n".to_string()); + let change = TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "after\n".to_string(), + }; + doc.apply_changes(&[change]).expect("apply ok"); + + doc.mark_saved(None); + assert_eq!(doc.text, "after\n"); + assert_eq!(doc.baseline_text, "after\n"); + assert!(doc.changed_lines.is_empty()); + } + + // --------------------------------------------------------------------- + // extract_workspace_root variants + // --------------------------------------------------------------------- + + #[test] + #[allow(deprecated)] + fn extract_workspace_root_prefers_workspace_folders() { + let params = InitializeParams { + workspace_folders: Some(vec![lsp_types::WorkspaceFolder { + uri: parse_uri("file:///tmp/ws"), + name: "ws".to_string(), + }]), + ..InitializeParams::default() + }; + let root = extract_workspace_root(¶ms); + assert_eq!(root, Some(PathBuf::from("/tmp/ws"))); + } + + #[test] + #[allow(deprecated)] + fn extract_workspace_root_falls_back_to_root_uri() { + let params = InitializeParams { + workspace_folders: None, + root_uri: Some(parse_uri("file:///tmp/ws-from-uri")), + ..InitializeParams::default() + }; + let root = extract_workspace_root(¶ms); + assert_eq!(root, Some(PathBuf::from("/tmp/ws-from-uri"))); + } + + #[test] + #[allow(deprecated)] + fn extract_workspace_root_falls_back_to_root_path() { + let params = InitializeParams { + workspace_folders: None, + root_uri: None, + root_path: Some("/tmp/legacy".to_string()), + ..InitializeParams::default() + }; + let root = extract_workspace_root(¶ms); + assert_eq!(root, Some(PathBuf::from("/tmp/legacy"))); + } + + #[test] + #[allow(deprecated)] + fn extract_workspace_root_returns_none_when_nothing_provided() { + let params = InitializeParams::default(); + assert!(extract_workspace_root(¶ms).is_none()); + } + + // --------------------------------------------------------------------- + // ServerState::from_initialize + // --------------------------------------------------------------------- + + #[test] + fn from_initialize_uses_default_max_findings_when_unset() { + let params = InitializeParams::default(); + let (state, _warning) = ServerState::from_initialize(¶ms); + assert_eq!(state.max_findings, DEFAULT_MAX_FINDINGS); + assert!(state.workspace_root.is_none()); + } + + #[test] + fn from_initialize_clamps_zero_max_findings_to_one() { + let params = InitializeParams { + initialization_options: Some(json!({ "maxFindings": 0 })), + ..InitializeParams::default() + }; + let (state, _warning) = ServerState::from_initialize(¶ms); + assert_eq!(state.max_findings, 1); + } + + #[test] + fn from_initialize_passes_force_language_through_trim() { + let params = InitializeParams { + initialization_options: Some(json!({ "forceLanguage": " rust " })), + ..InitializeParams::default() + }; + let (state, _warning) = ServerState::from_initialize(¶ms); + assert_eq!(state.force_language.as_deref(), Some("rust")); + } + + #[test] + fn from_initialize_emits_warning_when_config_path_fails_to_load() { + let params = InitializeParams { + initialization_options: Some(json!({ + "configPath": "/nonexistent/diffguard-missing.toml" + })), + ..InitializeParams::default() + }; + let (state, warning) = ServerState::from_initialize(¶ms); + // Falls back to built-in rules when load fails. + assert!(!state.config.rule.is_empty()); + assert!(warning.is_some()); + let warning_text = warning.expect("warning string"); + assert!(warning_text.contains("failed to load config")); + } + + // --------------------------------------------------------------------- + // is_config_uri + // --------------------------------------------------------------------- + + #[test] + fn is_config_uri_returns_false_when_no_config_path() { + let state = empty_state(); + let uri = parse_uri("file:///tmp/foo.rs"); + assert!(!is_config_uri(&state, &uri)); + } + + #[test] + fn is_config_uri_returns_false_when_uri_cannot_be_parsed() { + let mut state = empty_state(); + state.config_path = Some(PathBuf::from("/tmp/diffguard.toml")); + let uri = parse_uri("http://example.com/foo.toml"); + assert!(!is_config_uri(&state, &uri)); + } + + #[test] + fn is_config_uri_matches_normalized_paths() { + let mut state = empty_state(); + state.config_path = Some(PathBuf::from("/tmp/diffguard.toml")); + let uri = parse_uri("file:///tmp/diffguard.toml"); + assert!(is_config_uri(&state, &uri)); + } + + // --------------------------------------------------------------------- + // reload_config + // --------------------------------------------------------------------- + + #[test] + fn reload_config_reports_rule_count_on_success() { + let mut state = empty_state(); + // With no config_path and no_default_rules=true, load returns built_in but + // we set no_default_rules=true so we get an empty/built-in-less config. + state.no_default_rules = true; + state.config_path = None; + let result = reload_config(&mut state).expect("reload ok"); + assert!(result.contains("config reloaded")); + } + + #[test] + fn reload_config_resets_to_built_in_on_failure() { + let mut state = empty_state(); + state.config_path = Some(PathBuf::from("/nonexistent/diffguard-missing.toml")); + state.git_support = GitSupport::Available; + let err = reload_config(&mut state).expect_err("reload should fail"); + assert!(err.to_string().contains("failed to reload config")); + // After failure, git_support is reset and config falls back to built-in. + assert_eq!(state.git_support, GitSupport::Unknown); + assert!(!state.config.rule.is_empty()); + } + + // --------------------------------------------------------------------- + // handle_request / handle_notification with Connection::memory() + // --------------------------------------------------------------------- + + #[test] + fn handle_request_returns_method_not_found_for_unsupported_method() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let request = Request { + id: RequestId::from(1), + method: "totally/unsupported".to_string(), + params: json!({}), + }; + handle_request(&server, &mut state, request).expect("handle ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response sent"); + let error = response.error.as_ref().expect("error response"); + assert_eq!(error.code, METHOD_NOT_FOUND); + assert!(error.message.contains("totally/unsupported")); + } + + #[test] + fn handle_code_action_request_rejects_invalid_params() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let request = Request { + id: RequestId::from(7), + method: CodeActionRequest::METHOD.to_string(), + params: json!({ "totally": "wrong" }), + }; + handle_request(&server, &mut state, request).expect("handle ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response sent"); + let error = response.error.as_ref().expect("error response"); + assert_eq!(error.code, INVALID_PARAMS); + assert!(error.message.contains("invalid CodeActionParams")); + } + + #[test] + fn handle_execute_command_request_rejects_invalid_params() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let request = Request { + id: RequestId::from(2), + method: ExecuteCommand::METHOD.to_string(), + params: json!({ "nope": true }), + }; + handle_request(&server, &mut state, request).expect("handle ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response sent"); + let error = response.error.as_ref().expect("error response"); + assert_eq!(error.code, INVALID_PARAMS); + assert!(error.message.contains("invalid ExecuteCommandParams")); + } + + #[test] + fn handle_execute_command_rejects_explain_rule_without_arg() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let params = ExecuteCommandParams { + command: CMD_EXPLAIN_RULE.to_string(), + arguments: vec![], + work_done_progress_params: Default::default(), + }; + let request = Request { + id: RequestId::from(3), + method: ExecuteCommand::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + handle_request(&server, &mut state, request).expect("handle ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response sent"); + let error = response.error.as_ref().expect("error response"); + assert_eq!(error.code, INVALID_PARAMS); + assert!(error.message.contains("missing rule ID")); + } + + #[test] + fn handle_execute_command_rejects_show_rule_url_without_url() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let params = ExecuteCommandParams { + command: CMD_SHOW_RULE_URL.to_string(), + arguments: vec![], + work_done_progress_params: Default::default(), + }; + let request = Request { + id: RequestId::from(4), + method: ExecuteCommand::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + handle_request(&server, &mut state, request).expect("handle ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response sent"); + let error = response.error.as_ref().expect("error response"); + assert_eq!(error.code, INVALID_PARAMS); + assert!(error.message.contains("missing URL")); + } + + #[test] + fn handle_execute_command_rejects_unknown_command() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let params = ExecuteCommandParams { + command: "diffguard.unknown".to_string(), + arguments: vec![], + work_done_progress_params: Default::default(), + }; + let request = Request { + id: RequestId::from(5), + method: ExecuteCommand::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + handle_request(&server, &mut state, request).expect("handle ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response sent"); + let error = response.error.as_ref().expect("error response"); + assert_eq!(error.code, INVALID_PARAMS); + assert!(error.message.contains("unsupported command")); + assert!(error.message.contains("diffguard.unknown")); + } + + #[test] + fn handle_execute_command_explain_rule_responds_with_found_payload() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + state.config = config_with_rules(vec![sample_rule("rust.no_unwrap", None)]); + let params = ExecuteCommandParams { + command: CMD_EXPLAIN_RULE.to_string(), + arguments: vec![json!("rust.no_unwrap")], + work_done_progress_params: Default::default(), + }; + let request = Request { + id: RequestId::from(6), + method: ExecuteCommand::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + handle_request(&server, &mut state, request).expect("handle ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response sent"); + let result = response.result.as_ref().expect("result payload"); + assert_eq!(result.get("found").and_then(|v| v.as_bool()), Some(true)); + assert_eq!( + result.get("ruleId").and_then(|v| v.as_str()), + Some("rust.no_unwrap") + ); + // A showMessage notification accompanies the response. + assert!(find_notification(&messages, ShowMessage::METHOD).is_some()); + } + + #[test] + fn handle_execute_command_show_rule_url_uses_default_label_when_id_missing() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let params = ExecuteCommandParams { + command: CMD_SHOW_RULE_URL.to_string(), + arguments: vec![json!("https://example.com/docs")], + work_done_progress_params: Default::default(), + }; + let request = Request { + id: RequestId::from(8), + method: ExecuteCommand::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + handle_request(&server, &mut state, request).expect("handle ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response sent"); + let result = response.result.as_ref().expect("result payload"); + assert_eq!( + result.get("url").and_then(|v| v.as_str()), + Some("https://example.com/docs") + ); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); + let body = notif.params.to_string(); + assert!(body.contains("diffguard documentation")); + } + + #[test] + fn handle_notification_invalid_did_open_emits_warning() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let notification = Notification { + method: DidOpenTextDocument::METHOD.to_string(), + params: json!({ "garbage": true }), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + let messages = drain_messages(&client); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + assert!(notif.params.to_string().contains("invalid didOpen")); + } + + #[test] + fn handle_notification_invalid_did_change_emits_warning() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let notification = Notification { + method: DidChangeTextDocument::METHOD.to_string(), + params: json!({ "garbage": true }), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + let messages = drain_messages(&client); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + assert!(notif.params.to_string().contains("invalid didChange")); + } + + #[test] + fn handle_notification_invalid_did_save_emits_warning() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let notification = Notification { + method: DidSaveTextDocument::METHOD.to_string(), + params: json!({ "garbage": true }), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + let messages = drain_messages(&client); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + assert!(notif.params.to_string().contains("invalid didSave")); + } + + #[test] + fn handle_notification_invalid_did_close_emits_warning() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let notification = Notification { + method: DidCloseTextDocument::METHOD.to_string(), + params: json!({ "garbage": true }), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + let messages = drain_messages(&client); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + assert!(notif.params.to_string().contains("invalid didClose")); + } + + #[test] + fn handle_notification_invalid_did_change_configuration_emits_warning() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let notification = Notification { + method: DidChangeConfiguration::METHOD.to_string(), + params: json!("not-an-object"), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + let messages = drain_messages(&client); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + assert!( + notif + .params + .to_string() + .contains("invalid didChangeConfiguration") + ); + } + + #[test] + fn handle_notification_exit_signals_break_loop() { + let (_client, server) = Connection::memory(); + let mut state = empty_state(); + let notification = Notification { + method: Exit::METHOD.to_string(), + params: json!(null), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(exit); + } + + #[test] + fn handle_notification_unknown_method_is_silent_no_op() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let notification = Notification { + method: "made/up/method".to_string(), + params: json!({}), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + let messages = drain_messages(&client); + assert!(messages.is_empty()); + } + + #[test] + fn handle_notification_did_close_clears_diagnostics() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let uri = parse_uri("file:///tmp/test.rs"); + state.documents.insert( + uri.clone(), + DocumentState::new(PathBuf::from("/tmp/test.rs"), 1, "fn x() {}\n".to_string()), + ); + let params = DidCloseTextDocumentParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + }; + let notification = Notification { + method: DidCloseTextDocument::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + assert!(!state.documents.contains_key(&uri)); + let messages = drain_messages(&client); + // publishDiagnostics with empty list is emitted. + let publish = + find_notification(&messages, PublishDiagnostics::METHOD).expect("publish notif"); + let value = serde_json::to_value(&publish.params).expect("to value"); + let diags = value + .get("diagnostics") + .and_then(|d| d.as_array()) + .expect("diagnostics array"); + assert!(diags.is_empty()); + } + + #[test] + fn handle_notification_did_open_with_non_file_uri_skips_indexing() { + let (_client, server) = Connection::memory(); + let mut state = empty_state(); + let uri = parse_uri("http://example.com/foo.rs"); + let params = DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri, + language_id: "rust".to_string(), + version: 1, + text: "fn x() {}\n".to_string(), + }, + }; + let notification = Notification { + method: DidOpenTextDocument::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + // Document was not inserted because the URI couldn't be parsed as a file path. + assert!(state.documents.is_empty()); + } + + #[test] + fn handle_notification_did_change_for_unknown_document_is_ignored() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let uri = parse_uri("file:///tmp/never-opened.rs"); + let params = DidChangeTextDocumentParams { + text_document: VersionedTextDocumentIdentifier { uri, version: 2 }, + content_changes: vec![TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "new".to_string(), + }], + }; + let notification = Notification { + method: DidChangeTextDocument::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + // No document was inserted, no diagnostics are published. + let messages = drain_messages(&client); + assert!(find_notification(&messages, PublishDiagnostics::METHOD).is_none()); + } + + #[test] + fn handle_notification_did_save_marks_document_saved() { + let (_client, server) = Connection::memory(); + let mut state = empty_state(); + let uri = parse_uri("file:///tmp/save.rs"); + state.documents.insert( + uri.clone(), + DocumentState::new(PathBuf::from("/tmp/save.rs"), 1, "first\n".to_string()), + ); + // Mutate the document so changed_lines is non-empty. + if let Some(doc) = state.documents.get_mut(&uri) { + let change = TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "second\n".to_string(), + }; + doc.apply_changes(&[change]).expect("apply ok"); + assert!(!doc.changed_lines.is_empty()); + } + + let params = DidSaveTextDocumentParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + text: Some("third\n".to_string()), + }; + let notification = Notification { + method: DidSaveTextDocument::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + let doc = state.documents.get(&uri).expect("doc still present"); + assert_eq!(doc.text, "third\n"); + assert_eq!(doc.baseline_text, "third\n"); + assert!(doc.changed_lines.is_empty()); + } + + #[test] + fn handle_notification_did_change_configuration_triggers_reload_message() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let params = DidChangeConfigurationParams { + settings: json!({}), + }; + let notification = Notification { + method: DidChangeConfiguration::METHOD.to_string(), + params: serde_json::to_value(params).expect("serialize"), + }; + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + assert!(!exit); + let messages = drain_messages(&client); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); + // With no config_path set, reload succeeds and reports the rule count. + assert!(notif.params.to_string().contains("config reloaded")); + } + + #[test] + fn refresh_document_diagnostics_short_circuits_when_relative_path_empty() { + // With workspace_root set and the document's path equal to workspace_root, + // the stripped relative path is empty, which triggers the early-return + // branch that publishes an empty diagnostics list. + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let workspace = PathBuf::from("/tmp/empty-relative"); + state.workspace_root = Some(workspace.clone()); + let uri = parse_uri("file:///tmp/empty-relative"); + state + .documents + .insert(uri.clone(), DocumentState::new(workspace, 1, String::new())); + refresh_document_diagnostics(&server, &mut state, &uri).expect("refresh ok"); + let messages = drain_messages(&client); + let publish = + find_notification(&messages, PublishDiagnostics::METHOD).expect("publish notif"); + let value = serde_json::to_value(&publish.params).expect("to value"); + let diags = value + .get("diagnostics") + .and_then(|d| d.as_array()) + .expect("diagnostics array"); + assert!(diags.is_empty()); + } + + #[test] + fn refresh_document_diagnostics_no_op_for_missing_document() { + let (client, server) = Connection::memory(); + let mut state = empty_state(); + let uri = parse_uri("file:///tmp/missing.rs"); + refresh_document_diagnostics(&server, &mut state, &uri).expect("refresh ok"); + let messages = drain_messages(&client); + assert!(messages.is_empty()); + } + + // --------------------------------------------------------------------- + // send_*_response / show_message helpers + // --------------------------------------------------------------------- + + #[test] + fn send_error_response_constructs_proper_response_object() { + let (client, server) = Connection::memory(); + send_error_response( + &server, + RequestId::from(99), + INVALID_PARAMS, + "bad".to_string(), + ) + .expect("send ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response"); + let error = response.error.as_ref().expect("has error"); + assert_eq!(error.code, INVALID_PARAMS); + assert_eq!(error.message, "bad"); + assert!(response.result.is_none()); + } + + #[test] + fn send_ok_response_constructs_proper_response_object() { + let (client, server) = Connection::memory(); + send_ok_response(&server, RequestId::from(101), json!({ "ok": true })).expect("send ok"); + let messages = drain_messages(&client); + let response = find_response(&messages).expect("response"); + assert!(response.error.is_none()); + let result = response.result.as_ref().expect("has result"); + assert_eq!(result.get("ok").and_then(|v| v.as_bool()), Some(true)); + } + + #[test] + fn show_message_emits_show_message_notification() { + let (client, server) = Connection::memory(); + show_message(&server, MessageType::INFO, "hello").expect("send ok"); + let messages = drain_messages(&client); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); + assert!(notif.params.to_string().contains("hello")); + } } From 4bbe3db8bb5efe5d1d3badb30b5eaf224585dcda Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 08:17:09 +0000 Subject: [PATCH 2/3] test(diffguard-lsp): suppress rust.no_unwrap on test .expect()/.unwrap() calls Add inline 'diffguard: ignore rust.no_unwrap' suppressions to the new test module additions so the diffguard self-lint does not fire on .expect()/.unwrap() inside #[cfg(test)] code (the rule's exclude_paths already covers **/tests/** but not src/ files containing inline test modules). Refs #1609. --- crates/diffguard-lsp/src/server.rs | 164 ++++++++++++++--------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/crates/diffguard-lsp/src/server.rs b/crates/diffguard-lsp/src/server.rs index 6ad89857..0366aab4 100644 --- a/crates/diffguard-lsp/src/server.rs +++ b/crates/diffguard-lsp/src/server.rs @@ -1002,7 +1002,7 @@ mod tests { } fn parse_uri(uri_str: &str) -> Uri { - uri_str.parse().expect("uri parse") + uri_str.parse().expect("uri parse") // diffguard: ignore rust.no_unwrap } fn drain_messages(connection: &Connection) -> Vec { @@ -1053,11 +1053,11 @@ mod tests { #[test] fn initialize_payload_contains_server_name() { - let value = initialize_payload().expect("payload"); + let value = initialize_payload().expect("payload"); // diffguard: ignore rust.no_unwrap let info = value .get("serverInfo") .and_then(|v| v.as_object()) - .expect("server info"); + .expect("server info"); // diffguard: ignore rust.no_unwrap assert_eq!( info.get("name").and_then(|v| v.as_str()), Some("diffguard-lsp") @@ -1160,7 +1160,7 @@ mod tests { #[test] fn uri_to_file_path_parses_valid_file_uri() { let uri: Uri = parse_uri("file:///tmp/some/path.rs"); - let parsed = uri_to_file_path(&uri).expect("file path"); + let parsed = uri_to_file_path(&uri).expect("file path"); // diffguard: ignore rust.no_unwrap assert_eq!(parsed, PathBuf::from("/tmp/some/path.rs")); } @@ -1249,7 +1249,7 @@ mod tests { #[test] fn document_state_apply_changes_handles_empty_change_list() { let mut doc = DocumentState::new(PathBuf::from("a.rs"), 1, "hello\n".to_string()); - doc.apply_changes(&[]).expect("apply ok"); + doc.apply_changes(&[]).expect("apply ok"); // diffguard: ignore rust.no_unwrap assert_eq!(doc.text, "hello\n"); assert!(doc.changed_lines.is_empty()); } @@ -1262,7 +1262,7 @@ mod tests { range_length: None, text: "after\n".to_string(), }; - doc.apply_changes(&[change]).expect("apply ok"); + doc.apply_changes(&[change]).expect("apply ok"); // diffguard: ignore rust.no_unwrap assert_eq!(doc.text, "after\n"); assert!(!doc.changed_lines.is_empty()); } @@ -1282,7 +1282,7 @@ mod tests { text: "final\n".to_string(), }, ]; - doc.apply_changes(&changes).expect("apply ok"); + doc.apply_changes(&changes).expect("apply ok"); // diffguard: ignore rust.no_unwrap assert_eq!(doc.text, "final\n"); } @@ -1294,7 +1294,7 @@ mod tests { range_length: None, text: "after\n".to_string(), }; - doc.apply_changes(&[change]).expect("apply ok"); + doc.apply_changes(&[change]).expect("apply ok"); // diffguard: ignore rust.no_unwrap assert!(!doc.changed_lines.is_empty()); doc.mark_saved(Some("saved\n".to_string())); @@ -1311,7 +1311,7 @@ mod tests { range_length: None, text: "after\n".to_string(), }; - doc.apply_changes(&[change]).expect("apply ok"); + doc.apply_changes(&[change]).expect("apply ok"); // diffguard: ignore rust.no_unwrap doc.mark_saved(None); assert_eq!(doc.text, "after\n"); @@ -1413,7 +1413,7 @@ mod tests { // Falls back to built-in rules when load fails. assert!(!state.config.rule.is_empty()); assert!(warning.is_some()); - let warning_text = warning.expect("warning string"); + let warning_text = warning.expect("warning string"); // diffguard: ignore rust.no_unwrap assert!(warning_text.contains("failed to load config")); } @@ -1455,7 +1455,7 @@ mod tests { // we set no_default_rules=true so we get an empty/built-in-less config. state.no_default_rules = true; state.config_path = None; - let result = reload_config(&mut state).expect("reload ok"); + let result = reload_config(&mut state).expect("reload ok"); // diffguard: ignore rust.no_unwrap assert!(result.contains("config reloaded")); } @@ -1484,10 +1484,10 @@ mod tests { method: "totally/unsupported".to_string(), params: json!({}), }; - handle_request(&server, &mut state, request).expect("handle ok"); + handle_request(&server, &mut state, request).expect("handle ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response sent"); - let error = response.error.as_ref().expect("error response"); + let response = find_response(&messages).expect("response sent"); // diffguard: ignore rust.no_unwrap + let error = response.error.as_ref().expect("error response"); // diffguard: ignore rust.no_unwrap assert_eq!(error.code, METHOD_NOT_FOUND); assert!(error.message.contains("totally/unsupported")); } @@ -1501,10 +1501,10 @@ mod tests { method: CodeActionRequest::METHOD.to_string(), params: json!({ "totally": "wrong" }), }; - handle_request(&server, &mut state, request).expect("handle ok"); + handle_request(&server, &mut state, request).expect("handle ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response sent"); - let error = response.error.as_ref().expect("error response"); + let response = find_response(&messages).expect("response sent"); // diffguard: ignore rust.no_unwrap + let error = response.error.as_ref().expect("error response"); // diffguard: ignore rust.no_unwrap assert_eq!(error.code, INVALID_PARAMS); assert!(error.message.contains("invalid CodeActionParams")); } @@ -1518,10 +1518,10 @@ mod tests { method: ExecuteCommand::METHOD.to_string(), params: json!({ "nope": true }), }; - handle_request(&server, &mut state, request).expect("handle ok"); + handle_request(&server, &mut state, request).expect("handle ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response sent"); - let error = response.error.as_ref().expect("error response"); + let response = find_response(&messages).expect("response sent"); // diffguard: ignore rust.no_unwrap + let error = response.error.as_ref().expect("error response"); // diffguard: ignore rust.no_unwrap assert_eq!(error.code, INVALID_PARAMS); assert!(error.message.contains("invalid ExecuteCommandParams")); } @@ -1538,12 +1538,12 @@ mod tests { let request = Request { id: RequestId::from(3), method: ExecuteCommand::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - handle_request(&server, &mut state, request).expect("handle ok"); + handle_request(&server, &mut state, request).expect("handle ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response sent"); - let error = response.error.as_ref().expect("error response"); + let response = find_response(&messages).expect("response sent"); // diffguard: ignore rust.no_unwrap + let error = response.error.as_ref().expect("error response"); // diffguard: ignore rust.no_unwrap assert_eq!(error.code, INVALID_PARAMS); assert!(error.message.contains("missing rule ID")); } @@ -1560,12 +1560,12 @@ mod tests { let request = Request { id: RequestId::from(4), method: ExecuteCommand::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - handle_request(&server, &mut state, request).expect("handle ok"); + handle_request(&server, &mut state, request).expect("handle ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response sent"); - let error = response.error.as_ref().expect("error response"); + let response = find_response(&messages).expect("response sent"); // diffguard: ignore rust.no_unwrap + let error = response.error.as_ref().expect("error response"); // diffguard: ignore rust.no_unwrap assert_eq!(error.code, INVALID_PARAMS); assert!(error.message.contains("missing URL")); } @@ -1582,12 +1582,12 @@ mod tests { let request = Request { id: RequestId::from(5), method: ExecuteCommand::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - handle_request(&server, &mut state, request).expect("handle ok"); + handle_request(&server, &mut state, request).expect("handle ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response sent"); - let error = response.error.as_ref().expect("error response"); + let response = find_response(&messages).expect("response sent"); // diffguard: ignore rust.no_unwrap + let error = response.error.as_ref().expect("error response"); // diffguard: ignore rust.no_unwrap assert_eq!(error.code, INVALID_PARAMS); assert!(error.message.contains("unsupported command")); assert!(error.message.contains("diffguard.unknown")); @@ -1606,12 +1606,12 @@ mod tests { let request = Request { id: RequestId::from(6), method: ExecuteCommand::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - handle_request(&server, &mut state, request).expect("handle ok"); + handle_request(&server, &mut state, request).expect("handle ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response sent"); - let result = response.result.as_ref().expect("result payload"); + let response = find_response(&messages).expect("response sent"); // diffguard: ignore rust.no_unwrap + let result = response.result.as_ref().expect("result payload"); // diffguard: ignore rust.no_unwrap assert_eq!(result.get("found").and_then(|v| v.as_bool()), Some(true)); assert_eq!( result.get("ruleId").and_then(|v| v.as_str()), @@ -1633,17 +1633,17 @@ mod tests { let request = Request { id: RequestId::from(8), method: ExecuteCommand::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - handle_request(&server, &mut state, request).expect("handle ok"); + handle_request(&server, &mut state, request).expect("handle ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response sent"); - let result = response.result.as_ref().expect("result payload"); + let response = find_response(&messages).expect("response sent"); // diffguard: ignore rust.no_unwrap + let result = response.result.as_ref().expect("result payload"); // diffguard: ignore rust.no_unwrap assert_eq!( result.get("url").and_then(|v| v.as_str()), Some("https://example.com/docs") ); - let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); // diffguard: ignore rust.no_unwrap let body = notif.params.to_string(); assert!(body.contains("diffguard documentation")); } @@ -1656,10 +1656,10 @@ mod tests { method: DidOpenTextDocument::METHOD.to_string(), params: json!({ "garbage": true }), }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); let messages = drain_messages(&client); - let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); // diffguard: ignore rust.no_unwrap assert!(notif.params.to_string().contains("invalid didOpen")); } @@ -1671,10 +1671,10 @@ mod tests { method: DidChangeTextDocument::METHOD.to_string(), params: json!({ "garbage": true }), }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); let messages = drain_messages(&client); - let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); // diffguard: ignore rust.no_unwrap assert!(notif.params.to_string().contains("invalid didChange")); } @@ -1686,10 +1686,10 @@ mod tests { method: DidSaveTextDocument::METHOD.to_string(), params: json!({ "garbage": true }), }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); let messages = drain_messages(&client); - let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); // diffguard: ignore rust.no_unwrap assert!(notif.params.to_string().contains("invalid didSave")); } @@ -1701,10 +1701,10 @@ mod tests { method: DidCloseTextDocument::METHOD.to_string(), params: json!({ "garbage": true }), }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); let messages = drain_messages(&client); - let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); // diffguard: ignore rust.no_unwrap assert!(notif.params.to_string().contains("invalid didClose")); } @@ -1716,10 +1716,10 @@ mod tests { method: DidChangeConfiguration::METHOD.to_string(), params: json!("not-an-object"), }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); let messages = drain_messages(&client); - let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("warning"); // diffguard: ignore rust.no_unwrap assert!( notif .params @@ -1736,7 +1736,7 @@ mod tests { method: Exit::METHOD.to_string(), params: json!(null), }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(exit); } @@ -1748,7 +1748,7 @@ mod tests { method: "made/up/method".to_string(), params: json!({}), }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); let messages = drain_messages(&client); assert!(messages.is_empty()); @@ -1768,20 +1768,20 @@ mod tests { }; let notification = Notification { method: DidCloseTextDocument::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); assert!(!state.documents.contains_key(&uri)); let messages = drain_messages(&client); // publishDiagnostics with empty list is emitted. let publish = - find_notification(&messages, PublishDiagnostics::METHOD).expect("publish notif"); - let value = serde_json::to_value(&publish.params).expect("to value"); + find_notification(&messages, PublishDiagnostics::METHOD).expect("publish notif"); // diffguard: ignore rust.no_unwrap + let value = serde_json::to_value(&publish.params).expect("to value"); // diffguard: ignore rust.no_unwrap let diags = value .get("diagnostics") .and_then(|d| d.as_array()) - .expect("diagnostics array"); + .expect("diagnostics array"); // diffguard: ignore rust.no_unwrap assert!(diags.is_empty()); } @@ -1800,9 +1800,9 @@ mod tests { }; let notification = Notification { method: DidOpenTextDocument::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); // Document was not inserted because the URI couldn't be parsed as a file path. assert!(state.documents.is_empty()); @@ -1823,9 +1823,9 @@ mod tests { }; let notification = Notification { method: DidChangeTextDocument::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); // No document was inserted, no diagnostics are published. let messages = drain_messages(&client); @@ -1848,7 +1848,7 @@ mod tests { range_length: None, text: "second\n".to_string(), }; - doc.apply_changes(&[change]).expect("apply ok"); + doc.apply_changes(&[change]).expect("apply ok"); // diffguard: ignore rust.no_unwrap assert!(!doc.changed_lines.is_empty()); } @@ -1858,11 +1858,11 @@ mod tests { }; let notification = Notification { method: DidSaveTextDocument::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); - let doc = state.documents.get(&uri).expect("doc still present"); + let doc = state.documents.get(&uri).expect("doc still present"); // diffguard: ignore rust.no_unwrap assert_eq!(doc.text, "third\n"); assert_eq!(doc.baseline_text, "third\n"); assert!(doc.changed_lines.is_empty()); @@ -1877,12 +1877,12 @@ mod tests { }; let notification = Notification { method: DidChangeConfiguration::METHOD.to_string(), - params: serde_json::to_value(params).expect("serialize"), + params: serde_json::to_value(params).expect("serialize"), // diffguard: ignore rust.no_unwrap }; - let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); + let exit = handle_notification(&server, &mut state, notification).expect("handle ok"); // diffguard: ignore rust.no_unwrap assert!(!exit); let messages = drain_messages(&client); - let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); // diffguard: ignore rust.no_unwrap // With no config_path set, reload succeeds and reports the rule count. assert!(notif.params.to_string().contains("config reloaded")); } @@ -1900,15 +1900,15 @@ mod tests { state .documents .insert(uri.clone(), DocumentState::new(workspace, 1, String::new())); - refresh_document_diagnostics(&server, &mut state, &uri).expect("refresh ok"); + refresh_document_diagnostics(&server, &mut state, &uri).expect("refresh ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); let publish = - find_notification(&messages, PublishDiagnostics::METHOD).expect("publish notif"); - let value = serde_json::to_value(&publish.params).expect("to value"); + find_notification(&messages, PublishDiagnostics::METHOD).expect("publish notif"); // diffguard: ignore rust.no_unwrap + let value = serde_json::to_value(&publish.params).expect("to value"); // diffguard: ignore rust.no_unwrap let diags = value .get("diagnostics") .and_then(|d| d.as_array()) - .expect("diagnostics array"); + .expect("diagnostics array"); // diffguard: ignore rust.no_unwrap assert!(diags.is_empty()); } @@ -1917,7 +1917,7 @@ mod tests { let (client, server) = Connection::memory(); let mut state = empty_state(); let uri = parse_uri("file:///tmp/missing.rs"); - refresh_document_diagnostics(&server, &mut state, &uri).expect("refresh ok"); + refresh_document_diagnostics(&server, &mut state, &uri).expect("refresh ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); assert!(messages.is_empty()); } @@ -1935,10 +1935,10 @@ mod tests { INVALID_PARAMS, "bad".to_string(), ) - .expect("send ok"); + .expect("send ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response"); - let error = response.error.as_ref().expect("has error"); + let response = find_response(&messages).expect("response"); // diffguard: ignore rust.no_unwrap + let error = response.error.as_ref().expect("has error"); // diffguard: ignore rust.no_unwrap assert_eq!(error.code, INVALID_PARAMS); assert_eq!(error.message, "bad"); assert!(response.result.is_none()); @@ -1947,20 +1947,20 @@ mod tests { #[test] fn send_ok_response_constructs_proper_response_object() { let (client, server) = Connection::memory(); - send_ok_response(&server, RequestId::from(101), json!({ "ok": true })).expect("send ok"); + send_ok_response(&server, RequestId::from(101), json!({ "ok": true })).expect("send ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let response = find_response(&messages).expect("response"); + let response = find_response(&messages).expect("response"); // diffguard: ignore rust.no_unwrap assert!(response.error.is_none()); - let result = response.result.as_ref().expect("has result"); + let result = response.result.as_ref().expect("has result"); // diffguard: ignore rust.no_unwrap assert_eq!(result.get("ok").and_then(|v| v.as_bool()), Some(true)); } #[test] fn show_message_emits_show_message_notification() { let (client, server) = Connection::memory(); - show_message(&server, MessageType::INFO, "hello").expect("send ok"); + show_message(&server, MessageType::INFO, "hello").expect("send ok"); // diffguard: ignore rust.no_unwrap let messages = drain_messages(&client); - let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); + let notif = find_notification(&messages, ShowMessage::METHOD).expect("show message"); // diffguard: ignore rust.no_unwrap assert!(notif.params.to_string().contains("hello")); } } From 97c1c7f9f23bc7b9a4e25d9a53168d8d74d7985a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 08:20:46 +0000 Subject: [PATCH 3/3] test(diffguard-lsp): use https:// in test fixture URIs Switch http://example.com/... to https://example.com/... in the three non-file URI test fixtures to avoid tripping the diffguard self-lint security.http_url. Tests still exercise the non-file URI branches (uri_to_file_path / is_config_uri / didOpen-skip). Refs #1609. --- crates/diffguard-lsp/src/server.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/diffguard-lsp/src/server.rs b/crates/diffguard-lsp/src/server.rs index 0366aab4..fc7763bf 100644 --- a/crates/diffguard-lsp/src/server.rs +++ b/crates/diffguard-lsp/src/server.rs @@ -1153,7 +1153,7 @@ mod tests { #[test] fn uri_to_file_path_returns_none_for_malformed_uri() { // A scheme without authority is not a valid file URL. - let uri: Uri = parse_uri("http://example.com/foo.rs"); + let uri: Uri = parse_uri("https://example.com/foo.rs"); assert!(uri_to_file_path(&uri).is_none()); } @@ -1432,7 +1432,7 @@ mod tests { fn is_config_uri_returns_false_when_uri_cannot_be_parsed() { let mut state = empty_state(); state.config_path = Some(PathBuf::from("/tmp/diffguard.toml")); - let uri = parse_uri("http://example.com/foo.toml"); + let uri = parse_uri("https://example.com/foo.toml"); assert!(!is_config_uri(&state, &uri)); } @@ -1789,7 +1789,7 @@ mod tests { fn handle_notification_did_open_with_non_file_uri_skips_indexing() { let (_client, server) = Connection::memory(); let mut state = empty_state(); - let uri = parse_uri("http://example.com/foo.rs"); + let uri = parse_uri("https://example.com/foo.rs"); let params = DidOpenTextDocumentParams { text_document: TextDocumentItem { uri,