//! Independent regression probe of the stock native TypeScript API contract, //! speaking framed JSON-RPC directly to the backend instead of reusing the //! adapter under test. Set AVRIL_TEST_TYPESCRIPT to the backend's absolute //! path; the probe is skipped otherwise. use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::process::{Child, ChildStdin, Command, Stdio}; use std::rc::Rc; use std::sync::Mutex; use std::sync::mpsc::{Receiver, channel}; use std::time::{Duration, Instant}; use serde_json::{Value, json}; fn backend() -> Option { let path = std::env::var_os("AVRIL_TEST_TYPESCRIPT")?; if path.is_empty() { return None; } Some(PathBuf::from(path)) } fn write_frame(writer: &mut impl Write, message: &Value) -> std::io::Result<()> { let body = serde_json::to_vec(message).unwrap(); write!(writer, "Content-Length: {}\r\n\r\n", body.len())?; writer.write_all(&body)?; writer.flush() } fn read_frame(reader: &mut impl BufRead) -> std::io::Result> { let mut length = None; loop { let mut line = String::new(); if reader.read_line(&mut line)? == 0 { return Ok(None); } let line = line.trim_end_matches(['\r', '\n']); if line.is_empty() { break; } if let Some((name, value)) = line.split_once(':') && name.trim().eq_ignore_ascii_case("Content-Length") { length = Some(value.trim().parse::().unwrap()); } } let mut body = vec![0; length.unwrap()]; reader.read_exact(&mut body)?; Ok(Some(serde_json::from_slice(&body).unwrap())) } #[derive(Debug)] enum CallError { Rpc { code: i64, message: String }, Failed(String), } impl std::fmt::Display for CallError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CallError::Rpc { code, message } => write!(f, "RPC {code}: {message}"), CallError::Failed(message) => write!(f, "{message}"), } } } type Callback = Box Result>; struct ApiServer { child: Child, stdin: Option>, messages: Receiver, callbacks: HashMap, next_id: u64, failed: Option, } impl ApiServer { fn start(binary: &Path, callbacks: &[&str]) -> ApiServer { let mut child = Command::new(binary) .env_clear() .args([ "--api", "--async", "--cwd", "/avril-lsp", "--callbacks", &callbacks.join(","), ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn() .unwrap(); let stdin = Some(Mutex::new(child.stdin.take().unwrap())); let mut stdout = BufReader::new(child.stdout.take().unwrap()); let (tx, messages) = channel(); std::thread::spawn(move || { while let Ok(Some(message)) = read_frame(&mut stdout) { if tx.send(message).is_err() { return; } } }); ApiServer { child, stdin, messages, callbacks: HashMap::new(), next_id: 0, failed: None, } } fn send(&self, message: &Value) { write_frame(&mut *self.stdin.as_ref().unwrap().lock().unwrap(), message).unwrap(); } fn call(&mut self, method: &str, params: Value) -> Result { if let Some(error) = &self.failed { return Err(CallError::Failed(error.clone())); } self.next_id += 1; let id = self.next_id; self.send(&json!({"id": id, "method": method, "params": params})); let deadline = Instant::now() + Duration::from_secs(30); loop { let remaining = deadline.saturating_duration_since(Instant::now()); if remaining.is_zero() { self.failed = Some(format!("Timed out waiting for {method}")); return Err(CallError::Failed(self.failed.clone().unwrap())); } match self.messages.recv_timeout(remaining) { Ok(message) => { if let Some(callback_method) = message["method"].as_str() { // Callbacks may arrive while a request is in flight; // answering them inline keeps the reader progressing. let callback_id = message["id"].clone(); let response = match self.callbacks.get_mut(callback_method) { Some(callback) => match callback(&message["params"]) { Ok(result) => json!({"id": callback_id, "result": result}), Err(error) => json!({ "id": callback_id, "error": {"code": -32603, "message": error} }), }, None => json!({ "id": callback_id, "error": { "code": -32603, "message": format!("Unknown callback: {callback_method}") } }), }; self.send(&response); } else if message["id"].as_u64() == Some(id) { if let Some(error) = message.get("error") { return Err(CallError::Rpc { code: error["code"].as_i64().unwrap_or_default(), message: error["message"].as_str().unwrap_or_default().to_owned(), }); } return Ok(message["result"].clone()); } } Err(_) => { self.failed = Some("API server closed stdout".into()); return Err(CallError::Failed(self.failed.clone().unwrap())); } } } } fn close(mut self) { drop(self.stdin.take()); let deadline = Instant::now() + Duration::from_secs(2); loop { match self.child.try_wait().unwrap() { Some(status) => { assert!(status.success(), "API server exited with {status}"); return; } None if Instant::now() >= deadline => { self.child.kill().unwrap(); self.child.wait().unwrap(); panic!("API server did not exit"); } None => std::thread::sleep(Duration::from_millis(10)), } } } } impl Drop for ApiServer { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); } } fn version(binary: &Path) -> String { let output = Command::new(binary).arg("--version").output().unwrap(); assert!(output.status.success(), "Cannot run backend"); String::from_utf8(output.stdout).unwrap().trim().to_owned() } #[test] fn capabilities() { let Some(binary) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; println!("{}", version(&binary)); let mut api = ApiServer::start(&binary, &[]); println!("initialize: {}", api.call("initialize", json!({})).unwrap()); match api.call("createModuleResolver", json!({"compilerOptions": {}})) { Ok(resolver) => { api.call("releaseModuleResolver", json!({"resolver": resolver})) .unwrap(); println!("Custom module-resolution API: available"); } Err(CallError::Rpc { message, .. }) => { assert!( message.contains("unknown API method \"createModuleResolver\""), "Unexpected capability failure: {message}" ); println!("Custom module-resolution API: unavailable"); } Err(error) => panic!("Unexpected capability failure: {error}"), } api.close(); } struct Fixture { original_to_file: BTreeMap, directory_to_original: BTreeMap, files: BTreeMap, } impl Fixture { fn add(&mut self, original: &str, source: &str) -> String { let extension = if original.ends_with(".d.ts") { ".d.ts" } else if original.ends_with(".tsx") { ".tsx" } else { ".ts" }; let directory = format!("/avril-lsp/modules/{}", self.original_to_file.len()); let file = format!("{directory}/source{extension}"); self.original_to_file .insert(original.to_owned(), file.clone()); self.directory_to_original .insert(directory, original.to_owned()); self.files.insert(file.clone(), source.to_owned()); file } } fn utf16_index(line: &str, byte_index: usize) -> usize { line[..byte_index].encode_utf16().count() } fn layer(files: Vec<(String, String)>) -> Value { let mut map = serde_json::Map::new(); for (name, text) in files { map.insert(name, json!(text)); } Value::Object(map) } #[test] fn native_api_contract() { let Some(binary) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; println!("{}", version(&binary)); let lib_directory = binary.parent().unwrap().to_path_buf(); let fs_callbacks = [ "readFile", "fileExists", "directoryExists", "getAccessibleEntries", ]; let mut callback_names: Vec = fs_callbacks.iter().map(ToString::to_string).collect(); callback_names.extend( [ "realpath:identity", "stat:fakeStat", "writeFile:error", "removeFile:error", ] .iter() .map(ToString::to_string), ); let callback_refs: Vec<&str> = callback_names.iter().map(String::as_str).collect(); let mut api = ApiServer::start(&binary, &callback_refs); let fs_calls = Rc::new(RefCell::new(Vec::<(String, String, bool)>::new())); for method in fs_callbacks { let fs_calls = fs_calls.clone(); let lib_directory = lib_directory.clone(); api.callbacks.insert( method.to_owned(), Box::new(move |params| { let path = params .as_str() .ok_or_else(|| format!("Unexpected filesystem callback: {method}"))?; let library_name = path .strip_prefix(&format!("{}/", lib_directory.display())) .filter(|name| { name.starts_with("lib.") && name.ends_with(".d.ts") && name .bytes() .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-')) }); let allowed = library_name.is_some() || Path::new(path) == lib_directory || path.starts_with("bundled:///libs/") || path == "bundled:///libs"; fs_calls .borrow_mut() .push((method.to_owned(), path.to_owned(), allowed)); if allowed { return Ok(json!({"kind": "useOS"})); } if method == "readFile" { return Ok(json!({"kind": "missing"})); } Ok(json!({ "kind": "value", "value": if method == "getAccessibleEntries" { json!({"files": [], "directories": []}) } else { json!(false) } })) }), ); } let mut snapshots: Vec = Vec::new(); let mut resolvers: Vec = Vec::new(); let mut passed: Vec = Vec::new(); let pass = |passed: &mut Vec, message: &str| { passed.push(message.to_owned()); println!("PASS {message}"); }; println!("initialize: {}", api.call("initialize", json!({})).unwrap()); let raw_options = json!({ "strict": true, "noEmit": true, "skipLibCheck": true, "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "allowImportingTsExtensions": true, "jsx": "react-jsx", "jsxImportSource": "@fixture/ui", "types": [] }); let mut fixture = Fixture { original_to_file: BTreeMap::new(), directory_to_original: BTreeMap::new(), files: BTreeMap::new(), }; let main_uri = "file:///workspace/app/main.tsx"; let main_source = [ "import { process } from \"avril:std\";", "import { answer } from \"jsr:@fixture/data\";", "import { label } from \"./local.ts\";", "const emoji = \"😀\"; const wrong: number = \"bad\";", "const view = ;", "process.args.push(\"x\");", "export const result = { answer, label, view };", ] .join("\n"); let main = fixture.add(main_uri, &main_source); let a_uri = "file:///workspace/app/a.ts"; let b_uri = "file:///workspace/app/b.ts"; let a = fixture.add( a_uri, "import { value } from \"scoped\"; export const result: \"A\" = value;", ); let b = fixture.add( b_uri, "import { value } from \"scoped\"; export const result: \"B\" = value;", ); let completion_source = "import { process } from \"avril:std\"; process.args;"; let completion = fixture.add("file:///workspace/app/completion.ts", completion_source); fixture.add( "file:///workspace/deps/a.ts", "export const value = \"A\" as const;", ); fixture.add( "file:///workspace/deps/b.ts", "export const value = \"B\" as const;", ); fixture.add( "file:///workspace/app/local.ts", "export const label = \"local\";", ); fixture.add( "https://jsr.io/@fixture/data/1.0.0/mod.d.ts", "export declare const answer: 42;", ); fixture.add( "file:///workspace/ui/jsx-runtime.d.ts", "\nexport namespace JSX {\n interface Element {}\n interface IntrinsicElements { panel: { count: number } }\n}\nexport declare function jsx(...args: unknown[]): JSX.Element;\nexport declare const jsxs: typeof jsx;\nexport declare const Fragment: unique symbol;\n", ); // The production catalog includes both handwritten and generated declarations. let mut roots = vec![main.clone(), a.clone(), b.clone(), completion.clone()]; for (name, text) in avril_runtime::stdlib::declarations() { roots.push(fixture.add(&format!("avril:///lib/{name}"), text)); } let fixture = Rc::new(RefCell::new(fixture)); let calls = Rc::new(RefCell::new(Vec::::new())); let parse_options = |api: &mut ApiServer, compiler_options: &Value, roots: &[String]| { // The wire protocol wants normalized enum values, not tsconfig spellings. let parsed = api .call( "parseJsonConfigFileContent", json!({ "json": {"compilerOptions": compiler_options, "files": roots}, "configDirectory": "/avril-lsp" }), ) .unwrap(); assert!( parsed["errors"].as_array().unwrap().is_empty(), "Invalid compiler options: {}", parsed["errors"] ); parsed["options"].clone() }; let options = parse_options(&mut api, &raw_options, &roots); // Resolution policy is pinned to the resolver, including for older snapshots. let register_resolver = |api: &mut ApiServer, resolvers: &mut Vec, calls: &Rc>>, fixture: &Rc>, compiler_options: &Value, swap_scopes: bool| -> u64 { let callback = format!("avril/resolve/{}", resolvers.len()); let calls = calls.clone(); let fixture = fixture.clone(); api.callbacks.insert( callback.clone(), Box::new(move |params| { calls.borrow_mut().push(params.clone()); let fixture = fixture.borrow(); let original = fixture .directory_to_original .get(params["containingDirectory"].as_str().unwrap()); let resolved = match params["moduleName"].as_str().unwrap() { "scoped" => { let (dep_a, dep_b) = ( &fixture.original_to_file["file:///workspace/deps/a.ts"], &fixture.original_to_file["file:///workspace/deps/b.ts"], ); match (original.map(String::as_str), swap_scopes) { (Some("file:///workspace/app/a.ts"), false) => Some(dep_a), (Some("file:///workspace/app/a.ts"), true) => Some(dep_b), (Some("file:///workspace/app/b.ts"), false) => Some(dep_b), (Some("file:///workspace/app/b.ts"), true) => Some(dep_a), _ => None, } } "jsr:@fixture/data" => Some( &fixture.original_to_file["https://jsr.io/@fixture/data/1.0.0/mod.d.ts"], ), "@fixture/ui/jsx-runtime" => { Some(&fixture.original_to_file["file:///workspace/ui/jsx-runtime.d.ts"]) } "./local.ts" if original.map(String::as_str) == Some("file:///workspace/app/main.tsx") => { Some(&fixture.original_to_file["file:///workspace/app/local.ts"]) } _ => None, }; Ok(match resolved { Some(file) => json!({"resolvedFileName": file}), None => Value::Null, }) }), ); let id = api .call( "createModuleResolver", json!({ "compilerOptions": compiler_options, "resolveModuleNameCallback": callback }), ) .unwrap() .as_u64() .unwrap(); resolvers.push(id); id }; let (main_f, a_f, b_f) = (main.clone(), a.clone(), b.clone()); let files_snapshot = fixture.borrow().files.clone(); let mut resolver_id = register_resolver(&mut api, &mut resolvers, &calls, &fixture, &options, false); let _ = resolver_id; let mut snapshot = api .call( "createSnapshot", json!({ "fileSystem": {"kind": "layer", "files": files_snapshot}, "createPrograms": [{ "rootFiles": roots.clone(), "compilerOptions": options, "options": {"moduleResolver": resolver_id} }] }), ) .unwrap(); snapshots.push(snapshot["snapshot"].as_u64().unwrap()); let project = snapshot["operation"]["createdPrograms"][0] .as_str() .unwrap() .to_owned(); assert!( snapshot["projects"][0]["parsedCommandLine"]["errors"] .as_array() .unwrap() .is_empty(), "Invalid compiler options" ); let initial = snapshot.clone(); // Helper closures over the API. macro_rules! diagnostics { ($snapshot:expr, $selected:expr) => { api.call( "getSemanticDiagnostics", json!({ "snapshot": $snapshot["snapshot"], "project": project, "files": $selected }), ) .unwrap() }; } let first = diagnostics!(snapshot, [main_f.clone(), a_f.clone(), b_f.clone()]); let first = first.as_array().unwrap(); assert_eq!(first.len(), 3, "Expected exactly three intentional errors"); assert_eq!( first.iter().filter(|d| d["code"] == json!(2322)).count(), 2, "Missing assignment/JSX diagnostics" ); assert!( first.iter().any(|d| d["code"] == json!(2339)), "Actual avril declarations did not enforce readonly process.args" ); assert!( first.iter().all(|d| d["fileName"] == json!(main_f)), "Scoped resolution or declarations failed" ); pass( &mut passed, "unsaved TSX + actual avril declarations + local/JSR-shaped fixture imports", ); { let calls = calls.borrow(); let fixture = fixture.borrow(); assert!( calls .iter() .any(|c| c["moduleName"] == json!("@fixture/ui/jsx-runtime")), "JSX implicit import bypassed resolver" ); for uri in [a_uri, b_uri] { assert!( calls.iter().any(|c| { fixture .directory_to_original .get(c["containingDirectory"].as_str().unwrap()) .map(String::as_str) == Some(uri) && c["moduleName"] == json!("scoped") }), "No {uri} callback" ); } } pass( &mut passed, "per-file scopes via compiler-only directories; original source unmodified", ); let unicode = first .iter() .find(|d| d["startPosition"]["line"] == json!(3)) .unwrap(); let line = main_source.lines().nth(3).unwrap(); assert_eq!( unicode["startPosition"]["character"].as_u64().unwrap() as usize, utf16_index(line, line.find("wrong").unwrap()), "Diagnostic position is not UTF-16" ); pass( &mut passed, "diagnostic UTF-16 positions after an astral character", ); let completions = api .call( "getCompletionsAtPosition", json!({ "snapshot": snapshot["snapshot"], "project": project, "file": fixture.borrow().original_to_file["file:///workspace/app/completion.ts"], "position": completion_source.rfind("process.").unwrap() + "process.".len() }), ) .unwrap(); let names: Vec<&str> = completions["entries"] .as_array() .unwrap() .iter() .map(|entry| entry["name"].as_str().unwrap()) .collect(); assert!(names.contains(&"args"), "Missing process.args completion"); assert!(names.contains(&"cwd"), "Missing process.cwd completion"); pass(&mut passed, "member completion from real avril types"); let position = utf16_index(&main_source, main_source.rfind("answer").unwrap()); let ty = api .call( "getTypeAtPosition", json!({ "snapshot": snapshot["snapshot"], "project": project, "file": main_f, "position": position }), ) .unwrap(); let type_text = api .call( "typeToString", json!({ "snapshot": snapshot["snapshot"], "project": project, "type": ty["id"] }), ) .unwrap(); assert_eq!(type_text, json!("42"), "Unexpected type text: {type_text}"); let symbol = api .call( "getSymbolAtPosition", json!({ "snapshot": snapshot["snapshot"], "project": project, "file": main_f, "position": position }), ) .unwrap(); assert!( !symbol["declarations"].as_array().unwrap().is_empty(), "Missing symbol declaration handles" ); pass( &mut passed, "hover building blocks: type text and symbol/declaration handles", ); let alias = api .call( "getSymbolAtPosition", json!({ "snapshot": snapshot["snapshot"], "project": project, "file": main_f, "position": main_source.find("answer").unwrap() }), ) .unwrap(); let target = api .call( "getAliasedSymbol", json!({ "snapshot": snapshot["snapshot"], "project": project, "symbol": alias["reference"] }), ) .unwrap(); let remote_file = fixture.borrow().original_to_file["https://jsr.io/@fixture/data/1.0.0/mod.d.ts"].clone(); assert!( target["declarations"] .as_array() .unwrap() .iter() .any(|handle| handle .as_str() .unwrap() .ends_with(&format!(".{remote_file}"))), "Import alias did not lead to remote declaration" ); let local_references = api .call( "getReferencesToSymbolInFile", json!({ "snapshot": snapshot["snapshot"], "project": project, "file": main_f, "symbol": alias["reference"] }), ) .unwrap(); let local_references = local_references.as_array().unwrap(); assert!( local_references.len() >= 2, "Missing import/use reference handles" ); let references = api .call( "getReferencedSymbolsForNode", json!({ "snapshot": snapshot["snapshot"], "project": project, "node": local_references[0], "position": main_source.find("answer").unwrap() }), ) .unwrap(); let mut handles: Vec = Vec::new(); for entry in references.as_array().unwrap() { handles.push(entry["definition"].as_str().unwrap().to_owned()); handles.extend( entry["references"] .as_array() .unwrap() .iter() .map(|h| h.as_str().unwrap().to_owned()), ); } let handle_original = |handle: &str| -> String { let mut parts = handle.splitn(3, '.'); parts.next().unwrap(); parts.next().unwrap(); let file = parts.next().unwrap(); fixture .borrow() .original_to_file .iter() .find(|(_, candidate)| *candidate == file) .map(|(original, _)| original.clone()) .unwrap_or_else(|| panic!("Unknown compiler file: {file}")) }; let reference_originals: Vec = handles.iter().map(|h| handle_original(h)).collect(); assert!(reference_originals.iter().any(|u| u == main_uri)); assert!( reference_originals .iter() .any(|u| u == "https://jsr.io/@fixture/data/1.0.0/mod.d.ts") ); pass( &mut passed, "cross-file definition/reference handles map back to original module URIs", ); let fixed = main_source .replace("number = \"bad\"", "number = 1") .replace("count=\"wrong\"", "count={1}") .replace("process.args.push(\"x\")", "process.args.includes(\"x\")"); snapshot = api .call( "updateSnapshot", json!({ "snapshot": snapshot["snapshot"], "changes": { "fileSystem": {"kind": "layer", "files": layer(vec![(main_f.clone(), fixed)])}, "ensurePrograms": [project] } }), ) .unwrap(); snapshots.push(snapshot["snapshot"].as_u64().unwrap()); assert_eq!( diagnostics!(snapshot, [main_f.clone(), a_f.clone(), b_f.clone()]) .as_array() .unwrap() .len(), 0, "Unsaved fixes did not clear diagnostics" ); assert_eq!( diagnostics!(initial, [main_f.clone(), a_f.clone(), b_f.clone()]) .as_array() .unwrap() .len(), 3, "Old snapshot changed after edit" ); pass( &mut passed, "incremental unsaved edits; old snapshot remains queryable and unchanged", ); let b_edited = format!( "{}\nimport {{ result as bypass }} from \"{}\"; void bypass;", fixture.borrow().files[&b_f], a_f ); let denied = api .call( "updateSnapshot", json!({ "snapshot": snapshot["snapshot"], "changes": { "fileSystem": {"kind": "layer", "files": layer(vec![(b_f.clone(), b_edited)])}, "ensurePrograms": [project] } }), ) .unwrap(); snapshots.push(denied["snapshot"].as_u64().unwrap()); let denied_diagnostics = diagnostics!(denied, [main_f.clone(), a_f.clone(), b_f.clone()]); let denied_diagnostics = denied_diagnostics.as_array().unwrap(); assert_eq!( denied_diagnostics.len(), 1, "Null resolver result fell back to an existing compiler file" ); assert_eq!(denied_diagnostics[0]["code"], json!(2307)); assert_eq!( diagnostics!(snapshot, [main_f.clone(), a_f.clone(), b_f.clone()]) .as_array() .unwrap() .len(), 0, "Branch snapshot modified its parent" ); pass( &mut passed, "unmapped imports stay unresolved even when the target exists in the virtual filesystem", ); resolver_id = register_resolver(&mut api, &mut resolvers, &calls, &fixture, &options, true); snapshot = api .call( "updateSnapshot", json!({ "snapshot": snapshot["snapshot"], "changes": { "reconfigurePrograms": [{ "id": project, "rootFiles": roots, "compilerOptions": options, "options": {"moduleResolver": resolver_id} }] } }), ) .unwrap(); snapshots.push(snapshot["snapshot"].as_u64().unwrap()); let remapped = diagnostics!(snapshot, [main_f.clone(), a_f.clone(), b_f.clone()]); let remapped = remapped.as_array().unwrap(); assert_eq!( remapped.len(), 2, "Resolver replacement did not invalidate scoped imports" ); assert!( remapped.iter().all(|d| { d["code"] == json!(2322) && (d["fileName"] == json!(a_f) || d["fileName"] == json!(b_f)) }), "Resolver replacement did not invalidate scoped imports" ); assert_eq!( diagnostics!(initial, [main_f.clone(), a_f.clone(), b_f.clone()]) .as_array() .unwrap() .len(), 3, "Resolver replacement affected the old snapshot" ); pass( &mut passed, "import-map change: new resolver + reconfiguration invalidates cached resolutions", ); let raw_options_dev = json!({ "strict": true, "noEmit": true, "skipLibCheck": true, "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "allowImportingTsExtensions": true, "jsx": "react-jsxdev", "jsxImportSource": "@fixture/dev-ui", "types": [] }); let development_options = parse_options(&mut api, &raw_options_dev, &roots); resolver_id = register_resolver( &mut api, &mut resolvers, &calls, &fixture, &development_options, true, ); snapshot = api .call( "updateSnapshot", json!({ "snapshot": snapshot["snapshot"], "changes": { "reconfigurePrograms": [{ "id": project, "rootFiles": roots, "compilerOptions": development_options, "options": {"moduleResolver": resolver_id} }] } }), ) .unwrap(); snapshots.push(snapshot["snapshot"].as_u64().unwrap()); let jsx_diagnostics = diagnostics!(snapshot, [main_f.clone(), a_f.clone(), b_f.clone()]); { let calls = calls.borrow(); assert!( calls .iter() .any(|c| c["moduleName"] == json!("@fixture/dev-ui/jsx-dev-runtime")), "JSX settings did not update implicit resolution" ); } assert!( jsx_diagnostics.as_array().unwrap().iter().any(|d| { d["fileName"] == json!(main_f) && d["text"].as_str().unwrap().contains("jsx-dev-runtime") }), "Missing runtime was not diagnosed" ); pass( &mut passed, "JSX configuration change re-resolves the generated runtime import", ); // Negative control: same-directory filenames lose per-file scope identity. let raw_calls = Rc::new(RefCell::new(Vec::::new())); { let raw_calls = raw_calls.clone(); let dep_a = fixture.borrow().original_to_file["file:///workspace/deps/a.ts"].clone(); api.callbacks.insert( "avril/directory-control".to_owned(), Box::new(move |params| { if params["moduleName"] != json!("scoped") { return Ok(Value::Null); } raw_calls.borrow_mut().push(params.clone()); Ok(json!({"resolvedFileName": dep_a})) }), ); } let control_resolver = api .call( "createModuleResolver", json!({ "compilerOptions": options, "resolveModuleNameCallback": "avril/directory-control" }), ) .unwrap() .as_u64() .unwrap(); resolvers.push(control_resolver); let raw_a = "/avril-lsp/unmapped/a.ts"; let raw_b = "/avril-lsp/unmapped/b.ts"; let (a_source, b_source, dep_a_source) = { let fixture = fixture.borrow(); ( fixture.files[&a_f].clone(), fixture.files[&b_f].clone(), fixture.files[&fixture.original_to_file["file:///workspace/deps/a.ts"]].clone(), ) }; let dep_a_file = fixture.borrow().original_to_file["file:///workspace/deps/a.ts"].clone(); let control = api .call( "createSnapshot", json!({ "fileSystem": {"kind": "layer", "files": layer(vec![ (raw_a.to_owned(), a_source), (raw_b.to_owned(), b_source), (dep_a_file.clone(), dep_a_source), ])}, "createPrograms": [{ "rootFiles": [raw_a, raw_b], "compilerOptions": options, "options": {"moduleResolver": control_resolver} }] }), ) .unwrap(); snapshots.push(control["snapshot"].as_u64().unwrap()); let control_diagnostics = api .call( "getSemanticDiagnostics", json!({ "snapshot": control["snapshot"], "project": control["operation"]["createdPrograms"][0] }), ) .unwrap(); let control_diagnostics = control_diagnostics.as_array().unwrap(); assert_eq!( control_diagnostics.len(), 1, "Directory-only control did not reproduce the scope collision" ); assert_eq!(control_diagnostics[0]["code"], json!(2322)); assert_eq!(control_diagnostics[0]["fileName"], json!(raw_b)); { let raw_calls = raw_calls.borrow(); assert!(!raw_calls.is_empty()); assert!( raw_calls.iter().all(|call| { call["containingDirectory"] == json!("/avril-lsp/unmapped") && call.get("containingFile").is_none() }), "Callback now exposes a full referrer; reassess virtual directories" ); } pass( &mut passed, "negative control: original same-directory filenames lose per-file scope identity", ); { let fs_calls = fs_calls.borrow(); assert!( fs_calls .iter() .any(|(method, _, allowed)| method == "readFile" && *allowed), "No standard library reads observed" ); assert!( fs_calls.iter().any(|(_, _, allowed)| !allowed), "No out-of-library probes observed" ); } pass( &mut passed, "filesystem callbacks serve standard libraries and deny other host-file probes", ); for method in [ "getQuickInfoAtPosition", "getDefinitionAtPosition", "findRenameLocations", "textDocument/hover", "textDocument/definition", "textDocument/rename", ] { match api.call( method, json!({ "snapshot": snapshot["snapshot"], "project": project, "file": main_f, "position": position }), ) { Err(CallError::Rpc { message, .. }) => { assert!( message.contains("unknown API method"), "Unexpected failure probing {method}: {message}" ); println!("GAP {method}: not exposed by the API server"); } other => panic!("Unexpectedly supported {method}: {other:?}"), } } pass( &mut passed, "feature-gap probes distinguish absent methods from project/type errors", ); println!( "\n{} checks passed; {} resolver callbacks, {} retained snapshots.", passed.len(), calls.borrow().len(), snapshots.len() ); for snapshot_id in snapshots.iter().rev() { api.call("release", json!({"snapshot": snapshot_id})) .unwrap(); } for resolver in &resolvers { api.call("releaseModuleResolver", json!({"resolver": resolver})) .unwrap(); } api.close(); }