#![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, "passed");
assert_eq!(
resp.validation.validation_confidence.as_deref(),
Some("syntax_only")
);
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, "passed");
assert_eq!(
resp.validation.validation_confidence.as_deref(),
Some("syntax_only")
);
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, "passed");
assert_eq!(
resp.validation.validation_confidence.as_deref(),
Some("syntax_only")
);
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, "passed");
assert_eq!(
resp.validation.validation_confidence.as_deref(),
Some("syntax_only")
);
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, "passed");
assert_eq!(
resp.validation.validation_confidence.as_deref(),
Some("syntax_only")
);
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());
}
#[test]
fn test_build_validation_outcome_empty_snapshots_signals_warmup() {
use crate::server::tools::edit::text_edit::build_validation_outcome;
use std::path::Path;
let outcome = build_validation_outcome(
&[], &[], false,
Path::new("src/lib.rs"),
"pull",
);
assert!(
outcome.skipped,
"validation_skipped must be true when both snapshots are empty"
);
assert_eq!(
outcome.skipped_reason.as_deref(),
Some("empty_diagnostics_both_snapshots"),
"skipped_reason must identify the warmup signal"
);
assert_eq!(
outcome.validation.status, "uncertain",
"status must be 'uncertain' when both snapshots are empty (LSP may be warming up)"
);
assert!(
!outcome.should_block,
"should_block must be false — empty snapshots are never a blocker"
);
}
#[test]
fn test_build_validation_outcome_non_empty_pre_does_not_skip() {
use crate::server::tools::edit::text_edit::build_validation_outcome;
use pathfinder_lsp::types::{LspDiagnostic, LspDiagnosticSeverity};
use std::path::Path;
let pre = vec![LspDiagnostic {
severity: LspDiagnosticSeverity::Error,
code: None,
message: "pre-existing error".to_owned(),
file: "src/lib.rs".to_owned(),
start_line: 1,
end_line: 1,
}];
let outcome = build_validation_outcome(&pre, &[], false, Path::new("src/lib.rs"), "pull");
assert!(
!outcome.skipped,
"must not skip when pre_diags is non-empty (diff is meaningful)"
);
assert_eq!(outcome.validation.status, "passed");
assert!(!outcome.should_block);
}
#[tokio::test]
async fn test_push_validation_no_errors() {
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_capability_status(std::collections::HashMap::from([(
"go".to_string(),
pathfinder_lsp::types::LspLanguageStatus {
validation: true,
reason: "gopls connected (push diagnostics)".to_string(),
navigation_ready: Some(true),
indexing_complete: Some(true),
uptime_seconds: Some(30),
diagnostics_strategy: Some("push".to_string()),
supports_definition: Some(true),
supports_call_hierarchy: Some(true),
supports_diagnostics: Some(true),
supports_formatting: Some(false),
server_name: None,
},
)]));
mock_lawyer.push_pull_diagnostics_result(Ok(vec![]));
mock_lawyer.push_pull_diagnostics_result(Ok(vec![]));
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 — push validation with no errors");
let resp = result.0;
assert!(
resp.success,
"edit should succeed when push validation finds no new errors"
);
assert_eq!(
resp.validation.status, "passed",
"TS-1 fallback should upgrade status to 'passed' for valid Go syntax"
);
assert_eq!(
resp.validation.validation_confidence.as_deref(),
Some("syntax_only"),
"confidence must be syntax_only when upgraded by Tree-sitter"
);
assert!(
resp.validation_skipped,
"empty push snapshots should trigger skip"
);
assert_eq!(
resp.validation_skipped_reason.as_deref(),
Some("empty_diagnostics_both_snapshots"),
"skip reason should indicate empty snapshots"
);
}
#[tokio::test]
async fn test_push_validation_clean_pass() {
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.set_capability_status(std::collections::HashMap::from([(
"go".to_string(),
pathfinder_lsp::types::LspLanguageStatus {
validation: true,
reason: "gopls connected (push diagnostics)".to_string(),
navigation_ready: Some(true),
indexing_complete: Some(true),
uptime_seconds: Some(30),
diagnostics_strategy: Some("push".to_string()),
supports_definition: Some(true),
supports_call_hierarchy: Some(true),
supports_diagnostics: Some(true),
supports_formatting: Some(false),
server_name: None,
},
)]));
let existing_warning = LspDiagnostic {
severity: LspDiagnosticSeverity::Warning,
code: Some("W001".into()),
message: "unused variable".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 — push validation with 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());
}
#[tokio::test]
async fn test_push_validation_introduced_error() {
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.set_capability_status(std::collections::HashMap::from([(
"go".to_string(),
pathfinder_lsp::types::LspLanguageStatus {
validation: true,
reason: "gopls connected (push diagnostics)".to_string(),
navigation_ready: Some(true),
indexing_complete: Some(true),
uptime_seconds: Some(30),
diagnostics_strategy: Some("push".to_string()),
supports_definition: Some(true),
supports_call_hierarchy: Some(true),
supports_diagnostics: Some(true),
supports_formatting: Some(false),
server_name: None,
},
)]));
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 push diagnostics finds new errors");
};
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 from push path"
);
}
#[tokio::test]
async fn test_push_validation_pre_fails() {
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_capability_status(std::collections::HashMap::from([(
"go".to_string(),
pathfinder_lsp::types::LspLanguageStatus {
validation: true,
reason: "gopls connected (push diagnostics)".to_string(),
navigation_ready: Some(true),
indexing_complete: Some(true),
uptime_seconds: Some(30),
diagnostics_strategy: Some("push".to_string()),
supports_definition: Some(true),
supports_call_hierarchy: Some(true),
supports_diagnostics: Some(true),
supports_formatting: Some(false),
server_name: None,
},
)]));
mock_lawyer.push_pull_diagnostics_result(Err("push collection 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 — push pre-diag failure gracefully degrades");
let resp = result.0;
assert!(resp.success, "edit should succeed despite pre-diag failure");
assert_eq!(resp.validation.status, "passed");
assert_eq!(
resp.validation.validation_confidence.as_deref(),
Some("syntax_only")
);
assert!(resp.validation_skipped);
assert_eq!(
resp.validation_skipped_reason.as_deref(),
Some("lsp_protocol_error"),
"push pre-diag failure should map to lsp_protocol_error"
);
}
#[tokio::test]
async fn test_push_validation_post_fails() {
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_capability_status(std::collections::HashMap::from([(
"go".to_string(),
pathfinder_lsp::types::LspLanguageStatus {
validation: true,
reason: "gopls connected (push diagnostics)".to_string(),
navigation_ready: Some(true),
indexing_complete: Some(true),
uptime_seconds: Some(30),
diagnostics_strategy: Some("push".to_string()),
supports_definition: Some(true),
supports_call_hierarchy: Some(true),
supports_diagnostics: Some(true),
supports_formatting: Some(false),
server_name: None,
},
)]));
mock_lawyer.push_pull_diagnostics_result(Ok(vec![]));
mock_lawyer
.push_pull_diagnostics_result(Err("connection lost during push collection".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 — push post-diag failure gracefully degrades");
let resp = result.0;
assert!(
resp.success,
"edit should succeed despite post-diag failure"
);
assert_eq!(resp.validation.status, "passed");
assert_eq!(
resp.validation.validation_confidence.as_deref(),
Some("syntax_only")
);
assert!(resp.validation_skipped);
assert_eq!(
resp.validation_skipped_reason.as_deref(),
Some("lsp_protocol_error"),
"push post-diag failure should map to lsp_protocol_error"
);
}
#[test]
fn test_lsp_error_to_skip_reason_no_lsp() {
use crate::server::PathfinderServer;
use pathfinder_lsp::LspError;
let err = LspError::NoLspAvailable;
let reason = PathfinderServer::lsp_error_to_skip_reason(&err);
assert_eq!(reason, "no_lsp");
}
#[test]
fn test_lsp_error_to_skip_reason_timeout() {
use crate::server::PathfinderServer;
use pathfinder_lsp::LspError;
let err = LspError::Timeout {
operation: "textDocument/definition".to_owned(),
timeout_ms: 10_000,
};
let reason = PathfinderServer::lsp_error_to_skip_reason(&err);
assert_eq!(reason, "lsp_timeout");
}
#[test]
fn test_lsp_error_to_skip_reason_protocol() {
use crate::server::PathfinderServer;
use pathfinder_lsp::LspError;
let err = LspError::Protocol("malformed JSON response".to_owned());
let reason = PathfinderServer::lsp_error_to_skip_reason(&err);
assert_eq!(reason, "lsp_protocol_error");
}
#[test]
fn test_lsp_error_to_skip_reason_connection_lost() {
use crate::server::PathfinderServer;
use pathfinder_lsp::LspError;
let err = LspError::ConnectionLost;
let reason = PathfinderServer::lsp_error_to_skip_reason(&err);
assert_eq!(reason, "lsp_crash");
}
#[test]
fn test_lsp_error_to_skip_reason_unsupported_capability() {
use crate::server::PathfinderServer;
use pathfinder_lsp::LspError;
let err = LspError::UnsupportedCapability {
capability: "diagnosticProvider".to_owned(),
};
let reason = PathfinderServer::lsp_error_to_skip_reason(&err);
assert_eq!(reason, "pull_diagnostics_unsupported");
}
#[test]
fn test_lsp_error_to_skip_reason_io_not_found() {
use crate::server::PathfinderServer;
use pathfinder_lsp::LspError;
use std::io::{Error, ErrorKind};
let io_err = Error::new(ErrorKind::NotFound, "No such file or directory");
let err = LspError::Io(io_err);
let reason = PathfinderServer::lsp_error_to_skip_reason(&err);
assert_eq!(reason, "lsp_not_on_path");
}
#[test]
fn test_lsp_error_to_skip_reason_io_other_kinds() {
use crate::server::PathfinderServer;
use pathfinder_lsp::LspError;
use std::io::{Error, ErrorKind};
for (kind, name) in [
(ErrorKind::PermissionDenied, "PermissionDenied"),
(ErrorKind::ConnectionRefused, "ConnectionRefused"),
(ErrorKind::BrokenPipe, "BrokenPipe"),
(ErrorKind::TimedOut, "TimedOut"),
(ErrorKind::Other, "Other"),
] {
let io_err = Error::new(kind, format!("{name} error"));
let err = LspError::Io(io_err);
let reason = PathfinderServer::lsp_error_to_skip_reason(&err);
assert_eq!(
reason, "lsp_start_failed",
"ErrorKind::{name} should map to 'lsp_start_failed'"
);
}
}