899 lines
35 KiB
TypeScript
899 lines
35 KiB
TypeScript
/**
|
|
* Tests for LSP hook - configuration and utility functions
|
|
*
|
|
* Run with: npm test
|
|
*
|
|
* These tests cover:
|
|
* - Project root detection for various languages
|
|
* - Language ID mappings
|
|
* - URI construction
|
|
* - Server configuration correctness
|
|
*/
|
|
|
|
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
|
|
import { tmpdir } from "os";
|
|
import { join } from "path";
|
|
import { pathToFileURL } from "url";
|
|
import { LSP_SERVERS, LANGUAGE_IDS } from "../lsp-core.js";
|
|
|
|
// ============================================================================
|
|
// Test utilities
|
|
// ============================================================================
|
|
|
|
interface TestResult {
|
|
name: string;
|
|
passed: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
const tests: Array<{ name: string; fn: () => Promise<void> }> = [];
|
|
|
|
function test(name: string, fn: () => Promise<void>) {
|
|
tests.push({ name, fn });
|
|
}
|
|
|
|
function assert(condition: boolean, message: string) {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
function assertEquals<T>(actual: T, expected: T, message: string) {
|
|
assert(
|
|
actual === expected,
|
|
`${message}\nExpected: ${JSON.stringify(expected)}\nActual: ${JSON.stringify(actual)}`
|
|
);
|
|
}
|
|
|
|
function assertIncludes(arr: string[], item: string, message: string) {
|
|
assert(arr.includes(item), `${message}\nArray: [${arr.join(", ")}]\nMissing: ${item}`);
|
|
}
|
|
|
|
/** Create a temp directory with optional file structure */
|
|
async function withTempDir(
|
|
structure: Record<string, string | null>, // null = directory, string = file content
|
|
fn: (dir: string) => Promise<void>
|
|
): Promise<void> {
|
|
const dir = await mkdtemp(join(tmpdir(), "lsp-test-"));
|
|
try {
|
|
for (const [path, content] of Object.entries(structure)) {
|
|
const fullPath = join(dir, path);
|
|
if (content === null) {
|
|
await mkdir(fullPath, { recursive: true });
|
|
} else {
|
|
await mkdir(join(dir, path.split("/").slice(0, -1).join("/")), { recursive: true }).catch(() => {});
|
|
await writeFile(fullPath, content);
|
|
}
|
|
}
|
|
await fn(dir);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Language ID tests
|
|
// ============================================================================
|
|
|
|
test("LANGUAGE_IDS: TypeScript extensions", async () => {
|
|
assertEquals(LANGUAGE_IDS[".ts"], "typescript", ".ts should map to typescript");
|
|
assertEquals(LANGUAGE_IDS[".tsx"], "typescriptreact", ".tsx should map to typescriptreact");
|
|
assertEquals(LANGUAGE_IDS[".mts"], "typescript", ".mts should map to typescript");
|
|
assertEquals(LANGUAGE_IDS[".cts"], "typescript", ".cts should map to typescript");
|
|
});
|
|
|
|
test("LANGUAGE_IDS: JavaScript extensions", async () => {
|
|
assertEquals(LANGUAGE_IDS[".js"], "javascript", ".js should map to javascript");
|
|
assertEquals(LANGUAGE_IDS[".jsx"], "javascriptreact", ".jsx should map to javascriptreact");
|
|
assertEquals(LANGUAGE_IDS[".mjs"], "javascript", ".mjs should map to javascript");
|
|
assertEquals(LANGUAGE_IDS[".cjs"], "javascript", ".cjs should map to javascript");
|
|
});
|
|
|
|
test("LANGUAGE_IDS: Dart extension", async () => {
|
|
assertEquals(LANGUAGE_IDS[".dart"], "dart", ".dart should map to dart");
|
|
});
|
|
|
|
test("LANGUAGE_IDS: Go extension", async () => {
|
|
assertEquals(LANGUAGE_IDS[".go"], "go", ".go should map to go");
|
|
});
|
|
|
|
test("LANGUAGE_IDS: Rust extension", async () => {
|
|
assertEquals(LANGUAGE_IDS[".rs"], "rust", ".rs should map to rust");
|
|
});
|
|
|
|
test("LANGUAGE_IDS: Kotlin extensions", async () => {
|
|
assertEquals(LANGUAGE_IDS[".kt"], "kotlin", ".kt should map to kotlin");
|
|
assertEquals(LANGUAGE_IDS[".kts"], "kotlin", ".kts should map to kotlin");
|
|
});
|
|
|
|
test("LANGUAGE_IDS: Swift extension", async () => {
|
|
assertEquals(LANGUAGE_IDS[".swift"], "swift", ".swift should map to swift");
|
|
});
|
|
|
|
test("LANGUAGE_IDS: Python extensions", async () => {
|
|
assertEquals(LANGUAGE_IDS[".py"], "python", ".py should map to python");
|
|
assertEquals(LANGUAGE_IDS[".pyi"], "python", ".pyi should map to python");
|
|
});
|
|
|
|
test("LANGUAGE_IDS: Vue/Svelte/Astro extensions", async () => {
|
|
assertEquals(LANGUAGE_IDS[".vue"], "vue", ".vue should map to vue");
|
|
assertEquals(LANGUAGE_IDS[".svelte"], "svelte", ".svelte should map to svelte");
|
|
assertEquals(LANGUAGE_IDS[".astro"], "astro", ".astro should map to astro");
|
|
});
|
|
|
|
// ============================================================================
|
|
// Server configuration tests
|
|
// ============================================================================
|
|
|
|
test("LSP_SERVERS: has TypeScript server", async () => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript");
|
|
assert(server !== undefined, "Should have typescript server");
|
|
assertIncludes(server!.extensions, ".ts", "Should handle .ts");
|
|
assertIncludes(server!.extensions, ".tsx", "Should handle .tsx");
|
|
assertIncludes(server!.extensions, ".js", "Should handle .js");
|
|
assertIncludes(server!.extensions, ".jsx", "Should handle .jsx");
|
|
});
|
|
|
|
test("LSP_SERVERS: has Dart server", async () => {
|
|
const server = LSP_SERVERS.find(s => s.id === "dart");
|
|
assert(server !== undefined, "Should have dart server");
|
|
assertIncludes(server!.extensions, ".dart", "Should handle .dart");
|
|
});
|
|
|
|
test("LSP_SERVERS: has Rust Analyzer server", async () => {
|
|
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer");
|
|
assert(server !== undefined, "Should have rust-analyzer server");
|
|
assertIncludes(server!.extensions, ".rs", "Should handle .rs");
|
|
});
|
|
|
|
test("LSP_SERVERS: has Gopls server", async () => {
|
|
const server = LSP_SERVERS.find(s => s.id === "gopls");
|
|
assert(server !== undefined, "Should have gopls server");
|
|
assertIncludes(server!.extensions, ".go", "Should handle .go");
|
|
});
|
|
|
|
test("LSP_SERVERS: has Kotlin server", async () => {
|
|
const server = LSP_SERVERS.find(s => s.id === "kotlin");
|
|
assert(server !== undefined, "Should have kotlin server");
|
|
assertIncludes(server!.extensions, ".kt", "Should handle .kt");
|
|
assertIncludes(server!.extensions, ".kts", "Should handle .kts");
|
|
});
|
|
|
|
test("LSP_SERVERS: has Swift server", async () => {
|
|
const server = LSP_SERVERS.find(s => s.id === "swift");
|
|
assert(server !== undefined, "Should have swift server");
|
|
assertIncludes(server!.extensions, ".swift", "Should handle .swift");
|
|
});
|
|
|
|
test("LSP_SERVERS: has Pyright server", async () => {
|
|
const server = LSP_SERVERS.find(s => s.id === "pyright");
|
|
assert(server !== undefined, "Should have pyright server");
|
|
assertIncludes(server!.extensions, ".py", "Should handle .py");
|
|
assertIncludes(server!.extensions, ".pyi", "Should handle .pyi");
|
|
});
|
|
|
|
// ============================================================================
|
|
// TypeScript root detection tests
|
|
// ============================================================================
|
|
|
|
test("typescript: finds root with package.json", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"src/index.ts": "export const x = 1;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "src/index.ts"), dir);
|
|
assertEquals(root, dir, "Should find root at package.json location");
|
|
});
|
|
});
|
|
|
|
test("typescript: finds root with tsconfig.json", async () => {
|
|
await withTempDir({
|
|
"tsconfig.json": "{}",
|
|
"src/index.ts": "export const x = 1;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "src/index.ts"), dir);
|
|
assertEquals(root, dir, "Should find root at tsconfig.json location");
|
|
});
|
|
});
|
|
|
|
test("typescript: finds root with jsconfig.json", async () => {
|
|
await withTempDir({
|
|
"jsconfig.json": "{}",
|
|
"src/app.js": "const x = 1;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "src/app.js"), dir);
|
|
assertEquals(root, dir, "Should find root at jsconfig.json location");
|
|
});
|
|
});
|
|
|
|
test("typescript: returns undefined for deno projects", async () => {
|
|
await withTempDir({
|
|
"deno.json": "{}",
|
|
"main.ts": "console.log('deno');",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "main.ts"), dir);
|
|
assertEquals(root, undefined, "Should return undefined for deno projects");
|
|
});
|
|
});
|
|
|
|
test("typescript: nested package finds nearest root", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"packages/web/package.json": "{}",
|
|
"packages/web/src/index.ts": "export const x = 1;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "packages/web/src/index.ts"), dir);
|
|
assertEquals(root, join(dir, "packages/web"), "Should find nearest package.json");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Dart root detection tests
|
|
// ============================================================================
|
|
|
|
test("dart: finds root with pubspec.yaml", async () => {
|
|
await withTempDir({
|
|
"pubspec.yaml": "name: my_app",
|
|
"lib/main.dart": "void main() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
const root = server.findRoot(join(dir, "lib/main.dart"), dir);
|
|
assertEquals(root, dir, "Should find root at pubspec.yaml location");
|
|
});
|
|
});
|
|
|
|
test("dart: finds root with analysis_options.yaml", async () => {
|
|
await withTempDir({
|
|
"analysis_options.yaml": "linter: rules:",
|
|
"lib/main.dart": "void main() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
const root = server.findRoot(join(dir, "lib/main.dart"), dir);
|
|
assertEquals(root, dir, "Should find root at analysis_options.yaml location");
|
|
});
|
|
});
|
|
|
|
test("dart: nested package finds nearest root", async () => {
|
|
await withTempDir({
|
|
"pubspec.yaml": "name: monorepo",
|
|
"packages/core/pubspec.yaml": "name: core",
|
|
"packages/core/lib/core.dart": "void init() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
const root = server.findRoot(join(dir, "packages/core/lib/core.dart"), dir);
|
|
assertEquals(root, join(dir, "packages/core"), "Should find nearest pubspec.yaml");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Rust root detection tests
|
|
// ============================================================================
|
|
|
|
test("rust: finds root with Cargo.toml", async () => {
|
|
await withTempDir({
|
|
"Cargo.toml": "[package]\nname = \"my_crate\"",
|
|
"src/lib.rs": "pub fn hello() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
const root = server.findRoot(join(dir, "src/lib.rs"), dir);
|
|
assertEquals(root, dir, "Should find root at Cargo.toml location");
|
|
});
|
|
});
|
|
|
|
test("rust: nested workspace member finds nearest Cargo.toml", async () => {
|
|
await withTempDir({
|
|
"Cargo.toml": "[workspace]\nmembers = [\"crates/*\"]",
|
|
"crates/core/Cargo.toml": "[package]\nname = \"core\"",
|
|
"crates/core/src/lib.rs": "pub fn init() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
const root = server.findRoot(join(dir, "crates/core/src/lib.rs"), dir);
|
|
assertEquals(root, join(dir, "crates/core"), "Should find nearest Cargo.toml");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Go root detection tests (including gopls bug fix verification)
|
|
// ============================================================================
|
|
|
|
test("gopls: finds root with go.mod", async () => {
|
|
await withTempDir({
|
|
"go.mod": "module example.com/myapp",
|
|
"main.go": "package main",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
const root = server.findRoot(join(dir, "main.go"), dir);
|
|
assertEquals(root, dir, "Should find root at go.mod location");
|
|
});
|
|
});
|
|
|
|
test("gopls: finds root with go.work (workspace)", async () => {
|
|
await withTempDir({
|
|
"go.work": "go 1.21\nuse ./app",
|
|
"app/go.mod": "module example.com/app",
|
|
"app/main.go": "package main",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
const root = server.findRoot(join(dir, "app/main.go"), dir);
|
|
assertEquals(root, dir, "Should find root at go.work location (workspace root)");
|
|
});
|
|
});
|
|
|
|
test("gopls: prefers go.work over go.mod", async () => {
|
|
await withTempDir({
|
|
"go.work": "go 1.21\nuse ./app",
|
|
"go.mod": "module example.com/root",
|
|
"app/go.mod": "module example.com/app",
|
|
"app/main.go": "package main",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
const root = server.findRoot(join(dir, "app/main.go"), dir);
|
|
// go.work is found first, so it should return the go.work location
|
|
assertEquals(root, dir, "Should prefer go.work over go.mod");
|
|
});
|
|
});
|
|
|
|
test("gopls: returns undefined when no go.mod or go.work (bug fix verification)", async () => {
|
|
await withTempDir({
|
|
"main.go": "package main",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
const root = server.findRoot(join(dir, "main.go"), dir);
|
|
// This test verifies the bug fix: previously this would return undefined
|
|
// because `undefined !== cwd` was true, skipping the go.mod check
|
|
assertEquals(root, undefined, "Should return undefined when no go.mod or go.work");
|
|
});
|
|
});
|
|
|
|
test("gopls: finds go.mod when go.work not present (bug fix verification)", async () => {
|
|
await withTempDir({
|
|
"go.mod": "module example.com/myapp",
|
|
"cmd/server/main.go": "package main",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
const root = server.findRoot(join(dir, "cmd/server/main.go"), dir);
|
|
// This is the key test for the bug fix
|
|
// Previously: findRoot(go.work) returns undefined, then `undefined !== cwd` is true,
|
|
// so it would return undefined without checking go.mod
|
|
// After fix: if go.work not found, falls through to check go.mod
|
|
assertEquals(root, dir, "Should find go.mod when go.work is not present");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Kotlin root detection tests
|
|
// ============================================================================
|
|
|
|
test("kotlin: finds root with settings.gradle.kts", async () => {
|
|
await withTempDir({
|
|
"settings.gradle.kts": "rootProject.name = \"myapp\"",
|
|
"app/src/main/kotlin/Main.kt": "fun main() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "kotlin")!;
|
|
const root = server.findRoot(join(dir, "app/src/main/kotlin/Main.kt"), dir);
|
|
assertEquals(root, dir, "Should find root at settings.gradle.kts location");
|
|
});
|
|
});
|
|
|
|
test("kotlin: prefers settings.gradle(.kts) over nested build.gradle", async () => {
|
|
await withTempDir({
|
|
"settings.gradle": "rootProject.name = 'root'",
|
|
"app/build.gradle": "plugins {}",
|
|
"app/src/main/kotlin/Main.kt": "fun main() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "kotlin")!;
|
|
const root = server.findRoot(join(dir, "app/src/main/kotlin/Main.kt"), dir);
|
|
assertEquals(root, dir, "Should prefer settings.gradle at workspace root");
|
|
});
|
|
});
|
|
|
|
test("kotlin: finds root with pom.xml", async () => {
|
|
await withTempDir({
|
|
"pom.xml": "<project></project>",
|
|
"src/main/kotlin/Main.kt": "fun main() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "kotlin")!;
|
|
const root = server.findRoot(join(dir, "src/main/kotlin/Main.kt"), dir);
|
|
assertEquals(root, dir, "Should find root at pom.xml location");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Swift root detection tests
|
|
// ============================================================================
|
|
|
|
test("swift: finds root with Package.swift", async () => {
|
|
await withTempDir({
|
|
"Package.swift": "// swift-tools-version: 5.9",
|
|
"Sources/App/main.swift": "print(\"hi\")",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "swift")!;
|
|
const root = server.findRoot(join(dir, "Sources/App/main.swift"), dir);
|
|
assertEquals(root, dir, "Should find root at Package.swift location");
|
|
});
|
|
});
|
|
|
|
test("swift: finds root with Xcode project", async () => {
|
|
await withTempDir({
|
|
"MyApp.xcodeproj/project.pbxproj": "// pbxproj",
|
|
"MyApp/main.swift": "print(\"hi\")",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "swift")!;
|
|
const root = server.findRoot(join(dir, "MyApp/main.swift"), dir);
|
|
assertEquals(root, dir, "Should find root at Xcode project location");
|
|
});
|
|
});
|
|
|
|
test("swift: finds root with Xcode workspace", async () => {
|
|
await withTempDir({
|
|
"MyApp.xcworkspace/contents.xcworkspacedata": "<Workspace/>",
|
|
"MyApp/main.swift": "print(\"hi\")",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "swift")!;
|
|
const root = server.findRoot(join(dir, "MyApp/main.swift"), dir);
|
|
assertEquals(root, dir, "Should find root at Xcode workspace location");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Python root detection tests
|
|
// ============================================================================
|
|
|
|
test("pyright: finds root with pyproject.toml", async () => {
|
|
await withTempDir({
|
|
"pyproject.toml": "[project]\nname = \"myapp\"",
|
|
"src/main.py": "print('hello')",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
const root = server.findRoot(join(dir, "src/main.py"), dir);
|
|
assertEquals(root, dir, "Should find root at pyproject.toml location");
|
|
});
|
|
});
|
|
|
|
test("pyright: finds root with setup.py", async () => {
|
|
await withTempDir({
|
|
"setup.py": "from setuptools import setup",
|
|
"myapp/main.py": "print('hello')",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
const root = server.findRoot(join(dir, "myapp/main.py"), dir);
|
|
assertEquals(root, dir, "Should find root at setup.py location");
|
|
});
|
|
});
|
|
|
|
test("pyright: finds root with requirements.txt", async () => {
|
|
await withTempDir({
|
|
"requirements.txt": "flask>=2.0",
|
|
"app.py": "from flask import Flask",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
const root = server.findRoot(join(dir, "app.py"), dir);
|
|
assertEquals(root, dir, "Should find root at requirements.txt location");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// URI construction tests (pathToFileURL)
|
|
// ============================================================================
|
|
|
|
test("pathToFileURL: handles simple paths", async () => {
|
|
const uri = pathToFileURL("/home/user/project/file.ts").href;
|
|
assertEquals(uri, "file:///home/user/project/file.ts", "Should create proper file URI");
|
|
});
|
|
|
|
test("pathToFileURL: encodes special characters", async () => {
|
|
const uri = pathToFileURL("/home/user/my project/file.ts").href;
|
|
assert(uri.includes("my%20project"), "Should URL-encode spaces");
|
|
});
|
|
|
|
test("pathToFileURL: handles unicode", async () => {
|
|
const uri = pathToFileURL("/home/user/项目/file.ts").href;
|
|
// pathToFileURL properly encodes unicode
|
|
assert(uri.startsWith("file:///"), "Should start with file:///");
|
|
assert(uri.includes("file.ts"), "Should contain filename");
|
|
});
|
|
|
|
// ============================================================================
|
|
// Vue/Svelte root detection tests
|
|
// ============================================================================
|
|
|
|
test("vue: finds root with package.json", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"src/App.vue": "<template></template>",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "vue")!;
|
|
const root = server.findRoot(join(dir, "src/App.vue"), dir);
|
|
assertEquals(root, dir, "Should find root at package.json location");
|
|
});
|
|
});
|
|
|
|
test("vue: finds root with vite.config.ts", async () => {
|
|
await withTempDir({
|
|
"vite.config.ts": "export default {}",
|
|
"src/App.vue": "<template></template>",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "vue")!;
|
|
const root = server.findRoot(join(dir, "src/App.vue"), dir);
|
|
assertEquals(root, dir, "Should find root at vite.config.ts location");
|
|
});
|
|
});
|
|
|
|
test("svelte: finds root with svelte.config.js", async () => {
|
|
await withTempDir({
|
|
"svelte.config.js": "export default {}",
|
|
"src/App.svelte": "<script></script>",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "svelte")!;
|
|
const root = server.findRoot(join(dir, "src/App.svelte"), dir);
|
|
assertEquals(root, dir, "Should find root at svelte.config.js location");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Additional Rust tests (parity with TypeScript)
|
|
// ============================================================================
|
|
|
|
test("rust: finds root in src subdirectory", async () => {
|
|
await withTempDir({
|
|
"Cargo.toml": "[package]\nname = \"myapp\"",
|
|
"src/main.rs": "fn main() {}",
|
|
"src/lib.rs": "pub mod utils;",
|
|
"src/utils/mod.rs": "pub fn helper() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
const root = server.findRoot(join(dir, "src/utils/mod.rs"), dir);
|
|
assertEquals(root, dir, "Should find root from deeply nested src file");
|
|
});
|
|
});
|
|
|
|
test("rust: workspace with multiple crates", async () => {
|
|
await withTempDir({
|
|
"Cargo.toml": "[workspace]\nmembers = [\"crates/*\"]",
|
|
"crates/api/Cargo.toml": "[package]\nname = \"api\"",
|
|
"crates/api/src/lib.rs": "pub fn serve() {}",
|
|
"crates/core/Cargo.toml": "[package]\nname = \"core\"",
|
|
"crates/core/src/lib.rs": "pub fn init() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
// Each crate should find its own Cargo.toml
|
|
const apiRoot = server.findRoot(join(dir, "crates/api/src/lib.rs"), dir);
|
|
const coreRoot = server.findRoot(join(dir, "crates/core/src/lib.rs"), dir);
|
|
assertEquals(apiRoot, join(dir, "crates/api"), "API crate should find its Cargo.toml");
|
|
assertEquals(coreRoot, join(dir, "crates/core"), "Core crate should find its Cargo.toml");
|
|
});
|
|
});
|
|
|
|
test("rust: returns undefined when no Cargo.toml", async () => {
|
|
await withTempDir({
|
|
"main.rs": "fn main() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
const root = server.findRoot(join(dir, "main.rs"), dir);
|
|
assertEquals(root, undefined, "Should return undefined when no Cargo.toml");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Additional Dart tests (parity with TypeScript)
|
|
// ============================================================================
|
|
|
|
test("dart: Flutter project with pubspec.yaml", async () => {
|
|
await withTempDir({
|
|
"pubspec.yaml": "name: my_flutter_app\ndependencies:\n flutter:\n sdk: flutter",
|
|
"lib/main.dart": "import 'package:flutter/material.dart';",
|
|
"lib/screens/home.dart": "class HomeScreen {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
const root = server.findRoot(join(dir, "lib/screens/home.dart"), dir);
|
|
assertEquals(root, dir, "Should find root for Flutter project");
|
|
});
|
|
});
|
|
|
|
test("dart: returns undefined when no marker files", async () => {
|
|
await withTempDir({
|
|
"main.dart": "void main() {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
const root = server.findRoot(join(dir, "main.dart"), dir);
|
|
assertEquals(root, undefined, "Should return undefined when no pubspec.yaml or analysis_options.yaml");
|
|
});
|
|
});
|
|
|
|
test("dart: monorepo with multiple packages", async () => {
|
|
await withTempDir({
|
|
"pubspec.yaml": "name: monorepo",
|
|
"packages/auth/pubspec.yaml": "name: auth",
|
|
"packages/auth/lib/auth.dart": "class Auth {}",
|
|
"packages/ui/pubspec.yaml": "name: ui",
|
|
"packages/ui/lib/widgets.dart": "class Button {}",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
const authRoot = server.findRoot(join(dir, "packages/auth/lib/auth.dart"), dir);
|
|
const uiRoot = server.findRoot(join(dir, "packages/ui/lib/widgets.dart"), dir);
|
|
assertEquals(authRoot, join(dir, "packages/auth"), "Auth package should find its pubspec");
|
|
assertEquals(uiRoot, join(dir, "packages/ui"), "UI package should find its pubspec");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Additional Python tests (parity with TypeScript)
|
|
// ============================================================================
|
|
|
|
test("pyright: finds root with pyrightconfig.json", async () => {
|
|
await withTempDir({
|
|
"pyrightconfig.json": "{}",
|
|
"src/app.py": "print('hello')",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
const root = server.findRoot(join(dir, "src/app.py"), dir);
|
|
assertEquals(root, dir, "Should find root at pyrightconfig.json location");
|
|
});
|
|
});
|
|
|
|
test("pyright: returns undefined when no marker files", async () => {
|
|
await withTempDir({
|
|
"script.py": "print('hello')",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
const root = server.findRoot(join(dir, "script.py"), dir);
|
|
assertEquals(root, undefined, "Should return undefined when no Python project markers");
|
|
});
|
|
});
|
|
|
|
test("pyright: monorepo with multiple packages", async () => {
|
|
await withTempDir({
|
|
"pyproject.toml": "[project]\nname = \"monorepo\"",
|
|
"packages/api/pyproject.toml": "[project]\nname = \"api\"",
|
|
"packages/api/src/main.py": "from flask import Flask",
|
|
"packages/worker/pyproject.toml": "[project]\nname = \"worker\"",
|
|
"packages/worker/src/tasks.py": "def process(): pass",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
const apiRoot = server.findRoot(join(dir, "packages/api/src/main.py"), dir);
|
|
const workerRoot = server.findRoot(join(dir, "packages/worker/src/tasks.py"), dir);
|
|
assertEquals(apiRoot, join(dir, "packages/api"), "API package should find its pyproject.toml");
|
|
assertEquals(workerRoot, join(dir, "packages/worker"), "Worker package should find its pyproject.toml");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Additional Go tests
|
|
// ============================================================================
|
|
|
|
test("gopls: monorepo with multiple modules", async () => {
|
|
await withTempDir({
|
|
"go.work": "go 1.21\nuse (\n ./api\n ./worker\n)",
|
|
"api/go.mod": "module example.com/api",
|
|
"api/main.go": "package main",
|
|
"worker/go.mod": "module example.com/worker",
|
|
"worker/main.go": "package main",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
// With go.work present, all files should use workspace root
|
|
const apiRoot = server.findRoot(join(dir, "api/main.go"), dir);
|
|
const workerRoot = server.findRoot(join(dir, "worker/main.go"), dir);
|
|
assertEquals(apiRoot, dir, "API module should use go.work root");
|
|
assertEquals(workerRoot, dir, "Worker module should use go.work root");
|
|
});
|
|
});
|
|
|
|
test("gopls: nested cmd directory", async () => {
|
|
await withTempDir({
|
|
"go.mod": "module example.com/myapp",
|
|
"cmd/server/main.go": "package main",
|
|
"cmd/cli/main.go": "package main",
|
|
"internal/db/db.go": "package db",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
const serverRoot = server.findRoot(join(dir, "cmd/server/main.go"), dir);
|
|
const cliRoot = server.findRoot(join(dir, "cmd/cli/main.go"), dir);
|
|
const dbRoot = server.findRoot(join(dir, "internal/db/db.go"), dir);
|
|
assertEquals(serverRoot, dir, "cmd/server should find go.mod at root");
|
|
assertEquals(cliRoot, dir, "cmd/cli should find go.mod at root");
|
|
assertEquals(dbRoot, dir, "internal/db should find go.mod at root");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Additional TypeScript tests
|
|
// ============================================================================
|
|
|
|
test("typescript: pnpm workspace", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"pnpm-workspace.yaml": "packages:\n - packages/*",
|
|
"packages/web/package.json": "{}",
|
|
"packages/web/src/App.tsx": "export const App = () => null;",
|
|
"packages/api/package.json": "{}",
|
|
"packages/api/src/index.ts": "export const handler = () => {};",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const webRoot = server.findRoot(join(dir, "packages/web/src/App.tsx"), dir);
|
|
const apiRoot = server.findRoot(join(dir, "packages/api/src/index.ts"), dir);
|
|
assertEquals(webRoot, join(dir, "packages/web"), "Web package should find its package.json");
|
|
assertEquals(apiRoot, join(dir, "packages/api"), "API package should find its package.json");
|
|
});
|
|
});
|
|
|
|
test("typescript: returns undefined when no config files", async () => {
|
|
await withTempDir({
|
|
"script.ts": "const x = 1;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "script.ts"), dir);
|
|
assertEquals(root, undefined, "Should return undefined when no package.json or tsconfig.json");
|
|
});
|
|
});
|
|
|
|
test("typescript: prefers nearest tsconfig over package.json", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"apps/web/tsconfig.json": "{}",
|
|
"apps/web/src/index.ts": "export const x = 1;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "apps/web/src/index.ts"), dir);
|
|
// Should find tsconfig.json first (it's nearer than root package.json)
|
|
assertEquals(root, join(dir, "apps/web"), "Should find nearest config file");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Additional Vue/Svelte tests
|
|
// ============================================================================
|
|
|
|
test("vue: Nuxt project", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"nuxt.config.ts": "export default {}",
|
|
"pages/index.vue": "<template></template>",
|
|
"components/Button.vue": "<template></template>",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "vue")!;
|
|
const pagesRoot = server.findRoot(join(dir, "pages/index.vue"), dir);
|
|
const componentsRoot = server.findRoot(join(dir, "components/Button.vue"), dir);
|
|
assertEquals(pagesRoot, dir, "Pages should find root");
|
|
assertEquals(componentsRoot, dir, "Components should find root");
|
|
});
|
|
});
|
|
|
|
test("vue: returns undefined when no config", async () => {
|
|
await withTempDir({
|
|
"App.vue": "<template></template>",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "vue")!;
|
|
const root = server.findRoot(join(dir, "App.vue"), dir);
|
|
assertEquals(root, undefined, "Should return undefined when no package.json or vite.config");
|
|
});
|
|
});
|
|
|
|
test("svelte: SvelteKit project", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"svelte.config.js": "export default {}",
|
|
"src/routes/+page.svelte": "<script></script>",
|
|
"src/lib/components/Button.svelte": "<script></script>",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "svelte")!;
|
|
const routeRoot = server.findRoot(join(dir, "src/routes/+page.svelte"), dir);
|
|
const libRoot = server.findRoot(join(dir, "src/lib/components/Button.svelte"), dir);
|
|
assertEquals(routeRoot, dir, "Route should find root");
|
|
assertEquals(libRoot, dir, "Lib component should find root");
|
|
});
|
|
});
|
|
|
|
test("svelte: returns undefined when no config", async () => {
|
|
await withTempDir({
|
|
"App.svelte": "<script></script>",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "svelte")!;
|
|
const root = server.findRoot(join(dir, "App.svelte"), dir);
|
|
assertEquals(root, undefined, "Should return undefined when no package.json or svelte.config.js");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Stop boundary tests (findNearestFile respects cwd boundary)
|
|
// ============================================================================
|
|
|
|
test("stop boundary: does not search above cwd", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}", // This is at root
|
|
"projects/myapp/src/index.ts": "export const x = 1;",
|
|
// Note: no package.json in projects/myapp
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
// When cwd is set to projects/myapp, it should NOT find the root package.json
|
|
const projectDir = join(dir, "projects/myapp");
|
|
const root = server.findRoot(join(projectDir, "src/index.ts"), projectDir);
|
|
assertEquals(root, undefined, "Should not find package.json above cwd boundary");
|
|
});
|
|
});
|
|
|
|
test("stop boundary: finds marker at cwd level", async () => {
|
|
await withTempDir({
|
|
"projects/myapp/package.json": "{}",
|
|
"projects/myapp/src/index.ts": "export const x = 1;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const projectDir = join(dir, "projects/myapp");
|
|
const root = server.findRoot(join(projectDir, "src/index.ts"), projectDir);
|
|
assertEquals(root, projectDir, "Should find package.json at cwd level");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Edge cases
|
|
// ============================================================================
|
|
|
|
test("edge: deeply nested file finds correct root", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"src/components/ui/buttons/primary/Button.tsx": "export const Button = () => null;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "src/components/ui/buttons/primary/Button.tsx"), dir);
|
|
assertEquals(root, dir, "Should find root even for deeply nested files");
|
|
});
|
|
});
|
|
|
|
test("edge: file at root level finds root", async () => {
|
|
await withTempDir({
|
|
"package.json": "{}",
|
|
"index.ts": "console.log('root');",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "index.ts"), dir);
|
|
assertEquals(root, dir, "Should find root for file at root level");
|
|
});
|
|
});
|
|
|
|
test("edge: no marker files returns undefined", async () => {
|
|
await withTempDir({
|
|
"random.ts": "const x = 1;",
|
|
}, async (dir) => {
|
|
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
const root = server.findRoot(join(dir, "random.ts"), dir);
|
|
assertEquals(root, undefined, "Should return undefined when no marker files");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Run tests
|
|
// ============================================================================
|
|
|
|
async function runTests(): Promise<void> {
|
|
console.log("Running LSP tests...\n");
|
|
|
|
const results: TestResult[] = [];
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
for (const { name, fn } of tests) {
|
|
try {
|
|
await fn();
|
|
results.push({ name, passed: true });
|
|
console.log(` ${name}... ✓`);
|
|
passed++;
|
|
} catch (error) {
|
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
results.push({ name, passed: false, error: errorMsg });
|
|
console.log(` ${name}... ✗`);
|
|
console.log(` Error: ${errorMsg}\n`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
|
|
if (failed > 0) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
runTests();
|