Files
orca/src/main/ipc/filesystem-import-ssh.test.ts
T

284 lines
8.8 KiB
TypeScript

import path from 'path'
import { constants } from 'fs'
import { EventEmitter } from 'events'
import { Readable, Writable } from 'stream'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const handlers = new Map<string, (_event: unknown, args: unknown) => Promise<unknown>>()
const {
handleMock,
lstatMock,
mkdirMock,
realpathMock,
copyFileMock,
openMock,
readdirMock,
unlinkMock,
sftpExistsMock,
uploadFileMock,
uploadDirMock,
removeDirectorySftpMock,
mkdirSftpMock,
getConnMgrMock
} = vi.hoisted(() => ({
handleMock: vi.fn(),
lstatMock: vi.fn(),
mkdirMock: vi.fn(),
realpathMock: vi.fn(),
copyFileMock: vi.fn(),
openMock: vi.fn(),
readdirMock: vi.fn(),
unlinkMock: vi.fn(),
sftpExistsMock: vi.fn(),
uploadFileMock: vi.fn(),
uploadDirMock: vi.fn(),
removeDirectorySftpMock: vi.fn(),
mkdirSftpMock: vi.fn(),
getConnMgrMock: vi.fn()
}))
vi.mock('electron', () => ({ ipcMain: { handle: handleMock } }))
vi.mock('fs/promises', () => ({
lstat: lstatMock,
mkdir: mkdirMock,
rename: vi.fn(),
writeFile: vi.fn(),
realpath: realpathMock,
copyFile: copyFileMock,
open: openMock,
readdir: readdirMock,
unlink: unlinkMock,
rm: vi.fn()
}))
vi.mock('../ssh/sftp-upload', () => ({
sftpPathExists: sftpExistsMock,
uploadFile: uploadFileMock,
uploadDirectory: uploadDirMock,
removeDirectorySftp: removeDirectorySftpMock,
mkdirSftp: mkdirSftpMock
}))
vi.mock('./ssh', () => ({ getSshConnectionManager: getConnMgrMock }))
import { registerFilesystemMutationHandlers } from './filesystem-mutations'
const store = {
getRepos: () => [
{
id: 'r1',
path: path.resolve('/workspace/repo'),
displayName: 'repo',
badgeColor: '#000',
addedAt: 0
}
],
getSettings: () => ({ workspaceDir: path.resolve('/workspace') })
}
const enoent = (): Error => Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
type MockSftpWriteStream = EventEmitter & {
destroy: ReturnType<typeof vi.fn>
end: ReturnType<typeof vi.fn>
}
function createMockSftpWriteStream(): MockSftpWriteStream {
const stream = new EventEmitter() as MockSftpWriteStream
stream.destroy = vi.fn()
stream.end = vi.fn(() => stream.emit('close'))
return stream
}
describe('fs:importExternalPaths — SSH routing & connection', () => {
const destDir = '/home/user/project/src'
const connId = 'ssh-conn-1'
const sftpWriteStreams: MockSftpWriteStream[] = []
const mockSftp = {
end: vi.fn(),
createWriteStream: vi.fn(() => {
const stream = createMockSftpWriteStream()
sftpWriteStreams.push(stream)
return stream
})
}
const makeConn = (status = 'connected') => ({
getState: () => ({ status }),
sftp: vi.fn().mockResolvedValue(mockSftp)
})
const mockFile = (p: string): void => {
const rp = path.resolve(p)
lstatMock.mockImplementation(async (x: string) => {
if (x === rp) {
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
}
throw enoent()
})
}
const invoke = (args: Record<string, unknown>) =>
handlers.get('fs:importExternalPaths')!(null, args) as Promise<{
results: Record<string, unknown>[]
}>
beforeEach(() => {
handlers.clear()
;[
handleMock,
lstatMock,
mkdirMock,
realpathMock,
copyFileMock,
openMock,
readdirMock,
unlinkMock,
sftpExistsMock,
uploadFileMock,
uploadDirMock,
removeDirectorySftpMock,
mkdirSftpMock,
getConnMgrMock
].forEach((m) => m.mockReset())
mockSftp.end.mockReset()
mockSftp.createWriteStream.mockClear()
sftpWriteStreams.length = 0
handleMock.mockImplementation((ch: string, h: never) => {
handlers.set(ch, h)
})
realpathMock.mockImplementation(async (p: string) => p)
lstatMock.mockRejectedValue(enoent())
openMock.mockImplementation(async (_p: string, flags: unknown) => {
if (flags === 'wx') {
return {
createWriteStream: () =>
new Writable({
write(_chunk, _encoding, callback) {
callback()
}
}),
close: vi.fn().mockResolvedValue(undefined)
}
}
return {
stat: vi.fn().mockResolvedValue({
size: 12,
ino: 1,
dev: 1,
isFile: () => true
}),
createReadStream: () => Readable.from([Buffer.from('file-content')]),
close: vi.fn().mockResolvedValue(undefined)
}
})
unlinkMock.mockResolvedValue(undefined)
sftpExistsMock.mockResolvedValue(false)
uploadFileMock.mockResolvedValue(undefined)
uploadDirMock.mockResolvedValue(undefined)
removeDirectorySftpMock.mockResolvedValue(undefined)
mkdirSftpMock.mockResolvedValue(undefined)
registerFilesystemMutationHandlers(store as never)
})
it('routes to SFTP when connectionId is present', async () => {
getConnMgrMock.mockReturnValue({ getConnection: () => makeConn() })
mockFile('/tmp/dropped/file.txt')
const { results } = await invoke({
sourcePaths: ['/tmp/dropped/file.txt'],
destDir,
connectionId: connId
})
expect(results[0]).toMatchObject({ status: 'imported', kind: 'file' })
expect(uploadFileMock).toHaveBeenCalledWith(
mockSftp,
path.resolve('/tmp/dropped/file.txt'),
`${destDir}/file.txt`,
{ exclusive: true }
)
expect(copyFileMock).not.toHaveBeenCalled()
})
it('falls back to local import when connectionId is absent', async () => {
mockFile('/tmp/dropped/file.txt')
const { results } = await invoke({
sourcePaths: ['/tmp/dropped/file.txt'],
destDir: path.resolve('/workspace/repo/src')
})
expect(results[0]).toMatchObject({ status: 'imported' })
expect(openMock).toHaveBeenCalledWith(
path.resolve('/tmp/dropped/file.txt'),
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
)
})
it('returns empty results without opening SFTP', async () => {
const conn = makeConn()
getConnMgrMock.mockReturnValue({ getConnection: () => conn })
const { results } = await invoke({ sourcePaths: [], destDir, connectionId: connId })
expect(results).toHaveLength(0)
expect(conn.sftp).not.toHaveBeenCalled()
})
it('throws when connectionId has no matching connection', async () => {
getConnMgrMock.mockReturnValue({ getConnection: () => null })
await expect(
invoke({ sourcePaths: ['/tmp/x'], destDir, connectionId: connId })
).rejects.toThrow('No SSH connection')
})
it('throws when connection is reconnecting', async () => {
getConnMgrMock.mockReturnValue({ getConnection: () => makeConn('reconnecting') })
await expect(
invoke({ sourcePaths: ['/tmp/x'], destDir, connectionId: connId })
).rejects.toThrow('reconnecting')
})
it('throws when connection is not active', async () => {
getConnMgrMock.mockReturnValue({ getConnection: () => makeConn('disconnected') })
await expect(
invoke({ sourcePaths: ['/tmp/x'], destDir, connectionId: connId })
).rejects.toThrow('not active')
})
it('throws when conn.sftp() rejects', async () => {
const conn = makeConn()
conn.sftp.mockRejectedValue(new Error('SFTP subsystem not available'))
getConnMgrMock.mockReturnValue({ getConnection: () => conn })
await expect(
invoke({ sourcePaths: ['/tmp/x'], destDir, connectionId: connId })
).rejects.toThrow('SFTP subsystem')
})
it('closes SFTP channel after success', async () => {
getConnMgrMock.mockReturnValue({ getConnection: () => makeConn() })
mockFile('/tmp/dropped/file.txt')
await invoke({ sourcePaths: ['/tmp/dropped/file.txt'], destDir, connectionId: connId })
expect(mockSftp.end).toHaveBeenCalledOnce()
})
it('closes SFTP channel after upload error', async () => {
getConnMgrMock.mockReturnValue({ getConnection: () => makeConn() })
mockFile('/tmp/dropped/file.txt')
uploadFileMock.mockRejectedValue(new Error('disk full'))
const { results } = await invoke({
sourcePaths: ['/tmp/dropped/file.txt'],
destDir,
connectionId: connId
})
expect(results[0]).toMatchObject({ status: 'failed', reason: 'disk full' })
expect(mockSftp.end).toHaveBeenCalledOnce()
})
it('removes staging marker write listeners after remote stream close', async () => {
getConnMgrMock.mockReturnValue({ getConnection: () => makeConn() })
mockFile('/tmp/dropped/file.txt')
await invoke({
sourcePaths: ['/tmp/dropped/file.txt'],
destDir: '/home/user/project/.orca/drops',
connectionId: connId,
ensureDir: true
})
expect(mockSftp.createWriteStream).toHaveBeenCalledWith('/home/user/project/.orca/.gitignore')
expect(sftpWriteStreams[0]!.listenerCount('close')).toBe(0)
expect(sftpWriteStreams[0]!.listenerCount('error')).toBe(0)
expect(sftpWriteStreams[0]!.destroy).toHaveBeenCalledOnce()
})
})