122 lines
5.7 KiB
JavaScript
122 lines
5.7 KiB
JavaScript
// 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()
|
|
})
|