source_precedence_test.rs
1 mod common; 2 use common::TestFixture; 3 use ecolog_lsp::server::handlers::{ 4 compute_diagnostics, handle_completion, handle_execute_command, handle_hover, 5 }; 6 use serde_json::json; 7 use tower_lsp::lsp_types::{ 8 CompletionContext, CompletionParams, CompletionTriggerKind, ExecuteCommandParams, HoverParams, 9 Position, TextDocumentIdentifier, TextDocumentPositionParams, 10 }; 11 12 async fn set_shell_var(fixture: &TestFixture, name: &str, value: &str) { 13 std::env::set_var(name, value); 14 fixture 15 .state 16 .core 17 .refresh(abundantis::RefreshOptions::reset_all()) 18 .await 19 .expect("Refresh failed"); 20 } 21 22 async fn remove_shell_var(fixture: &TestFixture, name: &str) { 23 std::env::remove_var(name); 24 fixture 25 .state 26 .core 27 .refresh(abundantis::RefreshOptions::reset_all()) 28 .await 29 .expect("Refresh failed"); 30 } 31 32 async fn set_precedence(fixture: &TestFixture, sources: Vec<&str>) -> Option<serde_json::Value> { 33 let args: Vec<serde_json::Value> = sources.iter().map(|s| json!(s)).collect(); 34 let params = ExecuteCommandParams { 35 command: "ecolog.source.setPrecedence".to_string(), 36 arguments: args, 37 work_done_progress_params: Default::default(), 38 }; 39 handle_execute_command(params, &fixture.state).await 40 } 41 42 async fn get_hover( 43 fixture: &TestFixture, 44 uri: &tower_lsp::lsp_types::Url, 45 line: u32, 46 col: u32, 47 ) -> Option<tower_lsp::lsp_types::Hover> { 48 handle_hover( 49 HoverParams { 50 text_document_position_params: TextDocumentPositionParams { 51 text_document: TextDocumentIdentifier { uri: uri.clone() }, 52 position: Position::new(line, col), 53 }, 54 work_done_progress_params: Default::default(), 55 }, 56 &fixture.state, 57 ) 58 .await 59 } 60 61 async fn get_completions( 62 fixture: &TestFixture, 63 uri: &tower_lsp::lsp_types::Url, 64 line: u32, 65 col: u32, 66 ) -> Option<Vec<tower_lsp::lsp_types::CompletionItem>> { 67 handle_completion( 68 CompletionParams { 69 text_document_position: TextDocumentPositionParams { 70 text_document: TextDocumentIdentifier { uri: uri.clone() }, 71 position: Position::new(line, col), 72 }, 73 work_done_progress_params: Default::default(), 74 partial_result_params: Default::default(), 75 context: Some(CompletionContext { 76 trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, 77 trigger_character: Some(".".to_string()), 78 }), 79 }, 80 &fixture.state, 81 ) 82 .await 83 } 84 85 #[tokio::test] 86 async fn test_set_precedence_command() { 87 let fixture = TestFixture::new().await; 88 89 let result = set_precedence(&fixture, vec!["File"]).await; 90 assert!(result.is_some()); 91 let json = result.unwrap(); 92 assert!(json.get("success").unwrap().as_bool().unwrap()); 93 94 let result = set_precedence(&fixture, vec!["Shell"]).await; 95 assert!(result.is_some()); 96 let json = result.unwrap(); 97 assert!(json.get("success").unwrap().as_bool().unwrap()); 98 99 let result = set_precedence(&fixture, vec!["Shell", "File"]).await; 100 assert!(result.is_some()); 101 let json = result.unwrap(); 102 assert!(json.get("success").unwrap().as_bool().unwrap()); 103 } 104 105 #[tokio::test] 106 async fn test_disabled_shell_no_hover() { 107 let fixture = TestFixture::new().await; 108 109 set_shell_var(&fixture, "SHELL_ONLY_TEST_VAR", "shell_test_value").await; 110 111 let uri = fixture.create_file("test.js", "process.env.SHELL_ONLY_TEST_VAR"); 112 fixture 113 .state 114 .document_manager 115 .open( 116 uri.clone(), 117 "javascript".to_string(), 118 "process.env.SHELL_ONLY_TEST_VAR".to_string(), 119 0, 120 ) 121 .await; 122 123 let hover_before = get_hover(&fixture, &uri, 0, 20).await; 124 assert!( 125 hover_before.is_some(), 126 "Hover should work before disabling shell" 127 ); 128 assert!( 129 format!("{:?}", hover_before.unwrap()).contains("shell_test_value"), 130 "Hover should show shell value" 131 ); 132 133 set_precedence(&fixture, vec!["File"]).await; 134 135 let hover_after = get_hover(&fixture, &uri, 0, 20).await; 136 assert!( 137 hover_after.is_none(), 138 "Hover should NOT work after disabling shell for shell-only variable" 139 ); 140 141 remove_shell_var(&fixture, "SHELL_ONLY_TEST_VAR").await; 142 } 143 144 #[tokio::test] 145 async fn test_disabled_shell_no_completion() { 146 let fixture = TestFixture::new().await; 147 148 set_shell_var(&fixture, "SHELL_COMPLETION_VAR", "completion_value").await; 149 150 let uri = fixture.create_file("test.js", "process.env."); 151 fixture 152 .state 153 .document_manager 154 .open( 155 uri.clone(), 156 "javascript".to_string(), 157 "process.env.".to_string(), 158 0, 159 ) 160 .await; 161 162 let completions_before = get_completions(&fixture, &uri, 0, 12).await; 163 assert!(completions_before.is_some()); 164 let comp_str_before = format!("{:?}", completions_before.unwrap()); 165 assert!( 166 comp_str_before.contains("SHELL_COMPLETION_VAR"), 167 "Completion should include shell var before disabling" 168 ); 169 170 set_precedence(&fixture, vec!["File"]).await; 171 172 let completions_after = get_completions(&fixture, &uri, 0, 12).await; 173 if let Some(comp) = completions_after { 174 let comp_str_after = format!("{:?}", comp); 175 assert!( 176 !comp_str_after.contains("SHELL_COMPLETION_VAR"), 177 "Completion should NOT include shell var after disabling: {}", 178 comp_str_after 179 ); 180 } 181 182 remove_shell_var(&fixture, "SHELL_COMPLETION_VAR").await; 183 } 184 185 #[tokio::test] 186 async fn test_disabled_shell_undefined_diagnostic() { 187 let fixture = TestFixture::new().await; 188 189 set_shell_var(&fixture, "SHELL_DIAG_VAR", "diag_value").await; 190 191 let uri = fixture.create_file("test.js", "const x = process.env.SHELL_DIAG_VAR;"); 192 fixture 193 .state 194 .document_manager 195 .open( 196 uri.clone(), 197 "javascript".to_string(), 198 "const x = process.env.SHELL_DIAG_VAR;".to_string(), 199 0, 200 ) 201 .await; 202 203 let diags_before = compute_diagnostics(&uri, &fixture.state).await; 204 let undefined_before = diags_before 205 .iter() 206 .any(|d| d.message.contains("SHELL_DIAG_VAR") && d.message.contains("not defined")); 207 assert!( 208 !undefined_before, 209 "Should NOT have undefined diagnostic before disabling shell" 210 ); 211 212 set_precedence(&fixture, vec!["File"]).await; 213 214 let diags_after = compute_diagnostics(&uri, &fixture.state).await; 215 let undefined_after = diags_after 216 .iter() 217 .any(|d| d.message.contains("SHELL_DIAG_VAR") && d.message.contains("not defined")); 218 assert!( 219 undefined_after, 220 "Should have undefined diagnostic after disabling shell" 221 ); 222 223 remove_shell_var(&fixture, "SHELL_DIAG_VAR").await; 224 } 225 226 #[tokio::test] 227 async fn test_disabled_file_no_hover() { 228 let fixture = TestFixture::new().await; 229 230 let uri = fixture.create_file("test.js", "process.env.DB_URL"); 231 fixture 232 .state 233 .document_manager 234 .open( 235 uri.clone(), 236 "javascript".to_string(), 237 "process.env.DB_URL".to_string(), 238 0, 239 ) 240 .await; 241 242 let hover_before = get_hover(&fixture, &uri, 0, 14).await; 243 assert!( 244 hover_before.is_some(), 245 "Hover should work before disabling file" 246 ); 247 assert!( 248 format!("{:?}", hover_before.unwrap()).contains("postgres://"), 249 "Hover should show file value" 250 ); 251 252 set_precedence(&fixture, vec!["Shell"]).await; 253 254 let hover_after = get_hover(&fixture, &uri, 0, 14).await; 255 assert!( 256 hover_after.is_none(), 257 "Hover should NOT work after disabling file for file-only variable" 258 ); 259 } 260 261 #[tokio::test] 262 async fn test_enable_restores_functionality() { 263 let fixture = TestFixture::new().await; 264 265 set_shell_var(&fixture, "RESTORE_TEST_VAR", "restore_value").await; 266 267 let uri = fixture.create_file("test.js", "process.env.RESTORE_TEST_VAR"); 268 fixture 269 .state 270 .document_manager 271 .open( 272 uri.clone(), 273 "javascript".to_string(), 274 "process.env.RESTORE_TEST_VAR".to_string(), 275 0, 276 ) 277 .await; 278 279 let hover1 = get_hover(&fixture, &uri, 0, 20).await; 280 assert!(hover1.is_some(), "Hover should work initially"); 281 282 set_precedence(&fixture, vec!["File"]).await; 283 284 let hover2 = get_hover(&fixture, &uri, 0, 20).await; 285 assert!( 286 hover2.is_none(), 287 "Hover should NOT work after disabling shell" 288 ); 289 290 set_precedence(&fixture, vec!["Shell", "File"]).await; 291 292 let hover3 = get_hover(&fixture, &uri, 0, 20).await; 293 assert!( 294 hover3.is_some(), 295 "Hover should work after re-enabling shell" 296 ); 297 298 remove_shell_var(&fixture, "RESTORE_TEST_VAR").await; 299 } 300 301 #[tokio::test] 302 async fn test_empty_precedence_disables_all() { 303 let fixture = TestFixture::new().await; 304 305 set_shell_var(&fixture, "EMPTY_PREC_VAR", "empty_prec_value").await; 306 307 let uri = fixture.create_file("test.js", "process.env.EMPTY_PREC_VAR"); 308 fixture 309 .state 310 .document_manager 311 .open( 312 uri.clone(), 313 "javascript".to_string(), 314 "process.env.EMPTY_PREC_VAR".to_string(), 315 0, 316 ) 317 .await; 318 319 let params = ExecuteCommandParams { 320 command: "ecolog.source.setPrecedence".to_string(), 321 arguments: vec![], 322 work_done_progress_params: Default::default(), 323 }; 324 handle_execute_command(params, &fixture.state).await; 325 326 let hover = get_hover(&fixture, &uri, 0, 20).await; 327 assert!( 328 hover.is_none(), 329 "Hover should NOT work with empty precedence (no sources enabled)" 330 ); 331 332 remove_shell_var(&fixture, "EMPTY_PREC_VAR").await; 333 } 334 335 #[tokio::test] 336 async fn test_precedence_persists_after_refresh() { 337 let fixture = TestFixture::new().await; 338 339 set_shell_var(&fixture, "PERSIST_TEST_VAR", "persist_value").await; 340 341 let uri = fixture.create_file("test.js", "process.env.PERSIST_TEST_VAR"); 342 fixture 343 .state 344 .document_manager 345 .open( 346 uri.clone(), 347 "javascript".to_string(), 348 "process.env.PERSIST_TEST_VAR".to_string(), 349 0, 350 ) 351 .await; 352 353 set_precedence(&fixture, vec!["File"]).await; 354 355 let hover1 = get_hover(&fixture, &uri, 0, 20).await; 356 assert!( 357 hover1.is_none(), 358 "Hover should NOT work after disabling shell" 359 ); 360 361 fixture 362 .state 363 .core 364 .refresh(abundantis::RefreshOptions::reset_all()) 365 .await 366 .expect("Refresh failed"); 367 368 let hover2 = get_hover(&fixture, &uri, 0, 20).await; 369 assert!( 370 hover2.is_none(), 371 "Hover should still NOT work after refresh (precedence should persist)" 372 ); 373 374 remove_shell_var(&fixture, "PERSIST_TEST_VAR").await; 375 } 376 377 #[tokio::test] 378 async fn test_file_works_when_shell_disabled() { 379 let fixture = TestFixture::new().await; 380 381 let uri = fixture.create_file("test.js", "process.env.DB_URL"); 382 fixture 383 .state 384 .document_manager 385 .open( 386 uri.clone(), 387 "javascript".to_string(), 388 "process.env.DB_URL".to_string(), 389 0, 390 ) 391 .await; 392 393 set_precedence(&fixture, vec!["File"]).await; 394 395 let hover = get_hover(&fixture, &uri, 0, 14).await; 396 assert!( 397 hover.is_some(), 398 "File variable should still work when shell is disabled" 399 ); 400 assert!( 401 format!("{:?}", hover.unwrap()).contains("postgres://"), 402 "Hover should show file value" 403 ); 404 }