Implements the execa Agent Note's four sub-changes:
- execa (root devDep + loader-smoke dep) replaces the hand-rolled
spawn-collect-timeout choreography in loader-smoke, apps/cli and
cli-demo/acp-demo built-bin e2e, lsp-local and code-runtime-worker
built-lib e2e, the tui pty-harness outer collector, the jsonrpc
keyless smoke, and crash-recovery's child spawn. Genuinely custom
parts stay custom: cli-demo's interrupt-on-marker, jsonrpc's
line-predicate protocol driving, crash-recovery's SIGKILL-at-failpoint.
The two loader-smoke /* v8 ignore */ OS-error branches are gone.
- llm-mock-server CLI tokenizes via node:util parseArgs; numeric
coercion/bounds/cross-option constraints stay manual; pinned
error-message tests updated to the parseArgs texts.
- both loadRootEnv copies in apps/web/tests are deleted: the owning
vitest configs (web unconditionally, snapshot in record mode)
already load the repo-root .env before these files run.
- the four poll loops (acp-snapshot harness waits + crash-recovery
waitForFile) ride vi.waitFor with explicit {interval, timeout}.
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
import { existsSync } from 'node:fs'
|
|
import { mkdtemp, mkdir, rm, writeFile, realpath } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
import { execa } from 'execa'
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
/**
|
|
* Keyless built-artifact smoke: plain Node imports `@deepseek-ai/dsh-lsp` and
|
|
* `@deepseek-ai/dsh-lsp-local` by name through their exports maps, spawns the fixture server, runs
|
|
* one query (exercising real `Content-Length` framing over `lib/index.js`), and disposes (exercising
|
|
* subprocess cleanup). Unit tests use `src/`; this pins the downstream `lib/` path. Skips when `lib/`
|
|
* is absent; CI runs it after the build.
|
|
*/
|
|
|
|
const pkgDir = fileURLToPath(new URL('..', import.meta.url))
|
|
const seamLib = join(pkgDir, '../lsp/lib/index.js')
|
|
const built = existsSync(join(pkgDir, 'lib/index.js')) && existsSync(seamLib)
|
|
|
|
const fixtureServer = fileURLToPath(new URL('./fixture-server.ts', import.meta.url))
|
|
|
|
let root: string
|
|
let ws: string
|
|
|
|
beforeAll(async () => {
|
|
root = await realpath(await mkdtemp(join(tmpdir(), 'lsp-built-')))
|
|
ws = join(root, 'ws')
|
|
await mkdir(ws)
|
|
await writeFile(join(ws, 'a.ts'), 'const x = 1\n')
|
|
})
|
|
|
|
afterAll(async () => {
|
|
if (root) await rm(root, { recursive: true, force: true })
|
|
})
|
|
|
|
describe.skipIf(!built)('built lib real load path (plain node)', () => {
|
|
it('runs a query through lib/index.js and disposes cleanly, framing over the base protocol', async () => {
|
|
const location = JSON.stringify({ uri: pathToFileURL(join(ws, 'a.ts')).href, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } } })
|
|
const script = `
|
|
const { Context } = await import('cordis')
|
|
const { default: Lsp } = await import('@deepseek-ai/dsh-lsp')
|
|
const LspLocal = await import('@deepseek-ai/dsh-lsp-local')
|
|
const ctx = new Context()
|
|
await ctx.plugin(Lsp)
|
|
await ctx.plugin(LspLocal, {
|
|
servers: {
|
|
fake: {
|
|
command: ${JSON.stringify(process.execPath)},
|
|
args: [${JSON.stringify(fixtureServer)}],
|
|
env: { LSP_FAKE_DEF: ${JSON.stringify(location)} },
|
|
extensionToLanguage: { '.ts': 'typescript' },
|
|
},
|
|
},
|
|
})
|
|
const result = await ctx.lsp.query({ operation: 'goToDefinition', filePath: 'a.ts', position: { line: 0, character: 6 }, workspaceRoot: ${JSON.stringify(ws)} })
|
|
console.log(JSON.stringify(result))
|
|
await ctx.fiber.dispose()
|
|
`
|
|
const { exitCode, stdout, stderr } = await execa(process.execPath, ['--input-type=module', '-e', script], {
|
|
cwd: pkgDir,
|
|
stdin: 'ignore',
|
|
timeout: 55_000,
|
|
killSignal: 'SIGKILL',
|
|
reject: false,
|
|
})
|
|
|
|
expect(exitCode, `stderr:\n${stderr}`).toBe(0)
|
|
const lastLine = stdout.trim().split('\n').at(-1) ?? ''
|
|
const result = JSON.parse(lastLine) as { kind: string; locations: unknown[] }
|
|
expect(result.kind).toBe('locations')
|
|
expect(result.locations).toHaveLength(1)
|
|
}, 60_000)
|
|
})
|