/ tests / source_precedence_test.rs
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  }