Files

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()
})