1
0

integration.spec.ts 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { afterEach, beforeEach, describe, expect, it } from 'vitest'
  2. import { mkdtemp, mkdir, rm, writeFile, realpath } from 'node:fs/promises'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { pathToFileURL } from 'node:url'
  6. import { Context } from 'cordis'
  7. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  8. import ToolRegistry from '@deepseek-ai/dsh-tools'
  9. import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
  10. import Lsp from '@deepseek-ai/dsh-lsp'
  11. import * as LspLocal from '@deepseek-ai/dsh-lsp-local'
  12. import * as TimeoutPolicy from '@deepseek-ai/dsh-timeout-policy'
  13. import * as ToolLsp from '@deepseek-ai/dsh-tool-lsp'
  14. /**
  15. * Focused in-process integration of the model-facing tool, seam, local provider, and timeout policy.
  16. * The `lsp-definition` ACP snapshot owns the shipped Loader/app entry path.
  17. */
  18. let root: string
  19. let ws: string
  20. beforeEach(async () => {
  21. root = await realpath(await mkdtemp(join(tmpdir(), 'lsp-tool-int-')))
  22. ws = join(root, 'ws')
  23. await mkdir(ws)
  24. await writeFile(join(ws, 'a.ts'), 'const x = 1\n')
  25. })
  26. afterEach(async () => {
  27. await rm(root, { recursive: true, force: true })
  28. })
  29. /** An inline stdio server that answers initialize + definition; `hang` makes textDocument/* stall. */
  30. function serverScript(hang: boolean): string {
  31. const definition = JSON.stringify({ uri: pathToFileURL(join(ws, 'a.ts')).href, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 3 } } })
  32. return 'let b=Buffer.alloc(0);'
  33. + `const DEF=${definition};`
  34. + 'const fr=(o)=>{const x=Buffer.from(JSON.stringify({jsonrpc:"2.0",...o}));return Buffer.concat([Buffer.from(`Content-Length: ${x.length}\\r\\n\\r\\n`),x]);};'
  35. + 'process.stdin.on("data",c=>{b=Buffer.concat([b,c]);for(;;){const s=b.indexOf("\\r\\n\\r\\n");if(s<0)break;const len=Number(/(\\d+)/.exec(b.toString("ascii",0,s))[1]);if(b.length<s+4+len)break;const m=JSON.parse(b.toString("utf8",s+4,s+4+len));b=b.subarray(s+4+len);'
  36. + 'if(m.method==="initialize")process.stdout.write(fr({id:m.id,result:{capabilities:{positionEncoding:"utf-16",textDocumentSync:1,definitionProvider:true}}}));'
  37. + `else if(m.method==="textDocument/definition"){${hang ? '' : 'process.stdout.write(fr({id:m.id,result:DEF}));'}}`
  38. + 'else if(m.method==="shutdown")process.stdout.write(fr({id:m.id,result:null}));'
  39. + 'else if(m.method==="exit")process.exit(0);'
  40. + '}});'
  41. }
  42. async function mount(hang: boolean, timeoutMs?: number): Promise<Context> {
  43. const ctx = new Context()
  44. await ctx.plugin(SystemPrompt)
  45. await ctx.plugin(ToolRegistry)
  46. await ctx.plugin(Lsp)
  47. await ctx.plugin(LocalSubprocessService)
  48. await ctx.plugin(LspLocal, {
  49. servers: {
  50. inline: {
  51. command: process.execPath,
  52. args: ['-e', serverScript(hang)],
  53. extensionToLanguage: { '.ts': 'typescript' },
  54. shutdownTimeoutMs: 200,
  55. killGraceMs: 200,
  56. },
  57. },
  58. })
  59. await ctx.plugin(TimeoutPolicy)
  60. await ctx.plugin(ToolLsp, timeoutMs !== undefined ? { timeoutMs } : {})
  61. return ctx
  62. }
  63. let seq = 0
  64. const testToolSignal = new AbortController().signal
  65. function call(ctx: Context, args: unknown) {
  66. return ctx.tools.execute({
  67. signal: testToolSignal,
  68. callId: `int-${++seq}` as never,
  69. name: 'lsp',
  70. arguments: args,
  71. agent: { session: { header: { cwd: ws } } } as never,
  72. })
  73. }
  74. describe('tool-lsp integration', () => {
  75. it('round-trips a definition query through the real provider and renders a location', async () => {
  76. const ctx = await mount(false)
  77. const result = await call(ctx, { operation: 'goToDefinition', file_path: 'a.ts', line: 1, character: 7 })
  78. expect(result.isError).toBe(false)
  79. expect(result.content[0]).toEqual({ type: 'text', text: 'a.ts:1:1' })
  80. await ctx.fiber.dispose()
  81. }, 30_000)
  82. it('enforces the TOOL_TIMEOUT budget when the server hangs', async () => {
  83. const ctx = await mount(true, 300)
  84. const result = await call(ctx, { operation: 'goToDefinition', file_path: 'a.ts', line: 1, character: 7 })
  85. expect(result.isError).toBe(true)
  86. expect(result.error?.info?.code).toBe('TOOL_TIMEOUT')
  87. await ctx.fiber.dispose()
  88. }, 30_000)
  89. })