|
|
@@ -0,0 +1,146 @@
|
|
|
+import { spawn } from 'node:child_process'
|
|
|
+import { createServer } from 'node:http'
|
|
|
+import { mkdtemp, rm } from 'node:fs/promises'
|
|
|
+import { tmpdir } from 'node:os'
|
|
|
+import { join } from 'node:path'
|
|
|
+import { fileURLToPath } from 'node:url'
|
|
|
+import { describe, expect, it } from 'vitest'
|
|
|
+
|
|
|
+const binScript = fileURLToPath(new URL('../../../packages/examples/jsonrpc-demo/src/bin.ts', import.meta.url))
|
|
|
+const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
|
|
+const repoRoot = fileURLToPath(new URL('../../..', import.meta.url))
|
|
|
+
|
|
|
+function waitForLine(
|
|
|
+ lines: string[],
|
|
|
+ predicate: (value: Record<string, unknown>) => boolean,
|
|
|
+ stderr: () => string,
|
|
|
+): Promise<Record<string, unknown>> {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const deadline = Date.now() + 30_000
|
|
|
+ const poll = (): void => {
|
|
|
+ while (lines.length > 0) {
|
|
|
+ const line = lines.shift()!
|
|
|
+ if (!line.trim()) continue
|
|
|
+ try {
|
|
|
+ const value = JSON.parse(line) as Record<string, unknown>
|
|
|
+ if (predicate(value)) {
|
|
|
+ resolve(value)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ reject(new Error(`non-JSON stdout from DSBench runtime: ${line}`))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (Date.now() >= deadline) {
|
|
|
+ reject(new Error(`timed out waiting for JSON-RPC response; stderr=${stderr()}`))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ setTimeout(poll, 10)
|
|
|
+ }
|
|
|
+ poll()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+describe('dsbench-coding-agent keyless smoke', () => {
|
|
|
+ it('boots the real Cordis tree and serves initialize/shutdown over clean stdout', async () => {
|
|
|
+ const root = await mkdtemp(join(tmpdir(), 'dsh-dsbench-smoke-'))
|
|
|
+ const modelRequests: Record<string, unknown>[] = []
|
|
|
+ const modelServer = createServer((request, response) => {
|
|
|
+ let body = ''
|
|
|
+ request.setEncoding('utf8')
|
|
|
+ request.on('data', (chunk: string) => { body += chunk })
|
|
|
+ request.on('end', () => {
|
|
|
+ modelRequests.push(JSON.parse(body) as Record<string, unknown>)
|
|
|
+ response.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
|
+ response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n')
|
|
|
+ response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n')
|
|
|
+ response.write('data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n')
|
|
|
+ response.end('data: [DONE]\n\n')
|
|
|
+ })
|
|
|
+ })
|
|
|
+ await new Promise<void>(resolve => modelServer.listen(0, '127.0.0.1', resolve))
|
|
|
+ const address = modelServer.address()
|
|
|
+ if (address === null || typeof address === 'string') throw new Error('model server did not bind a TCP port')
|
|
|
+ const child = spawn(process.execPath, [
|
|
|
+ '--expose-internals',
|
|
|
+ '--import',
|
|
|
+ 'tsx',
|
|
|
+ binScript,
|
|
|
+ configPath,
|
|
|
+ ], {
|
|
|
+ cwd: repoRoot,
|
|
|
+ env: {
|
|
|
+ ...process.env,
|
|
|
+ DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
|
|
+ DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
|
|
|
+ DSH_CWD: root,
|
|
|
+ DSH_SESSION_ROOT: join(root, '.sessions'),
|
|
|
+ },
|
|
|
+ stdio: ['pipe', 'pipe', 'pipe'],
|
|
|
+ })
|
|
|
+ const lines: string[] = []
|
|
|
+ let stdoutBuffer = ''
|
|
|
+ let stderr = ''
|
|
|
+ child.stdout.setEncoding('utf8')
|
|
|
+ child.stdout.on('data', (chunk: string) => {
|
|
|
+ stdoutBuffer += chunk
|
|
|
+ const parts = stdoutBuffer.split('\n')
|
|
|
+ stdoutBuffer = parts.pop() ?? ''
|
|
|
+ lines.push(...parts)
|
|
|
+ })
|
|
|
+ child.stderr.setEncoding('utf8')
|
|
|
+ child.stderr.on('data', (chunk: string) => { stderr += chunk })
|
|
|
+
|
|
|
+ try {
|
|
|
+ child.stdin.write(`${JSON.stringify({
|
|
|
+ jsonrpc: '2.0',
|
|
|
+ id: 1,
|
|
|
+ method: 'initialize',
|
|
|
+ params: { cwd: root, model: 'deepseek-v4-pro' },
|
|
|
+ })}\n`)
|
|
|
+ const initialized = await waitForLine(lines, value => value.id === 1, () => stderr)
|
|
|
+ expect(initialized).toMatchObject({
|
|
|
+ jsonrpc: '2.0',
|
|
|
+ id: 1,
|
|
|
+ result: { serverInfo: { name: 'deepseek-harness-sdk-runtime' } },
|
|
|
+ })
|
|
|
+
|
|
|
+ child.stdin.write(`${JSON.stringify({
|
|
|
+ jsonrpc: '2.0',
|
|
|
+ id: 2,
|
|
|
+ method: 'session/prompt',
|
|
|
+ params: { sessionId: 'main', contentBlocks: [{ type: 'text', text: 'inspect tools' }] },
|
|
|
+ })}\n`)
|
|
|
+ const prompt = await waitForLine(lines, value => value.id === 2, () => stderr)
|
|
|
+ expect(prompt).toMatchObject({ jsonrpc: '2.0', id: 2, result: { accepted: true } })
|
|
|
+ const tools = modelRequests[0]?.tools as { function?: { name?: string } }[]
|
|
|
+ expect(tools.map(tool => tool.function?.name).sort()).toEqual([
|
|
|
+ 'bash',
|
|
|
+ 'edit',
|
|
|
+ 'read',
|
|
|
+ 'subagent',
|
|
|
+ 'todo_write',
|
|
|
+ 'write',
|
|
|
+ ])
|
|
|
+
|
|
|
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
|
|
|
+ const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr)
|
|
|
+ expect(shutdown).toMatchObject({ jsonrpc: '2.0', id: 3, result: {} })
|
|
|
+ if (child.exitCode === null) {
|
|
|
+ await new Promise<void>((resolve, reject) => {
|
|
|
+ child.once('exit', (code) => {
|
|
|
+ if (code === 0) resolve()
|
|
|
+ else reject(new Error(`runtime exited ${code}; stderr=${stderr}`))
|
|
|
+ })
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ expect(child.exitCode, stderr).toBe(0)
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (child.exitCode === null) child.kill('SIGKILL')
|
|
|
+ await new Promise<void>(resolve => modelServer.close(() => { resolve() }))
|
|
|
+ await rm(root, { recursive: true, force: true })
|
|
|
+ }
|
|
|
+ }, 40_000)
|
|
|
+})
|