Explorar el Código

test(ssh): isolate backend availability from helper dispatch

Tianyi Cui hace 4 semanas
padre
commit
b5fd914306

+ 1 - 2
packages/ssh/ssh/src/helper-processes.ts

@@ -327,8 +327,7 @@ export class RemoteProcesses {
   }
 
   private async release(id: SshProcessId): Promise<void> {
-    const record = this.records.get(id)
-    if (record === undefined) return
+    const record = this.record(id)
     record.release ??= (async () => {
       clearTimeout(record.expiry)
       record.controller.abort(new Error('SSH process reservation closed'))

+ 8 - 3
packages/ssh/ssh/tests/helper-runtime.spec.ts

@@ -1,6 +1,7 @@
 /** Real helper dispatch over private in-memory transport, without changing the Harness process cwd. */
 import { symlink, writeFile } from 'node:fs/promises'
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
+import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
 import { z } from 'zod'
 import { createHelperHarness as helper } from './fixtures/helper.ts'
 import { targetSchema, writeResultSchema, editResultSchema, infoSchema, entriesSchema } from '../src/schemas.ts'
@@ -85,12 +86,16 @@ describe.skipIf(process.platform === 'win32')('SSH helper runtime', () => {
 
   it('refuses unconfined sandbox requests and resolves executables in the helper world', async () => {
     const test = await helper()
+    const confine = vi.spyOn(LocalSandboxProvider.prototype, 'confine').mockImplementation(async argv => ({
+      argv: ['confined', ...argv], enforcement: 'full',
+    }))
     try {
       await expect(test.client.request('sandbox', { argv: ['true'], policy: { mode: 'danger-full-access', workspaceRoot: test.root } }, z.unknown())).rejects.toThrow('does not need')
       expect(await test.client.request('executable', { command: process.execPath, env: { REMOVED: null } }, z.string())).toBe(process.execPath)
       expect(await test.client.request('executable', { command: process.execPath }, z.string())).toBe(process.execPath)
       const wrapped = await test.client.request('sandbox', { argv: ['true'], policy: policy(test.root) }, z.looseObject({ argv: z.array(z.string()), enforcement: z.enum(['full', 'partial']) }))
-      expect(wrapped.argv.at(-1)).toBe('true')
-    } finally { await test.close() }
+      expect(wrapped.argv).toEqual(['confined', 'true'])
+      expect(confine).toHaveBeenCalledWith(['true'], policy(test.root), expect.any(AbortSignal))
+    } finally { confine.mockRestore(); await test.close() }
   })
 })

+ 7 - 1
packages/ssh/ssh/tests/stream-rebind.spec.ts

@@ -8,12 +8,13 @@ import { describe, expect, it } from 'vitest'
 import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
 import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
 import LocalSandboxProvider from '@deepseek-ai/dsh-sandbox-local'
+import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
 import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
 import { RemoteProcesses } from '../src/helper-processes.ts'
 import { authenticateStream } from '../src/stream-security.ts'
 
 describe.skipIf(process.platform === 'win32')('SSH stream pathname replacement', () => {
-  it('keeps the key and payload private when a workspace-write process rebinds and relays the listener', async () => {
+  it('keeps the key and payload private when a workspace-write process rebinds and relays the listener', async ({ skip }) => {
     const root = await realpath(await mkdtemp('/tmp/dsh-ssh-rebind-'))
     const ctx = new Context()
     const fs = ctx.plugin(LocalFileSystem, { cwd: root })
@@ -24,6 +25,11 @@ describe.skipIf(process.platform === 'win32')('SSH stream pathname replacement',
     const sockets: Socket[] = []
     let attacker: SubprocessHandle | undefined
     try {
+      try { await ctx.sandbox.confine(['true'], { mode: 'workspace-write', workspaceRoot: root }) }
+      catch (error) {
+        if (error instanceof SandboxUnavailableError) skip('No local process confinement backend is available')
+        throw error
+      }
       const prepared = await owner.prepare({
         argv: [process.execPath, '-e', 'const s=new(require(\'node:net\').Socket)({fd:7,readable:true,writable:true});const chunks=[];s.on(\'data\',v=>chunks.push(v));s.on(\'end\',()=>s.end(Buffer.concat(chunks)));'],
         cwd: root, graceMs: 500,