shell-discovery.spec.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. /** Shell facts and lookup failures retain remote execution semantics. */
  2. import { Context } from '@deepseek-ai/cordis'
  3. import { SubprocessExecutableNotFoundError } from '@deepseek-ai/dsh-subprocess'
  4. import { RemoteOperationError } from '@deepseek-ai/dsh-ssh/protocol'
  5. import { expect, it, vi } from 'vitest'
  6. import { z } from 'zod'
  7. import { SshSubprocessRuntime } from '../src/index.ts'
  8. it('validates remote shell facts and distinguishes a lookup miss from transport failure', async () => {
  9. const ctx = new Context()
  10. let facts: unknown = { platform: 'posix', defaultShell: '/remote/bin/zsh' }
  11. let failure: Error | undefined
  12. const request = vi.fn(async <T>(method: string, _params: unknown, schema: z.ZodType<T>): Promise<T> => {
  13. if (failure !== undefined) throw failure
  14. return schema.parse(method === 'terminal.environment' ? facts : '/remote/bin/bash')
  15. })
  16. ctx.provide('ssh', { request } as never)
  17. const runtime = new SshSubprocessRuntime(ctx)
  18. const signal = new AbortController().signal
  19. try {
  20. expect(await runtime.terminalEnvironment(signal)).toEqual(facts)
  21. expect(request).toHaveBeenCalledWith('terminal.environment', {}, expect.anything(), signal)
  22. facts = { platform: 'posix' }
  23. expect(await runtime.terminalEnvironment()).toEqual(facts)
  24. facts = { platform: 'untrusted' }
  25. await expect(runtime.terminalEnvironment()).rejects.toBeInstanceOf(z.ZodError)
  26. expect(await runtime.resolveExecutable('bash', undefined, signal)).toBe('/remote/bin/bash')
  27. failure = new RemoteOperationError('not installed', 'SUBPROCESS_EXECUTABLE_NOT_FOUND')
  28. await expect(runtime.resolveExecutable('absent')).rejects.toBeInstanceOf(SubprocessExecutableNotFoundError)
  29. failure = new RemoteOperationError('permission', 'EACCES')
  30. await expect(runtime.resolveExecutable('bash')).rejects.toBe(failure)
  31. failure = new Error('SSH disconnected')
  32. await expect(runtime.resolveExecutable('bash')).rejects.toBe(failure)
  33. } finally { await ctx.fiber.dispose() }
  34. })
  35. it.skipIf(process.platform === 'win32')('queries the real helper and resizes its PTY without replacing the shell', async () => {
  36. const { createHelperHarness } = await import('../../ssh/tests/fixtures/helper.ts')
  37. const helper = await createHelperHarness()
  38. const ctx = new Context()
  39. ctx.provide('ssh', helper.connection as never)
  40. const runtime = new SshSubprocessRuntime(ctx)
  41. try {
  42. expect(await runtime.terminalEnvironment()).toMatchObject({ platform: 'posix' })
  43. await expect(runtime.resolveExecutable(`${helper.root}/absent`)).rejects.toBeInstanceOf(SubprocessExecutableNotFoundError)
  44. const terminal = await runtime.spawnTerminal({ argv: ['/bin/sh', '-i'], cwd: helper.root, cols: 80, rows: 24, terminalType: 'xterm-256color', graceMs: 100, shellActivity: true })
  45. let output = ''
  46. terminal.output.on('data', (chunk: Buffer) => { output += chunk.toString() })
  47. expect((await terminal.inspectActivity()).state).toBe('unknown')
  48. await terminal.resize(120, 40)
  49. await terminal.write('printf "REMOTE_TERM:%s\\n" "$TERM"; stty size\n')
  50. await expect.poll(() => output).toContain('REMOTE_TERM:xterm-256color')
  51. await expect.poll(() => output).toMatch(/40\s+120/u)
  52. await terminal.terminate()
  53. await terminal.done
  54. } finally {
  55. try { await ctx.fiber.dispose() } finally { await helper.close() }
  56. }
  57. })