use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::backend::Backend;
use crate::backend::helpers::{create_error_diagnostic, normalize_path};
use crate::backend::revalidation::{
MAX_CONFIG_REVALIDATION_CONCURRENCY, config_revalidation_concurrency, for_each_bounded,
};
use crate::diagnostic_mapper::to_lsp_diagnostic;
use crate::position::byte_to_position;
#[test]
fn backend_new_test_creates_valid_instance() {
let backend = Backend::new_test();
let _config = backend.config.load();
assert!(backend.registry.total_validator_count() > 0);
assert_eq!(backend.config_generation.load(Ordering::Relaxed), 0);
assert_eq!(
backend
.project_validation_generation
.load(Ordering::Relaxed),
0
);
}
#[tokio::test]
async fn backend_fields_accessible() {
let backend = Backend::new_test();
assert!(backend.documents.read().await.is_empty());
assert!(backend.project_level_diagnostics.read().await.is_empty());
assert!(backend.project_diagnostics_uris.read().await.is_empty());
assert!(backend.workspace_root.read().await.is_none());
assert!(backend.workspace_root_canonical.read().await.is_none());
}
#[test]
fn helpers_normalize_path_accessible() {
let path = PathBuf::from("/a/b/../c");
let normalized = normalize_path(&path);
assert_eq!(normalized, PathBuf::from("/a/c"));
}
#[test]
fn helpers_normalize_path_root_guard() {
let path = PathBuf::from("/../etc/passwd");
let normalized = normalize_path(&path);
assert_eq!(normalized, PathBuf::from("/etc/passwd"));
}
#[test]
fn helpers_create_error_diagnostic_accessible() {
let diag = create_error_diagnostic("test::code", "something went wrong".to_string());
assert_eq!(
diag.code,
Some(tower_lsp::lsp_types::NumberOrString::String(
"test::code".to_string()
))
);
assert_eq!(diag.message, "something went wrong");
assert_eq!(
diag.severity,
Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR)
);
}
#[test]
fn revalidation_concurrency_accessible() {
assert_eq!(config_revalidation_concurrency(0), 0);
let n = config_revalidation_concurrency(4);
assert!(n >= 1);
assert!(n <= MAX_CONFIG_REVALIDATION_CONCURRENCY);
assert!(config_revalidation_concurrency(usize::MAX) <= MAX_CONFIG_REVALIDATION_CONCURRENCY);
}
#[tokio::test]
async fn revalidation_for_each_bounded_accessible() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let errors = for_each_bounded(0..4usize, 2, move |_i| {
let c = Arc::clone(&counter_clone);
async move {
c.fetch_add(1, Ordering::SeqCst);
}
})
.await;
assert!(errors.is_empty());
assert_eq!(counter.load(Ordering::SeqCst), 4);
}
#[tokio::test]
async fn revalidation_for_each_bounded_panic_path() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let errors = for_each_bounded(0..3usize, 2, move |i| {
let c = Arc::clone(&counter_clone);
async move {
if i == 1 {
panic!("intentional panic in test");
}
c.fetch_add(1, Ordering::SeqCst);
}
})
.await;
assert_eq!(errors.len(), 1);
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn revalidation_should_publish_diagnostics_accessible() {
use tower_lsp::lsp_types::Url;
let backend = Backend::new_test();
let uri = Url::parse("file:///CLAUDE.md").unwrap();
let content = Arc::new("content".to_string());
assert!(
!backend
.should_publish_diagnostics(&uri, None, Some(&content))
.await
);
backend
.documents
.write()
.await
.insert(uri.clone(), Arc::clone(&content));
assert!(
backend
.should_publish_diagnostics(&uri, None, Some(&content))
.await
);
let different_content = Arc::new("content".to_string());
assert!(
!backend
.should_publish_diagnostics(&uri, None, Some(&different_content))
.await
);
assert!(
!backend
.should_publish_diagnostics(&uri, Some(999), None)
.await
);
assert!(backend.should_publish_diagnostics(&uri, None, None).await);
backend.documents.write().await.remove(&uri);
assert!(backend.should_publish_diagnostics(&uri, None, None).await);
}
#[tokio::test]
async fn revalidation_handle_did_change_configuration_accessible() {
use tower_lsp::lsp_types::{DidChangeConfigurationParams, LSPAny};
let backend = Backend::new_test();
let params = DidChangeConfigurationParams {
settings: LSPAny::Null,
};
backend.handle_did_change_configuration(params).await;
}
#[test]
fn backend_is_project_level_trigger_accessible() {
assert!(Backend::is_project_level_trigger(Path::new("CLAUDE.md")));
assert!(Backend::is_project_level_trigger(Path::new(".agnix.toml")));
assert!(!Backend::is_project_level_trigger(Path::new("README.md")));
}
#[tokio::test]
async fn events_handle_did_open_accessible() {
use tower_lsp::lsp_types::{DidOpenTextDocumentParams, TextDocumentItem, Url};
let backend = Backend::new_test();
let uri = Url::parse("file:///CLAUDE.md").unwrap();
let crlf_content = "line1\r\nline2\r\n".to_string();
let params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: crlf_content,
},
};
backend.handle_did_open(params).await;
let stored = backend.documents.read().await.get(&uri).cloned();
assert!(stored.is_some());
assert_eq!(stored.unwrap().as_ref(), "line1\nline2\n");
}
#[tokio::test]
async fn events_handle_did_change_accessible() {
use tower_lsp::lsp_types::{
DidChangeTextDocumentParams, TextDocumentContentChangeEvent, Url,
VersionedTextDocumentIdentifier,
};
let backend = Backend::new_test();
let uri = Url::parse("file:///CLAUDE.md").unwrap();
backend
.documents
.write()
.await
.insert(uri.clone(), Arc::new("old content".to_string()));
let params = DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: uri.clone(),
version: 2,
},
content_changes: vec![TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "new\r\ncontent\r\n".to_string(),
}],
};
backend.handle_did_change(params).await;
let stored = backend.documents.read().await.get(&uri).cloned();
assert!(stored.is_some());
assert_eq!(stored.unwrap().as_ref(), "new\ncontent\n");
}
#[tokio::test]
async fn events_handle_did_save_accessible() {
use tower_lsp::lsp_types::{DidSaveTextDocumentParams, TextDocumentIdentifier, Url};
let backend = Backend::new_test();
let uri = Url::parse("file:///skill.yml").unwrap();
backend.documents.write().await.insert(
uri.clone(),
Arc::new("# Agent\nname: my-agent\n".to_string()),
);
let params = DidSaveTextDocumentParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
text: None,
};
backend.handle_did_save(params).await;
}
#[tokio::test]
async fn events_handle_did_close_accessible() {
use tower_lsp::lsp_types::{DidCloseTextDocumentParams, TextDocumentIdentifier, Url};
let backend = Backend::new_test();
let uri = Url::parse("file:///test.md").unwrap();
backend
.documents
.write()
.await
.insert(uri.clone(), Arc::new("content".to_string()));
let params = DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
};
backend.handle_did_close(params).await;
assert!(backend.documents.read().await.get(&uri).is_none());
}
#[tokio::test]
async fn backend_get_document_content_accessible() {
let backend = Backend::new_test();
let uri = tower_lsp::lsp_types::Url::parse("file:///test.md").unwrap();
assert!(backend.get_document_content(&uri).await.is_none());
let content = Arc::new("hello".to_string());
backend
.documents
.write()
.await
.insert(uri.clone(), Arc::clone(&content));
let retrieved = backend.get_document_content(&uri).await;
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().as_ref(), "hello");
}
#[test]
fn position_module_accessible() {
let pos = byte_to_position("hello\nworld", 6);
assert_eq!(pos.line, 1);
assert_eq!(pos.character, 0);
}
#[test]
fn diagnostic_mapper_accessible() {
let core_diag = agnix_core::Diagnostic {
level: agnix_core::DiagnosticLevel::Warning,
message: "test warning".to_string(),
file: PathBuf::from("test.md"),
line: 3,
column: 5,
rule: "AS-001".to_string(),
suggestion: None,
fixes: vec![],
assumption: None,
metadata: None,
};
let lsp_diag = to_lsp_diagnostic(&core_diag);
assert_eq!(
lsp_diag.severity,
Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING)
);
assert_eq!(lsp_diag.range.start.line, 2);
assert_eq!(lsp_diag.range.start.character, 4);
}