#![allow(clippy::unwrap_used, clippy::expect_used, clippy::needless_return)]
use crate::server::types::ReplaceFullParams;
use crate::server::PathfinderServer;
use super::helpers::UnsupportedDiagLawyer;
use pathfinder_common::config::PathfinderConfig;
use pathfinder_common::sandbox::Sandbox;
use pathfinder_common::types::{VersionHash, WorkspaceRoot};
use pathfinder_search::MockScout;
use pathfinder_treesitter::mock::MockSurgeon;
use rmcp::handler::server::wrapper::Parameters;
use std::sync::Arc;
use tempfile::tempdir;
fn make_server_with_lawyer(
ws_dir: &tempfile::TempDir,
mock_surgeon: MockSurgeon,
mock_lawyer: pathfinder_lsp::MockLawyer,
) -> PathfinderServer {
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
PathfinderServer::with_all_engines(
ws,
config,
sandbox,
Arc::new(MockScout::default()),
Arc::new(mock_surgeon),
Arc::new(mock_lawyer),
)
}
fn setup_full_replace_fixture(
ws_dir: &tempfile::TempDir,
filepath: &str,
src: &str,
) -> (MockSurgeon, VersionHash) {
let abs = ws_dir.path().join(filepath);
std::fs::create_dir_all(abs.parent().unwrap()).unwrap();
std::fs::write(&abs, src).unwrap();
let src_bytes = src.as_bytes();
let hash = VersionHash::compute(src_bytes);
let mock_surgeon = MockSurgeon::new();
mock_surgeon
.resolve_full_range_results
.lock()
.unwrap()
.push(Ok((
pathfinder_treesitter::surgeon::FullRange {
start_byte: 0,
end_byte: src_bytes.len(),
indent_column: 0,
},
std::sync::Arc::from(src_bytes),
hash.clone(),
)));
(mock_surgeon, hash)
}
#[tokio::test]
async fn test_run_lsp_validation_no_lsp() {
let ws_dir = tempdir().expect("temp dir");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let mock_lawyer = pathfinder_lsp::MockLawyer::default();
mock_lawyer.set_did_open_error(pathfinder_lsp::LspError::NoLspAvailable);
let server = make_server_with_lawyer(&ws_dir, mock_surgeon, mock_lawyer);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login() { return }\n".to_owned(),
ignore_validation_failures: false,
};
let result = server
.replace_full(Parameters(params))
.await
.expect("should succeed — no_lsp gracefully degrades");
let resp = result.0;
assert!(resp.success);
assert_eq!(resp.validation.status, "skipped");
assert!(resp.validation_skipped);
assert_eq!(resp.validation_skipped_reason.as_deref(), Some("no_lsp"));
}
#[tokio::test]
async fn test_run_lsp_validation_unsupported() {
let ws_dir = tempdir().expect("temp dir");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let mock_lawyer = pathfinder_lsp::MockLawyer::default();
mock_lawyer.set_did_open_error(pathfinder_lsp::LspError::UnsupportedCapability {
capability: "textDocument/diagnostic".to_owned(),
});
let server = make_server_with_lawyer(&ws_dir, mock_surgeon, mock_lawyer);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login() { return }\n".to_owned(),
ignore_validation_failures: false,
};
let result = server
.replace_full(Parameters(params))
.await
.expect("should succeed — unsupported gracefully degrades");
let resp = result.0;
assert_eq!(resp.validation.status, "skipped");
assert!(resp.validation_skipped);
assert_eq!(
resp.validation_skipped_reason.as_deref(),
Some("pull_diagnostics_unsupported")
);
}
#[tokio::test]
async fn test_run_lsp_validation_pre_diag_timeout() {
let ws_dir = tempdir().expect("temp dir");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let mock_lawyer = pathfinder_lsp::MockLawyer::default();
mock_lawyer.push_pull_diagnostics_result(Err("LSP timed out".to_owned()));
let server = make_server_with_lawyer(&ws_dir, mock_surgeon, mock_lawyer);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login() { return }\n".to_owned(),
ignore_validation_failures: false,
};
let result = server
.replace_full(Parameters(params))
.await
.expect("should succeed — pre-diag timeout gracefully degrades");
let resp = result.0;
assert!(resp.success);
assert_eq!(resp.validation.status, "skipped");
assert!(resp.validation_skipped);
assert_eq!(
resp.validation_skipped_reason.as_deref(),
Some("lsp_protocol_error")
);
}
#[tokio::test]
async fn test_run_lsp_validation_pull_diagnostics_unsupported() {
let ws_dir = tempdir().expect("temp dir");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (_mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let (mock_surgeon_2, _) = setup_full_replace_fixture(&ws_dir, filepath, src);
let ws = WorkspaceRoot::new(ws_dir.path()).expect("valid root");
let config = PathfinderConfig::default();
let sandbox = Sandbox::new(ws.path(), &config.sandbox);
let server = PathfinderServer::with_all_engines(
ws,
config,
sandbox,
Arc::new(MockScout::default()),
Arc::new(mock_surgeon_2),
Arc::new(UnsupportedDiagLawyer),
);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login() { return }\n".to_owned(),
ignore_validation_failures: false,
};
let result = server
.replace_full(Parameters(params))
.await
.expect("should succeed — pull_diagnostics_unsupported degrades");
let resp = result.0;
assert_eq!(resp.validation.status, "skipped");
assert_eq!(
resp.validation_skipped_reason.as_deref(),
Some("pull_diagnostics_unsupported")
);
}
#[tokio::test]
async fn test_run_lsp_validation_post_diag_timeout() {
let ws_dir = tempdir().expect("temp dir");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let mock_lawyer = pathfinder_lsp::MockLawyer::default();
mock_lawyer.push_pull_diagnostics_result(Ok(vec![]));
mock_lawyer.push_pull_diagnostics_result(Err("timeout after 10s".to_owned()));
let server = make_server_with_lawyer(&ws_dir, mock_surgeon, mock_lawyer);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login() { return }\n".to_owned(),
ignore_validation_failures: false,
};
let result = server
.replace_full(Parameters(params))
.await
.expect("should succeed — post-diag timeout gracefully degrades");
let resp = result.0;
assert!(resp.success);
assert_eq!(resp.validation.status, "skipped");
assert!(resp.validation_skipped);
assert_eq!(
resp.validation_skipped_reason.as_deref(),
Some("lsp_protocol_error")
);
}
#[tokio::test]
async fn test_run_lsp_validation_blocking() {
use pathfinder_lsp::types::{LspDiagnostic, LspDiagnosticSeverity};
let ws_dir = tempdir().expect("temp dir");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let mock_lawyer = pathfinder_lsp::MockLawyer::default();
mock_lawyer.push_pull_diagnostics_result(Ok(vec![]));
mock_lawyer.push_pull_diagnostics_result(Ok(vec![LspDiagnostic {
severity: LspDiagnosticSeverity::Error,
code: Some("E001".into()),
message: "undefined: Foo".into(),
file: filepath.to_owned(),
start_line: 1,
end_line: 1,
}]));
let server = make_server_with_lawyer(&ws_dir, mock_surgeon, mock_lawyer);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login() { Foo() }\n".to_owned(),
ignore_validation_failures: false,
};
let result = server.replace_full(Parameters(params)).await;
let Err(err) = result else {
panic!("expected VALIDATION_FAILED error when new errors are introduced");
};
let code = err
.data
.as_ref()
.and_then(|d| d.get("error"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(code, "VALIDATION_FAILED", "got: {err:?}");
let introduced = err
.data
.as_ref()
.and_then(|d| d.get("details"))
.and_then(|d| d.get("introduced_errors"))
.and_then(|v| v.as_array())
.map_or(0, Vec::len);
assert_eq!(
introduced, 1,
"one new error should appear in introduced_errors"
);
}
#[tokio::test]
async fn test_run_lsp_validation_workspace_blocking() {
use pathfinder_lsp::types::{LspDiagnostic, LspDiagnosticSeverity};
let ws_dir = tempdir().expect("temp dir");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let mock_lawyer = pathfinder_lsp::MockLawyer::default();
mock_lawyer.push_pull_diagnostics_result(Ok(vec![]));
mock_lawyer.push_pull_workspace_diagnostics_result(Ok(vec![]));
mock_lawyer.push_pull_diagnostics_result(Ok(vec![]));
mock_lawyer.push_pull_workspace_diagnostics_result(Ok(vec![LspDiagnostic {
severity: LspDiagnosticSeverity::Error,
code: Some("E002".into()),
message: "cannot call Login with 1 argument".into(),
file: "src/main.go".to_owned(), start_line: 5,
end_line: 5,
}]));
let server = make_server_with_lawyer(&ws_dir, mock_surgeon, mock_lawyer);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login(a string) { }\n".to_owned(), ignore_validation_failures: false,
};
let result = server.replace_full(Parameters(params)).await;
let Err(err) = result else {
panic!("expected VALIDATION_FAILED error when workspace errors are introduced");
};
let code = err
.data
.as_ref()
.and_then(|d| d.get("error"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert_eq!(code, "VALIDATION_FAILED", "got: {err:?}");
let introduced = err
.data
.as_ref()
.and_then(|d| d.get("details"))
.and_then(|d| d.get("introduced_errors"))
.and_then(|v| v.as_array())
.expect("should have introduced_errors");
assert_eq!(
introduced.len(),
1,
"one workspace error should appear in introduced_errors"
);
let first_err_file = introduced[0].get("file").and_then(|v| v.as_str()).unwrap();
assert_eq!(
first_err_file, "src/main.go",
"error should be in src/main.go"
);
}
#[tokio::test]
async fn test_run_lsp_validation_blocking_ignored() {
use pathfinder_lsp::types::{LspDiagnostic, LspDiagnosticSeverity};
let ws_dir = tempdir().expect("temp dir");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let mock_lawyer = pathfinder_lsp::MockLawyer::default();
mock_lawyer.push_pull_diagnostics_result(Ok(vec![]));
mock_lawyer.push_pull_diagnostics_result(Ok(vec![LspDiagnostic {
severity: LspDiagnosticSeverity::Error,
code: Some("E001".into()),
message: "undefined: Foo".into(),
file: filepath.to_owned(),
start_line: 1,
end_line: 1,
}]));
let server = make_server_with_lawyer(&ws_dir, mock_surgeon, mock_lawyer);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login() { Foo() }\n".to_owned(),
ignore_validation_failures: true,
};
let _result = server
.replace_full(Parameters(params))
.await
.expect("should succeed when ignore_validation_failures=true");
let filepath = "src/auth.go";
let src = "func Login() {}";
let (mock_surgeon, hash) = setup_full_replace_fixture(&ws_dir, filepath, src);
let mock_lawyer = pathfinder_lsp::MockLawyer::default();
let existing_warning = LspDiagnostic {
severity: LspDiagnosticSeverity::Warning,
code: Some("W001".into()),
message: "unused import".into(),
file: filepath.to_owned(),
start_line: 1,
end_line: 1,
};
mock_lawyer.push_pull_diagnostics_result(Ok(vec![existing_warning.clone()]));
mock_lawyer.push_pull_diagnostics_result(Ok(vec![existing_warning]));
let server = make_server_with_lawyer(&ws_dir, mock_surgeon, mock_lawyer);
let params = ReplaceFullParams {
semantic_path: format!("{filepath}::Login"),
base_version: hash.as_str().to_owned(),
new_code: "func Login() { return }\n".to_owned(),
ignore_validation_failures: false,
};
let result = server
.replace_full(Parameters(params))
.await
.expect("should succeed — no new errors");
let resp = result.0;
assert!(resp.success);
assert_eq!(resp.validation.status, "passed");
assert!(!resp.validation_skipped);
assert!(resp.validation.introduced_errors.is_empty());
assert!(resp.validation.resolved_errors.is_empty());
}