// Integration tests against real language servers (skipped when not installed). import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { ServerPool, which } from '../lib/servers.js' import { columnOf, applyTextEdits, formatDiagnostics } from '../lib/format.js' import * as plugin from '../index.js' const DSH = '/usr/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai' test('columnOf supports identifiers, substrings and #N', () => { assert.equal(columnOf('const foo = foo + 1', 'foo'), 6) assert.equal(columnOf('const foo = foo + 1', 'foo#2'), 12) assert.equal(columnOf('a.b(c)', 'b(c'), 2) assert.equal(columnOf('x', 'nope'), -1) }) test('applyTextEdits applies utf-16 ranges bottom-up', () => { const t = 'let a = 1\nlet b = a\n' const out = applyTextEdits(t, [ { range: { start: { line: 0, character: 4 }, end: { line: 0, character: 5 } }, newText: 'x' }, { range: { start: { line: 1, character: 8 }, end: { line: 1, character: 9 } }, newText: 'x' }, ]) assert.equal(out, 'let x = 1\nlet b = x\n') }) async function harness() { const { default: LocalFileSystem } = await import(`${DSH}/dsh-fs-local/lib/index.js`) const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'omp-lsp-')) const fsImpl = Object.create(LocalFileSystem.prototype) Object.assign(fsImpl, { config: { cwd: dir, diffBasisMaxBytes: 1 << 24 }, internals: {}, locks: new Map() }) const listeners = new Map() const tools = new Map() const ctx = { fs: fsImpl, tools: { register(d) { tools.set(d.name, d); return () => tools.delete(d.name) }, get: (n) => tools.get(n) }, on(n, f) { listeners.set(n, [...(listeners.get(n) ?? []), f]); return () => {} }, emit() {}, effect(fn) { return fn() }, get() {}, systemPrompt: { section: () => () => {} }, } const agent = { session: { header: { cwd: dir } } } const post = async (name, args, value) => { const exec = { name, arguments: args, agent, signal: new AbortController().signal } const result = { isError: false, value, content: [{ type: 'text', text: 'ok' }] } const cbs = [...(listeners.get('tools/post-execute') ?? [])] const next = () => (cbs.shift() ?? (async () => ({ kind: 'accept' })))(exec, result, next) return next() } const lsp = (args) => tools.get('lsp').execute(args, { agent, signal: new AbortController().signal }) return { ctx, dir, post, lsp, tools } } const hasTs = !!which('typescript-language-server') const hasPy = !!which('pyright-langserver') test('writethrough appends TypeScript diagnostics after edit', { skip: !hasTs, timeout: 90_000 }, async () => { const h = await harness() fs.writeFileSync(path.join(h.dir, 'package.json'), '{"name":"t"}') fs.writeFileSync(path.join(h.dir, 'tsconfig.json'), '{"compilerOptions":{"strict":true,"noEmit":true}}') fs.writeFileSync(path.join(h.dir, 'a.ts'), 'const n: number = "x"\nexport {}\n') plugin.apply(h.ctx, { budgetMs: 30000 }) const d = await h.post('edit', { input: `[a.ts#ABCD]\nPUT 1.=1:\n+const n: number = "x"` }, { results: [{ path: 'a.ts', operation: 'update' }] }) const text = d.content.map((c) => c.text).join('\n') assert.match(text, /LSP diagnostics after this edit/) assert.match(text, /a\.ts:1:\d+ error.*not assignable/) // lsp tool: hover + definition + rename fs.writeFileSync(path.join(h.dir, 'b.ts'), 'export function greet(name: string) { return name }\nexport const r = greet("x")\n') const hover = await h.lsp({ action: 'hover', file: 'b.ts', line: 2, symbol: 'greet' }) assert.match(hover.text, /greet/) const def = await h.lsp({ action: 'definition', file: 'b.ts', line: 2, symbol: 'greet' }) assert.match(def.text, /b\.ts:1:/) const ren = await h.lsp({ action: 'rename', file: 'b.ts', line: 1, symbol: 'greet', new_name: 'welcome' }) assert.match(ren.text, /Renamed to welcome/) assert.match(fs.readFileSync(path.join(h.dir, 'b.ts'), 'utf8'), /welcome\("x"\)/) const st = await h.lsp({ action: 'status' }) assert.match(st.text, /typescript .*ready/) await h.lsp({ action: 'reload' }) fs.rmSync(h.dir, { recursive: true, force: true }) }) test('writethrough for python via pyright', { skip: !hasPy, timeout: 90_000 }, async () => { const h = await harness() fs.writeFileSync(path.join(h.dir, 'pyproject.toml'), '[project]\nname="t"\n') fs.writeFileSync(path.join(h.dir, 'm.py'), 'def f() -> int:\n return "x"\n') plugin.apply(h.ctx, { budgetMs: 30000 }) const d = await h.post('write', { file_path: 'm.py', content: '' }, { path: path.join(h.dir, 'm.py'), operation: 'update' }) const text = d.content.map((c) => c.text).join('\n') assert.match(text, /m\.py:2:\d+ error/) await h.lsp({ action: 'reload' }) fs.rmSync(h.dir, { recursive: true, force: true }) }) test('no server for unknown extension leaves result untouched', async () => { const h = await harness() fs.writeFileSync(path.join(h.dir, 'x.unknownext'), 'hi') plugin.apply(h.ctx, {}) const d = await h.post('write', { file_path: 'x.unknownext', content: 'hi' }, { path: 'x.unknownext' }) assert.equal(d.content, undefined) fs.rmSync(h.dir, { recursive: true, force: true }) }) test('formatDiagnostics filters by severity', () => { const items = [ { severity: 1, message: 'bad', range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } } }, { severity: 4, message: 'hint', range: { start: { line: 1, character: 0 }, end: { line: 1, character: 1 } } }, ] const f = formatDiagnostics('/x/a.ts', items, { cwd: '/x', minSeverity: 2 }) assert.equal(f.text, 'a.ts:1:1 error: bad') assert.equal(f.counts.hint, 1) }) test('pool status lists installed servers', () => { const p = new ServerPool({}) const s = p.status() assert.ok(s.servers.find((x) => x.id === 'typescript')) p.stopAll() })