فهرست منبع

test(llm): cover the DeepSeek and pi-ai request paths with egress tests

Both adapters' inference requests were argued from a code read and, for
DeepSeek, from one product smoke. Drive each shipping adapter at an
unresolvable endpoint through a fake proxy instead, and cover pi-ai's provider
stream rather than only its model discovery.
Yichen Jiang 4 هفته پیش
والد
کامیت
ca20b9af11

+ 9 - 8
packages/llm/llm-deepseek/package.json

@@ -52,23 +52,24 @@
     "@deepseek-ai/schemastery": "workspace:^"
   },
   "devDependencies": {
+    "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-attachment": "workspace:^",
+    "@deepseek-ai/dsh-anonymous-user-id": "workspace:^",
     "@deepseek-ai/dsh-atomic-write": "workspace:^",
+    "@deepseek-ai/dsh-attachment": "workspace:^",
     "@deepseek-ai/dsh-brand": "workspace:^",
     "@deepseek-ai/dsh-credentials": "workspace:^",
     "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^",
     "@deepseek-ai/dsh-fs": "workspace:^",
-    "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:^",
-    "@deepseek-ai/dsh-launch-environment": "workspace:^",
+    "@deepseek-ai/dsh-home-paths": "workspace:^",
+    "@deepseek-ai/dsh-http-proxy": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/dsh-launch-environment": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-home-paths": "workspace:^",
-    "@deepseek-ai/dsh-settings": "workspace:^",
+    "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-log-deepseek": "workspace:^",
-    "@deepseek-ai/dsh-timeout": "workspace:^",
-    "@deepseek-ai/dsh-anonymous-user-id": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/dsh-settings": "workspace:^",
+    "@deepseek-ai/dsh-timeout": "workspace:^"
   }
 }

+ 69 - 0
packages/llm/llm-deepseek/tests/egress.spec.ts

@@ -0,0 +1,69 @@
+import { createServer, type Server } from 'node:http'
+import type { AddressInfo } from 'node:net'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import { installGlobalProxy, type ProxyPolicy } from '@deepseek-ai/dsh-http-proxy'
+
+let seen: string[] = []
+let proxy: Server
+let proxyUrl: string
+
+beforeAll(async () => {
+  proxy = createServer((request, response) => {
+    seen.push(`REQ ${request.url ?? ''}`)
+    response.writeHead(502); response.end('fake-proxy')
+  })
+  proxy.on('connect', (request, socket) => {
+    seen.push(`CONNECT ${request.url ?? ''}`)
+    socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.end()
+  })
+  const a = await new Promise<AddressInfo>((r) => { proxy.listen(0, '127.0.0.1', () => { r(proxy.address() as AddressInfo) }) })
+  proxyUrl = `http://127.0.0.1:${String(a.port)}`
+})
+afterAll(async () => { await new Promise<void>((r) => { proxy.close(() => { r() }) }) })
+
+function policy(): ProxyPolicy {
+  return { httpProxy: proxyUrl, httpsProxy: proxyUrl, noProxy: '', source: 'env' }
+}
+async function observe(run: () => Promise<unknown>): Promise<string[]> {
+  seen = []
+  const dispose = await installGlobalProxy(policy())
+  try { await run().catch(() => undefined) } finally { await dispose() }
+  return seen
+}
+import { mkdtempSync, rmSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { vi } from 'vitest'
+import { Context } from '@deepseek-ai/cordis'
+import LlmRuntime from '@deepseek-ai/dsh-llm'
+import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
+import * as LlmDeepSeek from '../src/index.ts'
+
+let home: string
+beforeAll(() => {
+  home = mkdtempSync(join(tmpdir(), 'dsh-deepseek-egress-'))
+  vi.stubEnv('DSH_HOME', home)
+  vi.stubEnv('DEEPSEEK_API_KEY', 'probe-key')
+})
+afterAll(() => {
+  vi.unstubAllEnvs()
+  rmSync(home, { recursive: true, force: true })
+})
+
+/** Drive the shipping adapter's chat-completions request at an unresolvable endpoint. */
+async function streamOnce(): Promise<void> {
+  const ctx = new Context()
+  await ctx.plugin(LlmRuntime)
+  await ctx.plugin(DeepSeekLlmApiExtensionRegistry)
+  await ctx.plugin(LlmDeepSeek, { baseURL: 'http://deepseek-probe.invalid/v1', models: [{ id: 'm' }] })
+  for await (const _chunk of ctx.llm.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) {
+    // The endpoint never answers; the proxy record is the assertion.
+  }
+}
+
+describe('llm-deepseek egress', () => {
+  it('sends the chat-completions request through the proxy', async () => {
+    const observed = await observe(streamOnce)
+    expect(observed.join('|')).toContain('deepseek-probe.invalid')
+  })
+})

+ 3 - 0
packages/llm/llm-deepseek/tsconfig.json

@@ -55,6 +55,9 @@
     },
     {
       "path": "../../identity/anonymous-user-id"
+    },
+    {
+      "path": "../../net/http-proxy"
     }
   ]
 }

+ 28 - 0
packages/llm/llm-pi-ai/tests/egress.spec.ts

@@ -30,10 +30,38 @@ async function observe(run: () => Promise<unknown>): Promise<string[]> {
   try { await run().catch(() => undefined) } finally { await dispose() }
   return seen
 }
+import { vi } from 'vitest'
+import { Context } from '@deepseek-ai/cordis'
+import LlmRuntime from '@deepseek-ai/dsh-llm'
+import * as LlmPiAi from '../src/index.ts'
 import { discoverModels } from '../src/discovery.ts'
+
+/** Drive the shipping adapter's provider stream at an unresolvable endpoint. */
+async function streamOnce(): Promise<void> {
+  vi.stubEnv('PI_TEST_KEY', 'probe-key')
+  try {
+    const ctx = new Context()
+    await ctx.plugin(LlmRuntime)
+    await ctx.plugin(LlmPiAi, {
+      providers: { deepseek: { apiKeyEnv: 'PI_TEST_KEY', baseURL: 'http://pi-stream-probe.invalid' } },
+    })
+    for await (const _chunk of ctx.llm.stream({ provider: 'deepseek', model: 'deepseek-v4-flash', messages: [] })) {
+      // The endpoint never answers; the proxy record is the assertion.
+    }
+  } finally {
+    vi.unstubAllEnvs()
+  }
+}
 describe('pi-ai discovery egress', () => {
   it('goes through the proxy', async () => {
     const observed = await observe(() => discoverModels({ baseURL: 'http://pi-probe.invalid/v1', api: 'openai-completions', apiKey: 'probe' }))
     expect(observed.join('|')).toContain('pi-probe.invalid')
   })
 })
+
+describe('pi-ai provider stream egress', () => {
+  it('sends the inference request through the proxy', async () => {
+    const observed = await observe(streamOnce)
+    expect(observed.join('|')).toContain('pi-stream-probe.invalid')
+  })
+})

+ 3 - 0
pnpm-lock.yaml

@@ -6443,6 +6443,9 @@ importers:
       '@deepseek-ai/dsh-home-paths':
         specifier: workspace:^
         version: link:../../util/home-paths
+      '@deepseek-ai/dsh-http-proxy':
+        specifier: workspace:^
+        version: link:../../net/http-proxy
       '@deepseek-ai/dsh-invariants':
         specifier: workspace:^
         version: link:../../runtime-diagnostics/invariants