diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7162d2f..f33d89f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,12 +49,22 @@ jobs: - name: Install dependencies run: bun install - - name: Test with coverage - run: bun test --coverage --coverage-reporter=lcov + - name: Run tests + run: bun run test + + - name: Generate coverage reports + run: | + cd packages/lsp && bun test --coverage --coverage-reporter=lcov + cd ../code && bun test --coverage --coverage-reporter=lcov || true + cd ../dora && bun test --coverage --coverage-reporter=lcov || true + continue-on-error: true - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/lcov.info + files: | + ./packages/lsp/coverage/lcov.info + ./packages/code/coverage/lcov.info + ./packages/dora/coverage/lcov.info fail_ci_if_error: false diff --git a/CLAUDE.md b/CLAUDE.md index de86154..3c88175 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,6 +125,7 @@ Claude/MCP Client <-> StdioTransport <-> McpServer <-> Providers | Go | gopls | go.mod, go.work | | Rust | rust-analyzer | Cargo.toml | | Kotlin | JetBrains Kotlin LSP (auto-download) | build.gradle.kts, build.gradle, pom.xml | +| Dart | dart language-server (auto-download) | pubspec.yaml, pubspec.lock | ### Built-in Formatters diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..70a5163 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,36 @@ +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: auto + +component_management: + default_rules: + statuses: + - type: project + target: auto + + individual_components: + - component_id: code + name: '@pleaseai/code' + paths: + - packages/code/src/** + + - component_id: code-format + name: '@pleaseai/code-format' + paths: + - packages/format/src/** + + - component_id: code-lsp + name: '@pleaseai/code-lsp' + paths: + - packages/lsp/src/** + + - component_id: dora + name: '@pleaseai/dora' + paths: + - packages/dora/src/** diff --git a/packages/lsp/package.json b/packages/lsp/package.json index 21574d8..f40927e 100644 --- a/packages/lsp/package.json +++ b/packages/lsp/package.json @@ -25,7 +25,9 @@ ], "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", - "test": "bun test ./src" + "test": "bun test ./test", + "test:unit": "bun test ./test/unit", + "test:integration": "bun test ./test/integration" }, "dependencies": { "vscode-jsonrpc": "^8.2.1", diff --git a/packages/lsp/src/index.ts b/packages/lsp/src/index.ts index a72c471..f42b23b 100644 --- a/packages/lsp/src/index.ts +++ b/packages/lsp/src/index.ts @@ -392,6 +392,7 @@ export { createLSPClient, type LSPClientInfo } from './client' // Re-export specific items to avoid conflicts export { getLanguageId, LANGUAGE_EXTENSIONS } from './language' export { + DartServer, DenoServer, getServerById, getServersForExtension, diff --git a/packages/lsp/src/server.ts b/packages/lsp/src/server.ts index 1f89ce4..a3c418f 100644 --- a/packages/lsp/src/server.ts +++ b/packages/lsp/src/server.ts @@ -130,6 +130,32 @@ async function downloadAndExtract(url: string, destDir: string): Promise { } } +// ============================================================================= +// Process Lifecycle Utilities +// ============================================================================= + +/** + * Attach error and exit event handlers to an LSP process + * Centralizes logging for process lifecycle events + */ +function attachLSPProcessHandlers( + proc: ChildProcessWithoutNullStreams, + serverId: string, +): void { + proc.on('error', (err) => { + console.error(`[${serverId}] LSP process error:`, err) + }) + + proc.on('exit', (code, signal) => { + if (code !== 0 && code !== null) { + console.error(`[${serverId}] LSP exited with code ${code}`) + } + if (signal) { + console.error(`[${serverId}] LSP killed by signal ${signal}`) + } + }) +} + // ============================================================================= // Root Detection Utilities // ============================================================================= @@ -651,24 +677,173 @@ export const KotlinServer: LSPServerInfo = { }, }) - // Log spawn errors - proc.on('error', (err) => { - console.error(`[kotlin] Kotlin LSP process error:`, err) - }) + attachLSPProcessHandlers(proc, 'kotlin') + return { process: proc } + } + catch (err) { + console.error(`[kotlin] Failed to spawn Kotlin LSP:`, err) + return undefined + } + }, +} + +// ============================================================================= +// Dart Language Server +// ============================================================================= - proc.on('exit', (code, signal) => { - if (code !== 0 && code !== null) { - console.error(`[kotlin] Kotlin LSP exited with code ${code}`) +/** + * Dart SDK runtime dependency configuration + * Uses official Dart SDK with built-in language server + */ +const DART_RUNTIME_DEPS = { + version: '3.7.1', + platforms: { + 'win-x64': { + url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip', + binaryPath: 'dart-sdk/bin/dart.exe', + }, + 'linux-x64': { + url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip', + binaryPath: 'dart-sdk/bin/dart', + }, + 'linux-arm64': { + url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-arm64-release.zip', + binaryPath: 'dart-sdk/bin/dart', + }, + 'osx-x64': { + url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip', + binaryPath: 'dart-sdk/bin/dart', + }, + 'osx-arm64': { + url: 'https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip', + binaryPath: 'dart-sdk/bin/dart', + }, + } as Record, +} + +/** + * Get the Dart LSP resources directory + */ +function getDartResourcesDir(): string { + return path.join(os.homedir(), '.cache', 'dora', 'dart-lsp') +} + +/** + * Setup Dart runtime dependencies + * Downloads Dart SDK if not available and dart is not in PATH + * + * Unlike setupKotlinDependencies which returns { javaHomePath, kotlinLspPath }, + * this returns only the binary path because: + * - Dart SDK is self-contained (no separate JRE dependency) + * - No environment variables (like JAVA_HOME) required for execution + */ +async function setupDartDependencies(platformId: PlatformId): Promise { + const config = DART_RUNTIME_DEPS.platforms[platformId] + + if (!config) { + console.warn(`[dart] Unsupported platform: ${platformId}`) + return undefined + } + + const resourcesDir = getDartResourcesDir() + const dartPath = path.join(resourcesDir, config.binaryPath) + + try { + await fs.access(dartPath) + return dartPath + } + catch (err) { + const isNotFound = err instanceof Error + && 'code' in err + && (err as NodeJS.ErrnoException).code === 'ENOENT' + + if (!isNotFound) { + console.error(`[dart] Cannot access Dart at ${dartPath}:`, err instanceof Error ? err.message : err) + return undefined + } + + // Dart not found, download it + console.warn(`[dart] Downloading Dart SDK ${DART_RUNTIME_DEPS.version} for ${platformId}...`) + try { + await downloadAndExtract(config.url, resourcesDir) + + // Make dart executable on Unix platforms + if (!platformId.startsWith('win-')) { + try { + await fs.chmod(dartPath, 0o755) } - if (signal) { - console.error(`[kotlin] Kotlin LSP killed by signal ${signal}`) + catch (chmodErr) { + console.error(`[dart] Failed to make Dart executable at ${dartPath}:`, chmodErr instanceof Error ? chmodErr.message : chmodErr) + return undefined } + } + } + catch (downloadErr) { + console.error(`[dart] Failed to download Dart SDK:`, downloadErr) + return undefined + } + } + + // Verify Dart exists + try { + await fs.access(dartPath) + return dartPath + } + catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err) + console.error(`[dart] Dart executable not accessible at ${dartPath}: ${errorMsg}`) + return undefined + } +} + +/** + * Dart Language Server + * Uses official Dart SDK with system-first fallback and auto-download + */ +export const DartServer: LSPServerInfo = { + id: 'dart', + extensions: ['.dart'], + root: nearestRoot(['pubspec.yaml', 'pubspec.lock']), + async spawn(root) { + // Try system dart first + const systemDart = Bun.which('dart') + if (systemDart) { + try { + const proc = spawn(systemDart, ['language-server', '--client-id', 'dora.dart', '--client-version', '1.0'], { + cwd: root, + }) + + attachLSPProcessHandlers(proc, 'dart') + return { process: proc } + } + catch (err) { + console.warn(`[dart] Failed to spawn system Dart LSP, trying auto-download:`, err) + } + } + + // Fallback to auto-download + const platformId = getPlatformId() + if (!platformId) { + console.warn(`[dart] Unsupported platform: ${process.platform}-${process.arch}`) + return undefined + } + + const dartPath = await setupDartDependencies(platformId) + if (!dartPath) { + console.warn(`[dart] Failed to setup Dart SDK. Check previous logs for details.`) + return undefined + } + + try { + const proc = spawn(dartPath, ['language-server', '--client-id', 'dora.dart', '--client-version', '1.0'], { + cwd: root, }) + attachLSPProcessHandlers(proc, 'dart') return { process: proc } } catch (err) { - console.error(`[kotlin] Failed to spawn Kotlin LSP:`, err) + console.error(`[dart] Failed to spawn Dart LSP:`, err) return undefined } }, @@ -685,6 +860,7 @@ export const LSP_SERVERS: LSPServerInfo[] = [ GoplsServer, RustAnalyzerServer, KotlinServer, + DartServer, ] /** diff --git a/packages/lsp/src/__tests__/fixture/fake-lsp-server.js b/packages/lsp/test/fixture/fake-lsp-server.js similarity index 100% rename from packages/lsp/src/__tests__/fixture/fake-lsp-server.js rename to packages/lsp/test/fixture/fake-lsp-server.js diff --git a/packages/lsp/test/fixtures/dart-project/lib/helper.dart b/packages/lsp/test/fixtures/dart-project/lib/helper.dart new file mode 100644 index 0000000..65c0986 --- /dev/null +++ b/packages/lsp/test/fixtures/dart-project/lib/helper.dart @@ -0,0 +1,17 @@ +/// Standalone subtract function +int subtract(int a, int b) { + return a - b; +} + +/// Divides two numbers with error handling +double divide(double a, double b) { + if (b == 0) { + throw ArgumentError('Cannot divide by zero'); + } + return a / b; +} + +/// Formats a number as currency +String formatCurrency(double amount, {String symbol = '\$'}) { + return '$symbol${amount.toStringAsFixed(2)}'; +} diff --git a/packages/lsp/test/fixtures/dart-project/lib/main.dart b/packages/lsp/test/fixtures/dart-project/lib/main.dart new file mode 100644 index 0000000..288411b --- /dev/null +++ b/packages/lsp/test/fixtures/dart-project/lib/main.dart @@ -0,0 +1,56 @@ +/// Calculator class for basic arithmetic operations +class Calculator { + /// Adds two numbers + int add(int a, int b) { + final result = a + b; + return result; + } + + /// Subtracts two numbers + int subtract(int a, int b) { + return a - b; + } + + /// Multiplies two numbers + int multiply(int a, int b) { + return a * b; + } +} + +/// Helper class with static methods +class MathHelper { + /// Calculates power + static double power(double base, int exponent) { + double result = 1; + for (int i = 0; i < exponent; i++) { + result *= base; + } + return result; + } + + /// Calculates absolute value + static double abs(double value) { + return value < 0 ? -value : value; + } +} + +/// Main entry point +void main() { + final calc = Calculator(); + + // Test arithmetic operations + final sum = calc.add(5, 3); + final diff = calc.subtract(10, 4); + final product = calc.multiply(6, 7); + + print('Sum: $sum'); + print('Difference: $diff'); + print('Product: $product'); + + // Test helper methods + final squared = MathHelper.power(2.0, 3); + final absolute = MathHelper.abs(-42.5); + + print('2^3 = $squared'); + print('|-42.5| = $absolute'); +} diff --git a/packages/lsp/test/fixtures/dart-project/pubspec.yaml b/packages/lsp/test/fixtures/dart-project/pubspec.yaml new file mode 100644 index 0000000..08857bf --- /dev/null +++ b/packages/lsp/test/fixtures/dart-project/pubspec.yaml @@ -0,0 +1,6 @@ +name: test_dart_app +description: Test Dart project for LSP integration tests +version: 1.0.0 + +environment: + sdk: ^3.0.0 diff --git a/packages/lsp/test/integration/dart.integration.test.ts b/packages/lsp/test/integration/dart.integration.test.ts new file mode 100644 index 0000000..4cc6ec0 --- /dev/null +++ b/packages/lsp/test/integration/dart.integration.test.ts @@ -0,0 +1,128 @@ +/** + * Dart Language Server Integration Tests + * + * These tests spawn an actual Dart language server and verify LSP functionality. + * Requires Dart SDK to be installed or will auto-download. + * + * Based on serena test patterns: ref/serena/test/solidlsp/dart/test_dart_basic.py + */ + +import path from 'node:path' +import { afterAll, beforeAll, describe, expect, test } from 'bun:test' +import { LSPManager } from '../../src/index' + +const DART_PROJECT_PATH = path.join(import.meta.dir, '../fixtures/dart-project') +const MAIN_DART_PATH = path.join(DART_PROJECT_PATH, 'lib/main.dart') +const HELPER_DART_PATH = path.join(DART_PROJECT_PATH, 'lib/helper.dart') + +// Check if Dart is available +const isDartAvailable = Bun.which('dart') !== null + +describe.skipIf(!isDartAvailable)('DartServer Integration', () => { + let manager: LSPManager + + beforeAll(async () => { + manager = new LSPManager(DART_PROJECT_PATH) + // Touch the main file to initialize LSP and wait for diagnostics + await manager.touchFile(MAIN_DART_PATH, true) + }, 60000) // 60s timeout for server startup + + afterAll(async () => { + await manager.shutdown() + }) + + test('LSP server starts and connects', async () => { + const status = await manager.status() + expect(status.length).toBeGreaterThan(0) + + const dartStatus = status.find(s => s.id === 'dart') + expect(dartStatus).toBeDefined() + expect(dartStatus?.status).toBe('connected') + }) + + test('returns diagnostics for Dart file', async () => { + // The test file is valid, so we expect no errors + const diagnostics = await manager.diagnostics() + const mainDartDiags = diagnostics[MAIN_DART_PATH] || [] + + // Valid file should have no errors (might have warnings/hints) + const errors = mainDartDiags.filter(d => d.severity === 1) + expect(errors.length).toBe(0) + }) + + test('provides hover information', async () => { + // Hover over 'Calculator' class name (line 1, char 6) + const hovers = await manager.hover({ + file: MAIN_DART_PATH, + line: 1, // class Calculator { + character: 6, + }) + + expect(hovers.length).toBeGreaterThan(0) + // At least one hover response should be non-null + const validHovers = hovers.filter(h => h !== null) + expect(validHovers.length).toBeGreaterThan(0) + }) + + test('finds workspace symbols', async () => { + const symbols = await manager.workspaceSymbol('Calculator') + + expect(symbols.length).toBeGreaterThan(0) + + // Should find the Calculator class + const calculatorSymbol = symbols.find(s => s.name === 'Calculator') + expect(calculatorSymbol).toBeDefined() + }) + + test('finds document symbols', async () => { + const uri = `file://${MAIN_DART_PATH}` + const symbols = await manager.documentSymbol(uri) + + expect(symbols.length).toBeGreaterThan(0) + + // Should find classes and methods + const symbolNames = symbols.map(s => s.name) + expect(symbolNames).toContain('Calculator') + }) + + test('touches multiple Dart files', async () => { + // Touch helper file + await manager.touchFile(HELPER_DART_PATH, true) + + const diagnostics = await manager.diagnostics() + const helperDiags = diagnostics[HELPER_DART_PATH] || [] + + // Valid file should have no errors + const errors = helperDiags.filter(d => d.severity === 1) + expect(errors.length).toBe(0) + }) + + test('finds symbols across files', async () => { + // Search for subtract function defined in helper.dart + const symbols = await manager.workspaceSymbol('subtract') + + // Should find it in helper.dart + expect(symbols.length).toBeGreaterThan(0) + const subtractSymbol = symbols.find(s => s.name === 'subtract') + expect(subtractSymbol).toBeDefined() + }) +}) + +// Separate describe block for tests that don't require Dart +describe('DartServer Integration (no Dart required)', () => { + test('fixture files exist', async () => { + const mainDart = Bun.file(MAIN_DART_PATH) + expect(await mainDart.exists()).toBe(true) + + const helperDart = Bun.file(HELPER_DART_PATH) + expect(await helperDart.exists()).toBe(true) + + const pubspec = Bun.file(path.join(DART_PROJECT_PATH, 'pubspec.yaml')) + expect(await pubspec.exists()).toBe(true) + }) + + test('manager creates without Dart installed', () => { + const manager = new LSPManager(DART_PROJECT_PATH) + expect(manager).toBeDefined() + }) +}) diff --git a/packages/lsp/src/__tests__/client.test.ts b/packages/lsp/test/unit/client.test.ts similarity index 96% rename from packages/lsp/src/__tests__/client.test.ts rename to packages/lsp/test/unit/client.test.ts index dff7494..2434729 100644 --- a/packages/lsp/src/__tests__/client.test.ts +++ b/packages/lsp/test/unit/client.test.ts @@ -1,11 +1,11 @@ -import type { LSPClientInfo } from '../client' +import type { LSPClientInfo } from '../../src/client' import { spawn } from 'node:child_process' import path from 'node:path' import { afterEach, describe, expect, test } from 'bun:test' -import { createLSPClient } from '../client' +import { createLSPClient } from '../../src/client' function spawnFakeServer() { - const serverPath = path.join(__dirname, 'fixture/fake-lsp-server.js') + const serverPath = path.join(import.meta.dir, '../fixture/fake-lsp-server.js') return { process: spawn(process.execPath, [serverPath], { stdio: 'pipe', diff --git a/packages/lsp/src/__tests__/index.test.ts b/packages/lsp/test/unit/index.test.ts similarity index 99% rename from packages/lsp/src/__tests__/index.test.ts rename to packages/lsp/test/unit/index.test.ts index 18c5b28..ecd4d65 100644 --- a/packages/lsp/src/__tests__/index.test.ts +++ b/packages/lsp/test/unit/index.test.ts @@ -6,7 +6,7 @@ import { LANGUAGE_EXTENSIONS, LSPManager, SymbolKind, -} from '../index' +} from '../../src/index' describe('LSPManager', () => { test('creates manager with project path', () => { diff --git a/packages/lsp/src/__tests__/server.test.ts b/packages/lsp/test/unit/server.test.ts similarity index 53% rename from packages/lsp/src/__tests__/server.test.ts rename to packages/lsp/test/unit/server.test.ts index 81d6d2e..c29e99c 100644 --- a/packages/lsp/src/__tests__/server.test.ts +++ b/packages/lsp/test/unit/server.test.ts @@ -1,5 +1,9 @@ +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' import { describe, expect, test } from 'bun:test' import { + DartServer, DenoServer, getServerById, getServersForExtension, @@ -10,7 +14,7 @@ import { PyrightServer, RustAnalyzerServer, TypescriptServer, -} from '../server' +} from '../../src/server' describe('LSP_SERVERS', () => { test('contains expected servers', () => { @@ -24,6 +28,7 @@ describe('LSP_SERVERS', () => { expect(serverIds).toContain('gopls') expect(serverIds).toContain('rust-analyzer') expect(serverIds).toContain('kotlin') + expect(serverIds).toContain('dart') }) }) @@ -141,6 +146,142 @@ describe('KotlinServer', () => { }) }) +describe('DartServer', () => { + test('has correct id', () => { + expect(DartServer.id).toBe('dart') + }) + + test('supports Dart extension', () => { + expect(DartServer.extensions).toContain('.dart') + }) + + test('has root function', () => { + expect(typeof DartServer.root).toBe('function') + }) + + test('has spawn function', () => { + expect(typeof DartServer.spawn).toBe('function') + }) + + test('root function detects pubspec.yaml', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-test-')) + try { + // Create pubspec.yaml + await fs.writeFile(path.join(tempDir, 'pubspec.yaml'), 'name: test_app\n') + + // Create a nested source file + const libDir = path.join(tempDir, 'lib') + await fs.mkdir(libDir) + const dartFile = path.join(libDir, 'main.dart') + await fs.writeFile(dartFile, '// test') + + const root = await DartServer.root(dartFile, tempDir) + expect(root).toBe(tempDir) + } + finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } + }) + + test('root function detects pubspec.lock', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-test-')) + try { + // Create pubspec.lock only + await fs.writeFile(path.join(tempDir, 'pubspec.lock'), 'packages:\n') + + const dartFile = path.join(tempDir, 'main.dart') + await fs.writeFile(dartFile, '// test') + + const root = await DartServer.root(dartFile, tempDir) + expect(root).toBe(tempDir) + } + finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } + }) + + test('root function returns projectPath when no pubspec found', async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-test-')) + try { + // No pubspec files + const dartFile = path.join(tempDir, 'main.dart') + await fs.writeFile(dartFile, '// test') + + const root = await DartServer.root(dartFile, tempDir) + expect(root).toBe(tempDir) + } + finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } + }) + + test('spawn function returns promise', () => { + // Verify spawn returns a promise (don't actually call it to avoid downloads) + const spawnFn = DartServer.spawn + expect(typeof spawnFn).toBe('function') + // Verify it's an async function by checking the constructor name + expect(spawnFn.constructor.name).toBe('AsyncFunction') + }) + + test('root function detects nested monorepo package', async () => { + // Simulates a monorepo with nested Dart packages + // root/ + // pubspec.yaml (root package) + // packages/ + // inner_app/ + // pubspec.yaml (inner package) + // lib/ + // main.dart + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-monorepo-')) + try { + // Create root pubspec.yaml + await fs.writeFile(path.join(tempDir, 'pubspec.yaml'), 'name: root_app\n') + + // Create nested package structure + const innerPkgDir = path.join(tempDir, 'packages', 'inner_app') + await fs.mkdir(innerPkgDir, { recursive: true }) + await fs.writeFile(path.join(innerPkgDir, 'pubspec.yaml'), 'name: inner_app\n') + + const libDir = path.join(innerPkgDir, 'lib') + await fs.mkdir(libDir) + const dartFile = path.join(libDir, 'main.dart') + await fs.writeFile(dartFile, '// inner package code') + + // Should find the inner package's pubspec.yaml, not root + const root = await DartServer.root(dartFile, tempDir) + expect(root).toBe(innerPkgDir) + } + finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } + }) + + test('root function finds nearest pubspec in deep nesting', async () => { + // Simulates deep directory structure + // root/ + // pubspec.yaml + // src/ + // features/ + // auth/ + // login.dart + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dart-deep-')) + try { + await fs.writeFile(path.join(tempDir, 'pubspec.yaml'), 'name: deep_app\n') + + const deepDir = path.join(tempDir, 'src', 'features', 'auth') + await fs.mkdir(deepDir, { recursive: true }) + const dartFile = path.join(deepDir, 'login.dart') + await fs.writeFile(dartFile, '// login feature') + + const root = await DartServer.root(dartFile, tempDir) + expect(root).toBe(tempDir) + } + finally { + await fs.rm(tempDir, { recursive: true, force: true }) + } + }) +}) + describe('getServerById', () => { test('returns typescript server', () => { const server = getServerById('typescript') @@ -195,6 +336,14 @@ describe('getServersForExtension', () => { expect(serverIds).toContain('kotlin') }) + test('returns servers for .dart extension', () => { + const servers = getServersForExtension('.dart') + expect(servers.length).toBeGreaterThan(0) + + const serverIds = servers.map(s => s.id) + expect(serverIds).toContain('dart') + }) + test('returns empty array for unknown extension', () => { const servers = getServersForExtension('.unknown') expect(servers).toEqual([])