use super::lsp::*;
const DEP_DOC: &str = "version: \"1.0\"\nname: range-test\n\nmachines:\n local:\n hostname: localhost\n addr: 127.0.0.1\n\nresources:\n r1:\n type: file\n machine: local\n path: /tmp/r1.txt\n content: \"one\"\n\n r2:\n type: file\n machine: local\n path: /tmp/r2.txt\n content: \"two\"\n depends_on: [ghost-resource]\n";
fn open_doc(server: &mut LspServer, uri: &str, text: &str) {
server.handle_message(&serde_json::json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {"textDocument": {"uri": uri, "languageId": "yaml", "version": 1, "text": text}}
}));
}
#[test]
fn advertised_diagnostic_provider_is_actually_dispatched() {
let mut server = LspServer::new();
let uri = "file:///tmp/fj-lsp-pull.yaml";
open_doc(&mut server, uri, DEP_DOC);
let resp = server
.handle_message(&serde_json::json!({
"jsonrpc": "2.0", "id": 2,
"method": "textDocument/diagnostic",
"params": {"textDocument": {"uri": uri}}
}))
.expect("pull diagnostics must produce a response");
assert!(
resp.get("error").is_none(),
"advertised diagnosticProvider still answers with an error: {resp}"
);
let result = &resp["result"];
assert_eq!(result["kind"], "full", "expected a full report: {resp}");
let items = result["items"].as_array().expect("items array");
assert_eq!(items.len(), 1, "expected the ghost-resource error: {resp}");
assert!(items[0]["message"]
.as_str()
.unwrap_or_default()
.contains("ghost-resource"));
}
#[test]
fn every_advertised_capability_has_a_dispatcher_arm() {
let caps = initialize_result();
let caps = &caps["capabilities"];
let advertised: &[(&str, &str)] = &[
("completionProvider", "textDocument/completion"),
("hoverProvider", "textDocument/hover"),
("diagnosticProvider", "textDocument/diagnostic"),
];
let mut server = LspServer::new();
let uri = "file:///tmp/fj-lsp-caps.yaml";
open_doc(&mut server, uri, DEP_DOC);
for (cap, method) in advertised {
assert!(
!caps[cap].is_null(),
"capability '{cap}' is no longer advertised — drop it from this table too"
);
let resp = server
.handle_message(&serde_json::json!({
"jsonrpc": "2.0", "id": 9, "method": method,
"params": {"textDocument": {"uri": uri}, "position": {"line": 10, "character": 4}}
}))
.unwrap_or_else(|| panic!("'{method}' produced no response at all"));
assert!(
resp.get("error").is_none(),
"capability '{cap}' is advertised but '{method}' answers: {resp}"
);
}
}
#[test]
fn unadvertised_methods_still_get_method_not_found() {
let mut server = LspServer::new();
let resp = server
.handle_message(&serde_json::json!({
"jsonrpc": "2.0", "id": 3, "method": "textDocument/definition", "params": {}
}))
.expect("a request always gets a response");
assert_eq!(resp["error"]["code"], -32601);
}
#[test]
fn completion_items_serialise_insert_text_as_camel_case() {
let items = completion_items(4, "");
assert!(!items.is_empty(), "resource-field completions must exist");
let json = serde_json::to_value(&items).expect("serialise");
let first = &json[0];
assert!(
first.get("insertText").is_some(),
"CompletionItem must use the LSP wire name insertText: {first}"
);
assert!(
first.get("insert_text").is_none(),
"snake_case insert_text must not appear on the wire: {first}"
);
let insert = first["insertText"].as_str().unwrap_or_default();
assert!(
insert.ends_with(": "),
"field completions must still insert 'key: ', got {insert:?}"
);
assert_ne!(insert, first["label"].as_str().unwrap_or_default());
}
#[test]
fn completion_response_over_the_wire_uses_camel_case() {
let mut server = LspServer::new();
let uri = "file:///tmp/fj-lsp-complete.yaml";
open_doc(&mut server, uri, DEP_DOC);
let resp = server
.handle_message(&serde_json::json!({
"jsonrpc": "2.0", "id": 5, "method": "textDocument/completion",
"params": {"textDocument": {"uri": uri}, "position": {"line": 10, "character": 4}}
}))
.expect("completion response");
let items = resp["result"].as_array().expect("item list");
assert!(!items.is_empty());
assert!(items[0].get("insertText").is_some(), "{resp}");
}
#[test]
fn semantic_diagnostics_are_not_all_anchored_at_line_zero() {
let diags = validate_yaml(DEP_DOC);
let ghost = diags
.iter()
.find(|d| d.message.contains("ghost-resource"))
.expect("the unknown-dependency error must still be produced");
assert!(
ghost.line > 0,
"diagnostic is still anchored at line 0: {ghost:?}"
);
assert!(
ghost.end_character > ghost.character,
"zero-width range is invisible in editors: {ghost:?}"
);
let line = DEP_DOC.lines().nth(ghost.line as usize).unwrap_or("");
assert!(
line.contains("r2") || line.contains("ghost-resource"),
"diagnostic points at an unrelated line {}: {line:?}",
ghost.line
);
}
#[test]
fn yaml_parse_errors_use_the_parsers_own_location() {
let broken = "version: \"1.0\"\nname: t\nmachines:\n local:\n hostname: localhost\n addr: 127.0.0.1\n";
let diags = validate_yaml(broken);
assert_eq!(diags.len(), 1, "{diags:?}");
let d = &diags[0];
assert!(d.message.contains("YAML parse error"));
assert!(
d.line > 0,
"parse error is still anchored at line 0 while the message names a line: {d:?}"
);
assert!(d.end_character > d.character, "{d:?}");
}
#[test]
fn line_level_diagnostics_keep_their_own_line() {
let doc = "version: \"1.0\"\nname: t\nmachines: {}\nresources:\n r:\n type: package\n ensure: banana\n";
let diags = validate_yaml(doc);
let ensure = diags
.iter()
.find(|d| d.message.contains("Unknown ensure value"))
.expect("ensure diagnostic");
assert_eq!(ensure.line, 6, "{ensure:?}");
}